As a quick and dirty hack, you can make an executable with a compiled C function as the ELF entry point. Just make sure you use exit
or _exit
instead of returning.
(Link with gcc -nostartfiles
to omit CRT but still link other libraries, and write a _start()
in C. Beware of ABI violations like stack alignment, e.g. use -mincoming-stack-boundary=2
or an __attribte__
on _start
, as in Compiling without libc)
If it's dynamically linked, you can still use glibc functions on Linux (because the dynamic linker runs glibc's init functions). Not all systems are like this, e.g. on cygwin you definitely can't call libc functions if you (or the CRT start code) hasn't called the libc init functions in the correct order. I'm not sure it's even guaranteed that this works on Linux, so don't depend on it except for experimentation on your own system.
I have used a C _start(void){ ... }
+ calling _exit()
for making a static executable to microbenchmark some compiler-generated code with less startup overhead for perf stat ./a.out
.
Glibc's _exit()
works even if glibc wasn't initialized (gcc -O3 -static
), or use inline asm to run xor %edi,%edi
/ mov $60, %eax
/ syscall
(sys_exit(0) on Linux) so you don't have to even statically link libc. (gcc -O3 -nostdlib
)
With even more dirty hacking and UB, you can access argc and argv by knowing the x86-64 System V ABI that you're compiling for (see @zwol's answer for a quote from ABI doc), and how the process startup state differers from the function calling convention:
argc
is where the return address would be for a normal function (pointed to by RSP). GNU C has a builtin for accessing the return address of the current function (or for walking up the stack.)
argv[0]
is where the 7th integer/pointer arg should be (the first stack arg, just above the return address). It happens to / seems to work to take its address and use that as an array!
// Works only for the x86-64 SystemV ABI; only tested on Linux.
// DO NOT USE THIS EXCEPT FOR EXPERIMENTS ON YOUR OWN COMPUTER.
#include <stdio.h>
#include <stdlib.h>
// tell gcc *this* function is called with a misaligned RSP
__attribute__((force_align_arg_pointer))
void _start(int dummy1, int dummy2, int dummy3, int dummy4, int dummy5, int dummy6, // register args
char *argv0) {
int argc = (int)(long)__builtin_return_address(0); // load (%rsp), casts to silence gcc warnings.
char **argv = &argv0;
printf("argc = %d, argv[argc-1] = %s\n", argc, argv[argc-1]);
printf("%f\n", 1.234); // segfaults if RSP is misaligned
exit(0);
//_exit(0); // without flushing stdio buffers!
}
# with a version without the FP printf
peter@volta:~/src/SO$ gcc -nostartfiles _start.c -o bare_start
peter@volta:~/src/SO$ ./bare_start
argc = 1, argv[argc-1] = ./bare_start
peter@volta:~/src/SO$ ./bare_start abc def hij
argc = 4, argv[argc-1] = hij
peter@volta:~/src/SO$ file bare_start
bare_start: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=af27c8416b31bb74628ef9eec51a8fc84e49550c, not stripped
# I could have used -fno-pie -no-pie to make a non-PIE executable
This works with or without optimization, with gcc7.3. I was worried that without optimization, the address of argv0
would be below rbp
where it copies the arg, rather than its original location. But apparently it works.
gcc -nostartfiles
links glibc but not the CRT start files.
gcc -nostdlib
omits both libraries and CRT startup files.
Very little of this is guaranteed to work, but it does in practice work with current gcc on current x86-64 Linux, and has worked in the past for years. If it breaks, you get to keep both pieces. IDK what C features are broken by omitting the CRT startup code and just relying on the dynamic linker to run glibc init functions. Also, taking the address of an arg and accessing pointers above it is UB, so you could maybe get broken code-gen. gcc7.3 happens to do what you'd expect in this case.
Things that definitely break
atexit()
cleanup, e.g. flushing stdio buffers.
- static destructors for static objects in dynamically-linked libraries. (On entry to
_start
, RDX is a function pointer you should register with atexit for this reason. In a dynamically linked executable, the dynamic linker runs before your _start
and sets RDX before jumping to your _start
. Statically linked executables have RDX=0 under Linux.)
gcc -mincoming-stack-boundary=3
(i.e. 2^3 = 8 bytes) is another way to get gcc to realign the stack, because the -mpreferred-stack-boundary=4
default of 2^4 = 16 is still in place. But that makes gcc assume under-aligned RSP for all functions, not just for _start
, which is why I looked in the docs and found an attribute that was intended for 32-bit when the ABI transitioned from only requiring 4-byte stack alignment to the current requirement of 16-byte alignment for ESP
in 32-bit mode.
The SysV ABI requirement for 64-bit mode has always been 16-byte alignment, but gcc options let you make code that doesn't follow the ABI.
// test call to a function the compiler can't inline
// to see if gcc emits extra code to re-align the stack
// like it would if we'd used -mincoming-stack-boundary=3 to assume *all* functions
// have only 8-byte (2^3) aligned RSP on entry, with the default -mpreferred-stack-boundary=4
void foo() {
int i = 0;
atoi(NULL);
}
With -mincoming-stack-boundary=3
, we get stack-realignment code there, where we don't need it. gcc's stack-realignment code is pretty clunky, so we'd like to avoid that. (Not that you'd really ever use this to compile a significant program where you care about efficiency, please only use this stupid computer trick as a learning experiment.)
But anyway, see the code on the Godbolt compiler explorer with and without -mpreferred-stack-boundary=3
.
_start
you have nothing, the only way to get the command line is to ask the OS for it. Either way if you're going to do this in ASM you'll need to make a system call. – Arezzoargv
andargc
are on the stack, not in registers. – Toshikotoss_start
straight in assembly and perform the handoff to a C function using the regular C calling convention. From there onwards, it's all C. Otherwise, I think you may be able to do something with anaked
function, but then again, you want to get the arguments and forward them to a "normal" C function using the regular ABI immediately. – Toshikotoss_start
. – SaharaRead from file /proc/self/cmdline
, Is this the real way? – Cacie