> I wish doing really basic things without libc on Linux didn’t require so much
> assembly.
GCC/Clang provide the builtin __builtin_frame_address(), which I’ve used in the
past to get argc/argv in pure C. I’m not entirely sure if it’s guaranteed to work,
but it worked for me across GCC and Clang, with or without optimizations.
Here’s the (old, untested for a couple years) code:
__attribute__ ((force_align_arg_pointer))
_Noreturn void _start() {
// find arguments: per the SysV ABI, these are placed on the stack when
// _start is invoked. Conveniently, gcc/clang give us __builtin_frame_address,
// which gives us a stable way to access the memory above our stack frame
// without worrying about things like the size of our stack frame or
// whether the compiler might optimize out rbp.
// This +1 offset is inexplicable: my reading of the ABI is that argc should
// be exactly at %rsp when the entry point is called, but actually there are
// 8 zero bytes first. Either I'm misunderstanding something (likely), or the
// ABI doesn't match what Linux actually does.
long* argc_ptr = ((long*) __builtin_frame_address(0)) + 1;
long argc = *argc_ptr;
char** argv = (char**) (argc_ptr + 1);
Interesting, thanks! I had tried something like this myself, but couldn't
get it working reliably. However, your example works in every case I've
tried on x86-64. Maybe I was using the wrong built-in before. I was also
able to grab the environment pointer:
char **envp = (char **)(argc_ptr + 1 + argc + 1);
Perhaps this will be useful! It also works on ARM64 at -O1 and higher, but
not at -O0 or -Og. Unfortunate since ultimately it would be nice to have a
pure C entry point rather than special _start assembly for most targets.
Here's my test program:
https://gist.github.com/skeeto/e7ff8823d8801a5b483c109fc84b6ab5