As specified in the comment, argc
and argv
are provided on the stack, so you cannot use a regular C function to get them, even with inline assembly, as the compiler will touch the stack pointer to allocate the local variables, setup the stack frame & co.; hence, _start
must be written in assembly, as it's done in glibc (x86; x86_64). A small stub can be written to just grab the stuff and forward it to your "real" C entrypoint according to the regular calling convention.
Here a minimal example of a program (both for x86 and x86_64) that reads argc
and argv
, prints all the values in argv
on stdout (separated by newline) and exits using argc
as status code; it can be compiled with the usual gcc -nostdlib
(and -static
to make sure ld.so
isn't involved; not that it does any harm here).
#ifdef __x86_64__
asm(
".global _start
"
"_start:
"
" xorl %ebp,%ebp
" // mark outermost stack frame
" movq 0(%rsp),%rdi
" // get argc
" lea 8(%rsp),%rsi
" // the arguments are pushed just below, so argv = %rbp + 8
" call bare_main
" // call our bare_main
" movq %rax,%rdi
" // take the main return code and use it as first argument for...
" movl $60,%eax
" // ... the exit syscall
" syscall
"
" int3
"); // just in case
asm(
"bare_write:
" // write syscall wrapper; the calling convention is pretty much ok as is
" movq $1,%rax
" // 1 = write syscall on x86_64
" syscall
"
" ret
");
#endif
#ifdef __i386__
asm(
".global _start
"
"_start:
"
" xorl %ebp,%ebp
" // mark outermost stack frame
" movl 0(%esp),%edi
" // argc is on the top of the stack
" lea 4(%esp),%esi
" // as above, but with 4-byte pointers
" sub $8,%esp
" // the start starts 16-byte aligned, we have to push 2*4 bytes; "waste" 8 bytes
" pushl %esi
" // to keep it aligned after pushing our arguments
" pushl %edi
"
" call bare_main
" // call our bare_main
" add $8,%esp
" // fix the stack after call (actually useless here)
" movl %eax,%ebx
" // take the main return code and use it as first argument for...
" movl $1,%eax
" // ... the exit syscall
" int $0x80
"
" int3
"); // just in case
asm(
"bare_write:
" // write syscall wrapper; convert the user-mode calling convention to the syscall convention
" pushl %ebx
" // ebx is callee-preserved
" movl 8(%esp),%ebx
" // just move stuff from the stack to the correct registers
" movl 12(%esp),%ecx
"
" movl 16(%esp),%edx
"
" mov $4,%eax
" // 4 = write syscall on i386
" int $0x80
"
" popl %ebx
" // restore ebx
" ret
"); // notice: the return value is already ok in %eax
#endif
int bare_write(int fd, const void *buf, unsigned count);
unsigned my_strlen(const char *ch) {
const char *ptr;
for(ptr = ch; *ptr; ++ptr);
return ptr-ch;
}
int bare_main(int argc, char *argv[]) {
for(int i = 0; i < argc; ++i) {
int len = my_strlen(argv[i]);
bare_write(1, argv[i], len);
bare_write(1, "
", 1);
}
return argc;
}
Notice that here several subtleties are ignored - in particular, the atexit
bit. All the documentation about the machine-specific startup state has been extracted from the comments in the two glibc files linked above.
与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…