Что находится в исполняемом файле помимо необработанных машинных инструкций?

#c #linux #code-size

#c #сборка #gcc #исполняемый файл

Вопрос:

Я пытаюсь понять низкоуровневые вычисления. Я заметил, что мои скомпилированные двоичные файлы намного больше, чем я думаю, что они должны быть. Итак, я попытался создать минимально возможную программу на c без какого-либо кода stdlib следующим образом:

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

gcc -nostdlib -o minimal minimal.c

Когда я разбираю двоичный файл, он показывает мне именно то, что я ожидаю, а именно этот точный код в трех строках сборки.

 $ 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>
  

Но мой фактический исполняемый файл по-прежнему имеет размер 13856 байт. Что делает это таким большим? Что еще находится в этом файле? Требуется ли ОС больше, чем эти 6 байт машинного кода?

Редактировать # 1: вывод size :

 $ 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
  

Комментарии:

1. Исполняемый файл содержит гораздо больше информации, чем просто сам код. Он может меняться между различными операционными системами и типами исполняемых файлов.

2. Если вы связываете его как elf, это не низкоуровневый.

3. Есть некоторые метаданные об исполняемом файле, предусмотренные стандартом ELF . Компилятор может добавить другие метаданные или разделы, попробуйте strip также файл.

4. Также см., Например, Этот учебник о том, как создавать минимальные исполняемые файлы.

5. Если вы скомпилируете 16-разрядную программу MSDOS .COM без указателей на фреймы, в итоге вы получите только код. Как уже отмечалось выше, для большинства современных операционных систем в дополнение к скомпилированному коду имеется информация.

Ответ №1:

Современные компиляторы и компоновщики на самом деле не оптимизированы для создания сверхмалого кода на полномасштабных платформах. Не потому, что работа сложная, а потому, что в этом обычно нет необходимости. Не обязательно, чтобы компилятор или компоновщик добавляли дополнительный код (хотя это может быть), скорее, он не будет стараться упаковать ваши данные и код в минимально возможное пространство.

В вашем случае я отмечаю, что вы используете динамическое связывание, хотя на самом деле ничего не связано. Использование «-static» сократит около 8 КБ. «-s» (strip) избавит от немного большего.

Я не знаю, возможно ли вообще с помощью gcc создать действительно минимальный исполняемый файл ELF. В вашем случае это должно быть около 400 байт, почти все из которых будут различными заголовками ELF, таблицей разделов и т. Д.

Я не знаю, разрешено ли мне ссылаться на свой собственный веб-сайт (я уверен, что кто-нибудь меня поправит, если нет), но у меня есть статья о создании крошечного исполняемого файла ELF путем создания его с нуля в двоичном формате:

http://kevinboone.me/elfdemo.html

Комментарии:

1. Упс — я забыл сказать: часть информации, написанной gcc / ld, будет полезна для таких инструментов, как отладчики, и на самом деле не нужна для нормального выполнения. Это все равно увеличит размер файла.

2. Для 32-разрядных программ Windows Visual C / C 4.0 будет генерировать файлы .EXE меньшего размера, чем текущие версии Visual Studio, но я не исследовал почему.

3. @rcgldr размер EXE-файла можно легко изменить, изменив заголовок DOS, поскольку этот заголовок содержит реальную программу DOS для отображения некоторого сообщения при запуске в DOS

4. @phuclv — мой последний комментарий был о 32-разрядных программах Windows, а не о программах DOS. Visual C / C 4.0 создает EXE-файлы меньшего размера, чем Visual Studio.

5. @rcgldr Я говорю о 32-разрядных EXE-файлах. Они всегда содержат заглушку DOS, чтобы предотвратить их выполнение в DOS, или в некоторых случаях для создания «толстых» двоичных файлов, которые могут выполняться в любой среде

Ответ №2:

Существует много разных форматов исполняемых файлов. .com, .exe, .elf, .coff, a.out и т.д. В идеале они содержат машинный код и другие разделы (.text (code), .data, .bss, .rodata и, возможно, другие, имена зависят от набора инструментов), а также содержат отладочную информацию. Обратите внимание, как ваша разборка показала метку _start ? это строка среди прочих и другая информация, позволяющая связать эту строку с адресом для отладки. Вывод objdump также показал, что вы используете файл elf, вы можете легко найти формат файла и тривиально написать свою собственную программу для анализа файла или попытаться использовать readelf и другие инструменты, чтобы увидеть, что там (высокий уровень, а не raw).

В операционной системе, где в целом (не всегда, но, думаю, на ПК) программы загружаются в ОЗУ и затем запускаются, поэтому вы хотите в первую очередь иметь формат файла, поддерживаемый операционной системой, у них нет причин поддерживать более одного, но они могут. Это зависит от ОС / системного дизайна, но ОС может быть спроектирована так, чтобы не только загружать код, но и загружать / инициализировать данные (.data, .bss). Скажем, при загрузке mcu вам нужно встроить данные в двоичный двоичный двоичный объект, а само приложение копирует данные в ОЗУ с флэш-памяти, но в ОС это необязательно, но для этого вам нужен формат файла, который может различать разделы, целевые местоположения и размеры. Что означает дополнительные байты в файле для определения этого и формата файла.

Двоичный файл включает в себя код начальной загрузки, прежде чем он сможет ввести сгенерированный на C код, в зависимости от системы, в зависимости от библиотеки C (на компьютере можно использовать несколько / много библиотек C, а bootstrap специфичен для библиотеки C, в общем, не для цели, не для операционной системы, а не для компилятора)таким образом, некоторый процент файла — это загрузочный код, также, когда ваша основная программа очень маленькая, большая часть размера файла является накладной.

Вы можете, например, использовать strip, чтобы уменьшить размер файла, избавившись от некоторых символов и других второстепенных элементов, например, размер файла должен уменьшиться, но тогда при разборке objdump не будет меток, а для случая x86 набор инструкций переменной длины, который в лучшем случае трудно разобрать, будет уменьшен.намного сложнее, поэтому выходные данные с метками или без них могут не отражать фактические инструкции, но без меток дизассемблер gnu не сбрасывает себя на метки и может ухудшить результат.

Комментарии:

1. В этом конкретном примере нет «кода начальной загрузки», потому -nostdlib что используется. С другой стороны, это означает, что _start это не было вызвано с использованием стандартных соглашений о вызовах C, что может привести к проблемам, если программа будет более сложной.

Ответ №3:

Если вы используете clang 10.0 и lld 10.0 и удаляете ненужные разделы, вы можете получить размер 64-разрядного статически связанного исполняемого файла размером менее 800 байт.

 $ 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