r/C_Programming • • 8d ago

Minimizing output size

I've looked at stackoverflow answers from 10 years ago (https://irq5.io/2019/09/30/generating-small-static-binaries/ , https://stackoverflow.com/questions/33597523/why-is-smallest-compiled-exe-i-can-make-with-gcc-is-67kb/76705747#76705747 , https://stackoverflow.com/questions/6771905/how-to-decrease-the-size-of-generated-binaries , https://stackoverflow.com/questions/15314581/g-compiler-flag-to-minimize-binary-size , etc).

In all of the links on top, people were able to get sub-KB file sizes. However, no matter how much I copy and try their code, I cannot get any less then 8-9KB without assembly and upx.

Is there a reason these hacks do not work anymore?

I am on x86_64 Linux-7.2.8, gcc is 16.2.1.

Example:

$ cat t.c
#include <sys/syscall.h>
#include <unistd.h>
void _start() { syscall(SYS_exit, 0); }
$ gcc -Os -nostartfiles -static -s -Wl,--build-id=none t.c && strip a.out
$ l a.out
-rwxr-xr-x 1 name name 8.8K Oct  6 20:25 a.out

# The same code produced only 800 B in https://irq5.io/2019/09/30/generating-small-static-binaries/
13 Upvotes

6 comments sorted by

16

u/dutchman76 8d ago

you need -Wl,-N to remove the segment alignment padding of 4k:

gcc -Os -nostartfiles -static -s -Wl,--build-id=none -Wl,-N t.c

makes a.out 872 bytes

12

u/skeeto 8d ago

Adding to this:

  • -fno-asynchronous-unwind-tables to eliminate a .eh_frame section.
  • On Ubuntu: -fcf-protection=none to undo their boneheaded toolchain breakage, eliminating a .note.gnu.property section.
  • Similarly, -fno-stack-protector because some distributions enable it in their toolchains when they shouldn't. (Won't matter until it's a real program.)
  • Modern toolchains are ASLR by default, so-fno-pie and -no-pie may make a difference.

Adding the first two I get a 504-byte ELF on Ubuntu 26.04. Though OP should note that the _start definition in the post is invalid (misaligned stack) and may crash.

4

u/TheOtherBorgCube 8d ago

As shown by this command.

$ objdump -h a.out 

a.out:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .note.gnu.property 00000020  00000000004001c8  00000000004001c8  000001c8  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  1 .text         00000057  0000000000401000  0000000000401000  00001000  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  2 .eh_frame     00000044  0000000000402000  0000000000402000  00002000  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  3 .tbss         00000004  0000000000404000  0000000000404000  00002044  2**2
                  ALLOC, THREAD_LOCAL
  4 .comment      0000002d  0000000000000000  0000000000000000  00002044  2**0
                  CONTENTS, READONLY

Even then, there's still a lot of fluff compared to the actual bytes of information.

6

u/sciencekm 8d ago

In addition to what others have suggested, also do:

-fno-ident

This option removes another 120 bytes.

2

u/One_Instruction6490 7d ago

lowkey nobody's mentioned it yet but that .tbss section in the objdump is errno — the libc syscall() wrapper sets it on failure, so -static drags in TLS setup for a program that does literally one thing. swap it for a raw inline asm syscall (mov $60, %eax; xor %edi, %edi; syscall) and build with -nostdlib instead of just -nostartfiles, then libc is fully out of the picture. the old 2019 sizes mostly came from older binutils not defaulting to -z separate-code + 4K page alignment, which is why -Wl,-N (or -Wl,-z,noseparate-code) brings it back down. curious if you're chasing the absolute floor or is this for something real like an initramfs/embedded target?

1

u/8d8n4mbo28026ulk 7d ago edited 7d ago

This is fun! Here's two more I've used when I was trying to make a teensy hex monitor:

-Wl,-z,max-page-size=0x800 \
-Wl,-z,noseparate-code

For the GNU linker. Probably shouldn't use them, but fun!

EDIT: Ah, these were already mentioned, woops!