Search code examples
assemblygccldgnu-assembler

gcc: passing -nostartfiles to ld via gcc for minimal binary size


consider the following gas (GNU assembler) file for 64 bit linux

go.s

        .global _start
        .text

_start:
        mov     $60, %rax               # system call 60 is exit
        xor     %rdi, %rdi              # we want return code 0
        syscall  

Now to compile:

rm -f go go.o
as -o go.o go.s
ld -o go -s -nostartfiles go.o
ls -l go                                            # -> 344 bytes

We get a super-small size of 344 bytes.

Great! With gcc and separate ld command:

# gcc with separate link
rm -f go go.o
gcc -xassembler -c go.s
ld -s -nostartfiles -o go go.o
ls -l go                                            # -> 344 bytes

But how does one get that small size, using only a single gcc command?

# single gcc command                                                                                                                                                                     
rm -f go go.o
gcc -xassembler -s -static -nostartfiles -o go go.s
ls -l go                                            # -> 4400 bytes  too big!!!

Damn 4400 bytes! Which single line gcc invocation will give 344 bytes?

Running the above single line with the -v flag...

gcc -v -xassembler -s -static -nostartfiles -o go go.s

... shows that -nostartfiles is not passed to the collect2 link command.

Hmmm...

So task: show me the single-line gcc invocation giving the minimal size!

Thanks.


Experimenting with the -v flag:

gcc -v -xassembler -s -static -nostartfiles -o go go.s

does the following:

as -v --64 -o go.o go.s

# will give 4400 bytes
/usr/lib/gcc/x86_64-linux-gnu/8/collect2 -plugin /usr/lib/gcc/x86_64-linux-gnu/8/liblto_plugin.so -plugin-opt=/usr/lib/gcc/x86_64-linux-gnu/8/lto-wrapper -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_eh -plugin-opt=-pass-through=-lc --build-id -m elf_x86_64 --hash-style=gnu -static -o go -s -L/usr/lib/gcc/x86_64-linux-gnu/8 -L/usr/lib/gcc/x86_64-linux-gnu/8/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/8/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/8/../../.. go.o --start-group -lgcc -lgcc_eh -lc --end-group

Manually adding -nostartfiles (directly after collect2) in the /usr/lib/gcc/x86_64-linux-gnu/8/collect2 command (above) gives a size of 520 bytes.

Manually adding -nostartfiles (directly after collect2) and removing --build-id gives a size of 344 bytes. As we can see here, -Wl,--build-id=none should remove the build-id.

But how does one instruct gcc to pass -nostartfiles to ld or collect2?


In response to Jester's comment:

Doing the following (-Wl,-nostartfiles):

gcc -Wl,-nostartfiles,--build-id=none -v -xassembler -s -static -nostartfiles -o go go.s

will add -nostartfiles too far at the end of the collect2 command, with the result that the output binary go is not even created. How does one give the command, so that -nostartfiles occurs earlier in the collect2 ... command: I know it works, if I manually construct -nostartfiles to be a flag right at the beginning.

Looking here, there is a claim that -Wl,-nostartfiles will not be handled correctly. But if I just use gcc -nostartflags ..., then it is not passed to the linker: Maby this is a bug in gcc??

Maby all code gets optimized away... leaving nothing at all?? See here


Solution

  • -nostartfiles is not an ld option. It parses as ld -n -o startfiles.

    I tried your commands, and they don't create a file called go, they create an executable called startfiles.

    $ cat > go.s
      paste + control-d
    $ as -o go.o go.s
    $ ld -o go -s -nostartfiles go.o
    $ ll go
    ls: cannot access 'go': No such file or directory
    $ ll -clrt
    -rw-r--r-- 1 peter peter  193 May 13 11:33 go.s
    -rw-r--r-- 1 peter peter  704 May 13 11:33 go.o
    -rwxr-xr-x 1 peter peter  344 May 13 11:33 startfiles
    

    Your go must have been left over from your ld -s -nostartfiles -o go go.o where -o go was the last instance of -o on the command line, not overridden by -o startfiles.


    The option that makes your binary small is ld -n:

    -n
    --nmagic
    Turn off page alignment of sections, and disable linking against shared libraries. If the output format supports Unix style magic numbers, mark the output as "NMAGIC".

    Related: Minimal executable size now 10x larger after linking than 2 years ago, for tiny programs?


    As a GCC option, -nostartfiles tells the gcc front-end not to link crt*.o files. If you're running ld manually, you just omit mentioning them. There's no need to tell ld what you're not linking, the ld command itself doesn't link anything it's not explicitly told to. Linking CRT files and libc / libgcc are gcc defaults, not ld.

    $ gcc -s -nostdlib -static  -Wl,--nmagic,--build-id=none go.s
    $ ll a.out 
    -rwxr-xr-x 1 peter peter 344 May 13 12:35 a.out
    

    You want -nostdlib to omit libraries as well as CRT start files.
    -nostartfiles is only a subset of what -nostdlib does.
    (Although when statically linking, ld doesn't pull in any code from libc.a or libgcc.a because your file doesn't reference any external symbols. So you actually still get the same 344 byte file from using -nostartfiles as -nostdlib. But -nostdlib replicates your manual ld command more exactly, not passing any extra files.)

    You need -static to not dynamically link, on a GCC where -pie is the default. (This also implies -no-pie; -static-pie won't be enabled by default.)
    --nmagic fails with error: PHDR segment not covered by LOAD segment if you let GCC try to dynamically link a PIE executable (even with no shared libraries).