What is in an executable besides the raw machine instructions?

Viewed 342

I am on a quest to understand low-level computing. I have noticed my compiled binaries are a lot bigger then I think they should be. So I tried to build the smallest possible c program without any stdlib code as follows:

void _start()
{
    while(1) {};
}

gcc -nostdlib -o minimal minimal.c

When I disasseble the binary, it shows me exactly what I expect, namely this exact code in three lines of assembly.

$ objdump -d minimal

minimal:     file format elf64-x86-64


Disassembly of section .text:

0000000000001000 <_start>:
    1000:   55                      push   %rbp
    1001:   48 89 e5                mov    %rsp,%rbp
    1004:   eb fe                   jmp    1004 <_start+0x4>

But my actual executable is still 13856 Bytes in size. What is it, that makes this so large? What else is in that file? Does the OS need more than these 6 Bytes of machine code?

Edit #1: The output of size is:

$ size -A minimal
minimal  :
section              size    addr
.interp                28     680
.note.gnu.build-id     36     708
.gnu.hash              28     744
.dynsym                24     776
.dynstr                 1     800
.text                   6    4096
.eh_frame_hdr          20    8192
.eh_frame              52    8216
.dynamic              208   16176
.comment               18       0
Total                 421
3 Answers

There are many different executable file formats. .com, .exe, .elf, .coff, a.out, etc. They ideally contain the machine code and other sections (.text (code), .data, .bss, .rodata and possibly others, names depend on toolchain) plus they contain debugging information. Notice how your disassembly showed the label _start? that is a string among others and other info to be able to connect that string to the address for debugging. The output of objdump also showed that you are using an elf file, you can easily look up the file format and can trivially write your own program to parse through the file, or try to use readelf and other tools to see what is in there (high level not raw).

On an operating system where in general (not always, but think pc) the programs are being loaded into ram and then run, so you want to have first and foremost a file format that is supported by the operating system, there is no reason for them to support more than one, but they might. It is os/system design dependent, but the os may be designed to not only load the code, but also load/initialize the data (.data, .bss). When booting say an mcu you need to embed the data into the binary blob and the application itself copies the data to ram from the flash, but in an os that isn't necessarily required, but in order to do it you need a file format that can distinguish the sections, target locations, and sizes. Which means extra bytes in the file to define this and a file format.

A binary includes the bootstrap code before it can enter the C generated code, depending on the system, depending on the C library (multiple/many C libraries can be used on a computer and bootstrap is specific to the C library in general not the target, nor operating system, not a compiler thing), so some percentage of the file is the bootstrap code, too when your main program is very tiny the a lot of the file size is overhead.

You can for example use strip to make the file smaller by getting rid of some symbols and other non-essential items like that the file size should get smaller but the objdump disassembly will then not have labels and for the case of x86, a variable length instruction set which is difficult at best to disassemble gets much harder, so the output with or without labels may not reflect the actual instructions, but without the labels the gnu disassembler doesn't reset itself at the labels and can make the output worse.

Modern compilers and linkers aren't really optimized for producing ultra-small code on full-scale platforms. Not because the job is difficult, but because there's usually no need to. It isn't necessarily that the compiler or linker adds additional code (although it might), but rather that it won't try hard to pack your data and code into the smallest possible space.

In your case, I note that you're using dynamic linking, even though nothing is actually linked. Using "-static" will shave off about 8kB. "-s" (strip) will get rid of a bit more.

I don't know if it's even possible with gcc to make a truly minimal ELF executable. In your case, that ought to be about 400 bytes, nearly all of which will be the various ELF headers, section table, etc.

I don't know if I'm allowed to link my own website (I'm sure somebody will put me right if not), but I have an article on producing a tiny ELF executable by building it from scratch in binary:

http://kevinboone.me/elfdemo.html

If you use clang 10.0 and lld 10.0 and strip out unnecessary sections you can get the size of a 64-bit statically linked executable to under 800 bytes.

$ cat minimal.c
void _start(void)
{
    int i = 0;

    while (i < 11) {
       i++;
    }

    asm( "int $0x80" :: "a"(1), "b"(i) );
}

$ clang -static -nostdlib -flto -fuse-ld=lld -o minimal minimal.c
$ ls -l minimal
-rwxrwxr-x 1 fpm fpm 1376 Sep  4 17:38 minimal

$ readelf --string-dump .comment minimal
String dump of section '.comment':
  [     0]  Linker: LLD 10.0.0
  [    13]  clang version 10.0.0 (Fedora 10.0.0-2.fc32)

$ readelf -W --section-headers minimal
There are 9 section headers, starting at offset 0x320:

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 00002a 00  AX  0   0 16
  [ 5] .comment          PROGBITS        0000000000000000 00022a 000040 01  MS  0   0  1
  [ 6] .symtab           SYMTAB          0000000000000000 000270 000048 18      8   2  8
  [ 7] .shstrtab         STRTAB          0000000000000000 0002b8 000055 00      0   0  1
  [ 8] .strtab           STRTAB          0000000000000000 00030d 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 minimal
$ strip -R .comment -R .note.gnu.build-id minimal
strip: minimal: warning: empty loadable segment detected at vaddr=0x200000, is this intentional?

$ readelf -W --section-headers minimal
There are 3 section headers, starting at offset 0x240:

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 00002a 00  AX  0   0 16
  [ 2] .shstrtab         STRTAB          0000000000000000 00022a 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)

$ ll minimal
-rwxrwxr-x 1 fpm fpm 768 Sep  4 17:45 minimal
Related