Различия между GCC/Clang при линковке
Из-за значительно лучшего времени компиляции C++ я недавно добавил возможность компилировать проект для микроконтроллера ARM Cortex-M4 с помощью Clang вместо инструментальной цепочки arm-none-eabi-gcc. Весь процесс прошел довольно гладко, и у меня быстро появились рабочие файлы ELF и HEX. Только вчера вечером я заметил, что файлы ELF на самом деле довольно сильно отличаются...
Прежде чем я продолжу, давайте проверим ELF, созданный GCC, чтобы получить какую-то основу.
ELF GCC содержит следующие разделы (помимо отладки)
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .vector_table PROGBITS 08000000 010000 000010 00 A 0 0 4
[ 2] .version NOBITS 08000010 010010 000010 00 WA 0 0 1
[ 3] .text PROGBITS 08000020 010020 000138 00 AX 0 0 4
[ 4] .rodata PROGBITS 08000158 010158 000000 00 WA 0 0 1
[ 5] .data PROGBITS 20000000 010158 000000 00 WA 0 0 1
[ 6] .data2 PROGBITS 10000000 010158 000000 00 W 0 0 1
[ 7] .bss NOBITS 20000000 020000 00038c 00 WA 0 0 512
[ 8] .bss2 PROGBITS 2000038c 010158 000000 00 W 0 0 1
[ 9] ._user_heap_stack NOBITS 2000038c 020000 000e04 00 WA 0 0 1
Но несмотря на то, что.data и.bss отмечены флагом "A" (alloc), загружаются только следующие
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x010000 0x08000000 0x08000000 0x00158 0x00158 RWE 0x10000
LOAD 0x000000 0x20000000 0x20000000 0x00000 0x01190 RW 0x10000
Section to Segment mapping:
Segment Sections...
00 .vector_table .version .text
01 .bss ._user_heap_stack
Все идет нормально.
Проблема возникла, когда я попытался создать двоичные файлы из ELF, созданного Clang. Эти файлы были огромного размера (256 МБ), что далеко от того, что я ожидал. Теперь, если вы не знакомы с микроконтроллерами ARM, они обычно содержат флэш-память и оперативную память с очень разными адресами (например, 0x0800'0000 для FLASH и 0x2000'0000 для RAM, как показано выше). Итак, у меня уже были некоторые подозрения относительно того, что происходит... Я проверил свой сценарий компоновщика и поместил директиву NOLOAD в каждый раздел, который идет исключительно в RAM. Задача решена?
Ну не совсем. Фактически мои двоичные файлы стали еще больше.
Давайте посмотрим на ELF Кланга. Меня немного беспокоит то, что Clang, похоже, не удаляет раздел для разматывания (ARM.exidx), хотя я компилирую с -fno-unwind-tables и -gc-section, но хорошо, я могу жить с этими 16B.
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .vector_table PROGBITS 08000000 001000 000010 00 A 0 0 4
[ 2] .version PROGBITS 08000010 001010 000010 00 A 0 0 1
[ 3] .text PROGBITS 08000020 001020 000334 00 AX 0 0 4
[ 4] .rodata PROGBITS 08000354 001354 000000 00 AX 0 0 1
[ 5] .ARM.exidx ARM_EXIDX 08000354 001354 000010 00 AL 3 0 4
[ 6] .preinit_array PROGBITS 08000364 001364 000000 00 A 0 0 1
[ 7] .init_array INIT_ARRAY 08000364 001364 000004 04 WA 0 0 4
[ 8] .fini_array FINI_ARRAY 08000368 001368 000004 04 WA 0 0 4
[ 9] .data PROGBITS 20000000 002000 000000 00 WA 0 0 1
[10] .data2 PROGBITS 10000000 002000 000000 00 WA 0 0 1
[11] .bss NOBITS 20000000 002000 0001ac 00 WA 0 0 512
[12] .bss2 PROGBITS 200001ac 002000 000000 00 WA 0 0 1
[13] ._user_heap_stack PROGBITS 200001ac 002000 000e04 00 WA 0 0 1
Вот где становится интересно, и я понятия не имею, что происходит. Что такое GNU_RELRO и GNU_STACK и как они там попадают? Почему GNU_STACK по адресу 0. Есть ли вероятность, что эта запись раздувает мои двоичные файлы?
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x001000 0x08000000 0x08000000 0x00364 0x00364 R E 0x1000
LOAD 0x001364 0x08000364 0x08000364 0x00008 0x00008 RW 0x1000
GNU_RELRO 0x001364 0x08000364 0x08000364 0x00008 0x00c9c R 0x1
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0
EXIDX 0x001354 0x08000354 0x08000354 0x00010 0x00010 R 0x4
Section to Segment mapping:
Segment Sections...
00 .vector_table .version .text .rodata .ARM.exidx
01 .preinit_array .init_array .fini_array
02 .preinit_array .init_array .fini_array
03
04 .rodata .ARM.exidx
Дальнейшие вопросы:
- Как GCC может удалить все разделы RAM, несмотря на то, что у них ранее не было директивы NOLOAD в сценарии компоновщика?
- Рабочий размер в ELF Clang считает минимальный размер стека, который я определяю в сценарии компоновщика для раздела.data, тогда как в ELF GCC это не так. Как так? Мой скрипт компоновщика содержит раздел, который выглядит так
._user_heap_stack (NOLOAD) :
{
. = ALIGN(8);
PROVIDE ( end = . );
PROVIDE ( _end = . );
. = . + _Min_Heap_Size;
. = . + _Min_Stack_Size;
. = ALIGN(8);
} >RAM
- но, насколько мне известно, это должно только "проверить", что оперативной памяти достаточно, чтобы покрыть мой определенный минимальный размер кучи и стека. Этот раздел на самом деле ничего не содержит, так как его можно засчитать в.data?
Я знаю, что могу удалить ненужные разделы с помощью objcopy при создании двоичных файлов, но мне действительно хотелось бы понять эти тонкие различия между GCC и Clang.
/edit Я только что заметил, что мой раздел._user_heap_stack имеет разные типы в зависимости от компилятора (NOBITS vs PROGBITS). Думаю, это объясняет, почему он засчитывается в.data...
/edit Теперь (потенциальная) ошибка в @ LLVMhttps://bugs.llvm.org/show_bug.cgi?id=46299
/edit И закрыт с lld 10.0.1https://bugs.llvm.org/show_bug.cgi?id=46225