2

I'm investigating the size of an extremely small C program on Linux (ubuntu 20.04).

I'm compiling as follows:

gcc -s -nostdlib test.c -o test

the following progam:

__attribute__((naked))
void _start() {
    asm("movl $1,%eax;"
    "xorl %ebx,%ebx;"
    "int  $0x80");
}

Basically the idea is to make the Linux system call to exit rather than depending on the C runtime to do that for us. (which would be the case in void main() { }). The program moves 1 into register EAX, clears register EBX (which would otherwise contain the return value), and then executes the linux system call interrupt 0x80. This interrupt triggers the kernel to process our call.

I would expect this program to be extremely small (less than 1K), however ..

du -h test
# >> 16K
ldd test
# >> statically linked

Why is this program still 16K?

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
Gio
  • 3,242
  • 1
  • 25
  • 53
  • 6
    I have just read a very detailed post precisely about this topic. http://www.muppetlabs.com/~breadbox/software/tiny/teensy.html – Ivan C Dec 09 '20 at 01:49
  • 1
    16K could just be the disk's block size. Try `ls -l test`. – user3386109 Dec 09 '20 at 01:51
  • Try the [size](https://linux.die.net/man/1/size) command to get text/data/bss sizes – Rachid K. Dec 09 '20 at 10:38
  • `_start` isn't a function, so don't waste space letting the compiler put `push %rbp` and so on around your asm statement. Also please don't suggest putting Basic `asm()` statements inside functions that aren't `__attribute__((naked))`. It's a bad example leading to problems like [Linux getting terminal arguments from \_start not working with inline assembly in C](https://stackoverflow.com/q/65169694), easily solved by putting `_start:` *inside* the asm statement at global scope like [How Get arguments value using inline assembly in C without Glibc?](https://stackoverflow.com/q/50260855) – Peter Cordes Dec 09 '20 at 14:01
  • @Peter, Regarding _start, see https://stackoverflow.com/questions/29694564/what-is-the-use-of-start-in-c, furthermore I think this comment is unrelated to the question, its about understanding how something works, not about how to write production code. – Gio Dec 09 '20 at 23:23
  • Yes, of course it's a learning exercise; production code wouldn't write it's own `_start` at all. I'm trying to help you learn that `_start` isn't a real function; on entry there's no return address on the stack. The `_start()` in the answer you linked is broken; it's missing `__attribute__((force_align_arg_pointer))` or equivalent, so the call to `main` will be done with RSP misaligned, e.g. leading to a segfault if you use scanf in a function that didn't already over-align the stack for something like `alignas(32) volatile int buf[8] = {0};` – Peter Cordes Dec 10 '20 at 00:26
  • And since you were trying to minimize executable size, you don't want the compiler putting extra instructions around your asm statement. And also, GNU C inline asm is hard enough to learn without bad examples on Stack Overflow. This is not very bad; you're not calling other functions or interacting with C variables at all. But stamping out misuse of inline asm is kind of a quixotic crusade of mine. https://godbolt.org/z/6e665c shows the compiler-output asm for your `_start` with/without `__attribute__((naked))`. I made an edit to your question to improve the `_start` definition. – Peter Cordes Dec 10 '20 at 00:29

3 Answers3

3

du reports the disk space used by a file whereas ls reports the actual size of a file. Typically the size reported by du is significantly larger for small files.

You can significantly reduce the size of the binary by changing compile and linking options and stripping out unnecessary sections.

$ cat test.c
void _start() {
    asm("movl $1,%eax;"
    "xorl %ebx,%ebx;"
    "int  $0x80");
}

$ gcc -s -nostdlib test.c -o test
$ ./test
$ ls -l test
-rwxrwxr-x 1 fpm fpm 8840 Dec  9 04:09 test

$ readelf -W --section-headers test
There are 7 section headers, starting at offset 0x20c8:

Section Headers:
  [Nr] Name              Type            Address          Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            0000000000000000 000000 000000 00      0   0  0
  [ 1] .note.gnu.build-id NOTE            0000000000400190 000190 000024 00   A  0   0  4
  [ 2] .text             PROGBITS        0000000000401000 001000 000010 00  AX  0   0  1
  [ 3] .eh_frame_hdr     PROGBITS        0000000000402000 002000 000014 00   A  0   0  4
  [ 4] .eh_frame         PROGBITS        0000000000402018 002018 000038 00   A  0   0  8
  [ 5] .comment          PROGBITS        0000000000000000 002050 00002e 01  MS  0   0  1
  [ 6] .shstrtab         STRTAB          0000000000000000 00207e 000045 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  l (large), p (processor specific)
$

$ gcc -s -nostdlib -Wl,--nmagic test.c -o test
$ ls -l test
-rwxrwxr-x 1 fpm fpm 984 Dec  9 16:55 test
$ strip -R .comment -R .note.gnu.build-id test
$ strip -R .eh_frame_hdr -R .eh_frame test
$ ls -l test
-rwxrwxr-x 1 fpm fpm 520 Dec  9 17:03 test
$ 

Note that clang can produce a significantly smaller binary than gcc by default in this particular instance. However, after compiling with clang and stripping unnecessary sections, the final size of the binary is 736 bytes, which is bigger than the 520 bytes possible with gcc -s -nostdlib -Wl,--nmagic test.c -o test.

$ clang -static -nostdlib -flto -fuse-ld=lld -o test test.c
$ ls -l test
-rwxrwxr-x 1 fpm fpm 1344 Dec  9 04:15 test
$

$ readelf -W --section-headers test
There are 9 section headers, starting at offset 0x300:

Section Headers:
  [Nr] Name              Type            Address          Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            0000000000000000 000000 000000 00      0   0  0
  [ 1] .note.gnu.build-id NOTE            0000000000200190 000190 000018 00   A  0   0  4
  [ 2] .eh_frame_hdr     PROGBITS        00000000002001a8 0001a8 000014 00   A  0   0  4
  [ 3] .eh_frame         PROGBITS        00000000002001c0 0001c0 00003c 00   A  0   0  8
  [ 4] .text             PROGBITS        0000000000201200 000200 00000f 00  AX  0   0 16
  [ 5] .comment          PROGBITS        0000000000000000 00020f 000040 01  MS  0   0  1
  [ 6] .symtab           SYMTAB          0000000000000000 000250 000048 18      8   2  8
  [ 7] .shstrtab         STRTAB          0000000000000000 000298 000055 00      0   0  1
  [ 8] .strtab           STRTAB          0000000000000000 0002ed 000012 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  l (large), p (processor specific)
$ 
  
$ strip -R .eh_frame_hdr -R .eh_frame test
$ strip -R .comment -R .note.gnu.build-id test
strip: test: warning: empty loadable segment detected at vaddr=0x200000, is this intentional?
$ ls -l test
-rwxrwxr-x 1 fpm fpm 736 Dec  9 04:19 test
$ readelf -W --section-headers test
There are 3 section headers, starting at offset 0x220:

Section Headers:
  [Nr] Name              Type            Address          Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            0000000000000000 000000 000000 00      0   0  0
  [ 1] .text             PROGBITS        0000000000201200 000200 00000f 00  AX  0   0 16
  [ 2] .shstrtab         STRTAB          0000000000000000 00020f 000011 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  l (large), p (processor specific)
$ 

.text is your code, .shstrtab is the Section Header String table. Each ElfHeader structure contains an e_shstrndx member which is an index into the .shstrtab table. If you use this index, you can find the name of that section.

fpmurphy
  • 2,464
  • 1
  • 18
  • 22
  • Anyway, the main thing you're getting from clang is using a different linker, other than GNU binutils `ld` which by default aligns sections for increased security these days (data that doesn't need to be in an executable page won't be mapped into one). [Minimal executable size now 10x larger after linking than 2 years ago, for tiny programs?](https://stackoverflow.com/q/65037919) – Peter Cordes Dec 09 '20 at 14:05
2

du, by default, reports the space used on disk by the file--this means that the smallest value will be one disk block. If you want to know the actual size of the file, use ls -l.

Kurt Weber
  • 176
  • 10
0

The modified program to get an exit code equal to 3 (for the fun):

void _start() {
    asm("movl $1,%eax;"
    "movl $3,%ebx;"
    "int  $0x80");
}

The build with:

-s
Remove all symbol table and relocation information from the executable.
-nostdlib
Do not use the standard system startup files or libraries when linking.

$ gcc -s -nostdlib pg.c -o pg
$ ./pg
$ echo $?
3
$ ldd ./pg
    statically linked

The size of the resulting executable file is 13 KB:

$ ls -l ./pg
-rwxrwxr-x 1 xxx xxx 13296 dec.   9 11:42 ./pg

The code disassembly shows that the text section is actually 23 bytes long and there is no data:

$ objdump -S ./pg
./pg:     file format elf64-x86-64

Disassembly of section .text:

0000000000001000 <.text>:
    1000:   f3 0f 1e fa             endbr64 
    1004:   55                      push   %rbp
    1005:   48 89 e5                mov    %rsp,%rbp
    1008:   b8 01 00 00 00          mov    $0x1,%eax
    100d:   bb 03 00 00 00          mov    $0x3,%ebx
    1012:   cd 80                   int    $0x80
    1014:   90                      nop
    1015:   5d                      pop    %rbp
    1016:   c3                      retq

But the size utility shows that the data section is 224 (size of the .dynamic section) and the size reported for the text is 248 bytes. This is the total size of the other sections minus the .comment:

$ size pg
   text    data     bss     dec     hex filename
    248     224       0     472     1d8 pg
$ size pg --format=SysV
pg  :
section              size    addr
.interp                28     792
.note.gnu.property     32     824
.note.gnu.build-id     36     856
.gnu.hash              28     896
.dynsym                24     928
.dynstr                 1     952
.text                  23    4096
.eh_frame_hdr          20    8192
.eh_frame              56    8216
.dynamic              224   16160
.comment               42       0
Total                 514

If we rebuild the program adding:

-static
On systems that support dynamic linking, this overrides -pie and prevents linking with the shared libraries. On other systems, this option has no effect.

The size of the file decreases (8816 bytes instead of 13 KB):

$ gcc -s -static -nostdlib pg.c -o pg
$ ls -l ./pg
-rwxrwxr-x 1 xxxx xxxx 8816 dec.   9 13:03 ./pg

The "-static" option made disappear several dynamic linking related sections:

$ size pg
   text    data     bss     dec     hex filename
    147       0       0     147      93 pg
$ size pg --format=SysV
pg  :
section              size      addr
.note.gnu.property     32   4194760
.note.gnu.build-id     36   4194792
.text                  23   4198400
.eh_frame              56   4202496
.comment               42         0
Total                 189
Rachid K.
  • 4,490
  • 3
  • 11
  • 30