This is an echo
program with no runtime or standard library. It's meant to be compiled with -nostdlib
on an amd64 Linux system.
static signed long mywrite(int fd, const void *buf, unsigned long count) {
signed long retval;
__asm__ __volatile__(
"syscall" :
"=a" (retval) :
"a" (1), "D" (fd), "S" (buf), "d" (count) :
"rcx", "r11", "memory"
);
return retval;
}
static void myexit(int status) __attribute__((__noreturn__));
static void myexit(int status) {
__asm__ __volatile__(
"syscall" :
:
"a" (60), "D" (status) :
);
__builtin_unreachable();
}
static unsigned long mystrlen(const char *str) {
const char *pos = str;
while(*pos) ++pos;
return pos - str;
}
static void writearg(char *str, char end) {
unsigned long size = mystrlen(str) + 1;
unsigned long written = 0;
str[size - 1] = end;
do {
signed long result = mywrite(1, str + written, size - written);
if(result < 0) myexit(1);
written += result;
} while(written < size);
}
void _start(void) __attribute__((__naked__, __noreturn__));
void _start(void) {
__asm__(
"pop %rdi\n\t"
"mov %rsp, %rsi\n\t"
"jmp mymain"
);
}
static void mymain(int argc, char *argv[]) __attribute__((__noreturn__, __used__));
static void mymain(int argc, char *argv[]) {
if(argc <= 1) {
myexit(mywrite(1, "\n", 1) != 1);
}
int i;
for(i = 1; i < argc - 1; ++i) {
writearg(argv[i], ' ');
}
writearg(argv[i], '\n');
myexit(0);
}
Some of my concerns:
- Is my program's behavior fully compliant with the standard for
echo
? - Am I making any unwarranted assumptions that could make my code not work on a future release of Linux (or compiler)? In particular, are popping
argc
and the way I'm overwriting null terminators inargv
values okay? - Since my code is Linux-on-amd64-only anyway, are there any other assumptions that I can make? For example, can I assume that Linux will always have continuous
argv
values, and so just make one bigwrite
call after swapping out all the nulls, instead of one per argument? (I know I'd still have to loopwrite
in case of partial writes. I also know I could just copy the strings around myself, but I'd rather write them from where I got them.) - Instead of having
_start
as an assembly stub and my real code inmymain
, is there any way I can put my real code in_start
but still be able to safely get ahold of the command-line arguments?
unsigned long
for pointer differences? Does Linux specify that? I'd expectsize_t
orptrdiff_t
. \$\endgroup\$typedef
it myself, in which case there's no portability win, or#include
a header from the standard library to get it, which defeats the purpose of what I did entirely. \$\endgroup\$