Peter gave a very good answer, and I'm going to followup with a response that is cringe worthy and might garner some down votes. When linking directly with LD or indirectly with GCC, the default entry point for ELF executables is the label _start
.
Your NASM code uses a global label _start
so when your program is run the first code in your program will be the instructions of _start
. When using GCC your program's typical entry point is the function main
. What is hidden from you is that your C program also has a _start
label but it is supplied by the C runtime startup objects.
The question now is - is there a way to bypass the C startup files so that the startup code can be avoided? Technically yes, but this is perilous territory that could yield undefined behaviour. If you are adventurous you can actually tell GCC to change the entry point of your program with the -e
command line option. Rather than _start
we could make our entry point main
bypassing the C startup code. Since we are bypassing the C startup code we can also dispense with linking in the C runtime startup code with the -nostartfiles
option.
You could use this command line to compile your C program:
gcc test.c -e main -nostartfiles
Unfortunately, there is a bit of a gotchya that has to be fixed in the C code. Normally when using the C runtime startup objects, after the environment is initialized a CALL is made to main
. Normally main
does a RET instruction which returns back to the C runtime code. At that point the C runtime gracefully exits your program. RET doesn't have anywhere to return when the -nostartfiles
option is used, so it will likely segfault. To get around that we can call the C library _exit
function to exit our program.
#include <stdio.h>
int main()
{
printf("Hello, world!\n");
_exit(0); /* We exit application here, never reaching the return */
return 0;
}
Unless you omit frame pointers there are a few extra instructions emitted by GCC to setup the stack frame and tear it down, but the overhead is minimal.
Special Note
The process above doesn't seem to work for static builds (-static
option in GCC) with standard glibc C library. This is discussed in this Stackoverflow answer. The dynamic version works because a shared object can register a function that gets called by the dynamic loader to perform initialization. When building statically this is generally done by the C runtime, but we've skipped that initialization. Because of that GLIBC functions like printf
can fail. There are replacement C libraries that are standards compliant that can operate without C runtime initialization. One such product is MUSL.
Installing MUSL as an alternative to GLIBC
On Ubuntu 64-bit these commands should build and install the 64-bit version of MUSL:
git clone git://git.musl-libc.org/musl
cd musl
./configure --prefix=/usr/local/musl/x86-64
make
sudo make install
You can then use the MUSL wrapper for GCC to work with the MUSL's C library instead of the default GLIBC library on most Linux distributions. Parameters are just like GCC so you should be able to do:
/usr/local/musl/x86-64/bin/musl-gcc -e main -static -nostartfiles test.c
When running ./a.out
generated with GLIBC it would likely segfault. MUSL doesn't need initialization prior to using most of the C library functions, so it should work even with the -static
GCC option.
A fairer comparison
One of the issues with your comparison is that you call the SYS_WRITE system call directly in NASM, in C you are using printf
. User EOF correctly commented that you might want to make it a fairer comparison by calling the write
function in C instead of printf
. write
has far less overhead to it. You could amend your code to be:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
char *str = "Hello, world\n";
write (STDOUT_FILENO, str, 13);
_exit(0);
return 0;
}
This will have more overhead than NASM's direct SYS_WRITE syscall, but far less than what printf
would generate.
I'm going to issue the caveat that such code and trickery would likely not be taken well in a code review except for some fringe cases of software development.
write()
instead ofprintf()
in your C-program. Also, you can actually see some of the code that runs beforemain()
if you disassemble the executable (e.g.objdump -d [executable]
). – EOF