由于 C++ 编译时间大大缩短,我最近添加了使用 Clang 而不是 arm-none-eabi-gcc 工具链为 ARM Cortex-M4 微控制器编译项目的选项。整个过程运行得非常顺利,我很快就有了 ELF 和 HEX 文件。直到昨天晚上我才注意到 ELF 文件实际上差异很大......
在我继续之前,让我们检查一下 GCC 生成的 ELF 以获得某种基线。
GCC 的 ELF 包含以下部分(除了调试内容)
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
到目前为止,一切都很好。
当我尝试从 Clang 生成的 ELF 创建二进制文件时,问题出现了。这些文件的大小(256MB)与我的预期相去甚远。现在,如果您不熟悉 ARM 微控制器,它们通常包含位于不同地址位置的 FLASH 和 RAM 存储器(例如,如上所示,FLASH 为 0x0800'0000,RAM 为 0x2000'0000)。所以我已经对发生了什么有些怀疑......我检查了我的链接器脚本并在每个单独进入 RAM 的部分上放置了一个 NOLOAD 指令。问题解决了?
嗯...不是真的。事实上,我的二进制文件变得更大了。
我们来看看 Clang 的 ELF。尽管我使用 -fno-unwind-tables 和 -gc-sections 进行编译,但 Clang 似乎并没有删除展开部分(ARM.exidx),这让我有点烦恼,但好吧,我可以忍受那些 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
进一步的问题:
- 尽管以前在链接描述文件中没有 NOLOAD 指令,但 GCC 如何能够删除所有 RAM 部分?
- 在 Clang 的 ELF 上运行大小将我在链接描述文件中定义的最小堆栈大小计入 .data 部分,而在 GCC 的 ELF 上则没有。那个怎么样?我的链接器脚本包含一个看起来像这样的部分
._user_heap_stack (NOLOAD) :
{
. = ALIGN(8);
PROVIDE ( end = . );
PROVIDE ( _end = . );
. = . + _Min_Heap_Size;
. = . + _Min_Stack_Size;
. = ALIGN(8);
} >RAM
- 但据我所知,这应该只“检查”至少有足够的 RAM 来覆盖我定义的最小堆和堆栈大小。该部分实际上不包含任何内容,那么它如何计入.data?
我知道在实际创建二进制文件时可以使用 objcopy 删除不需要的部分,但我真的很想了解 GCC 和 Clang 之间的那些细微差别。
/edit 我刚刚注意到我的 ._user_heap_stack 部分具有不同的类型,具体取决于编译器(NOBITS 与 PROGBITS)。猜猜这解释了为什么它被计入.data ...
/edit 现在是@ LLVM 上的(潜在)错误 https://bugs.llvm.org/show_bug.cgi?id=46299
/edit 并从 lld 10.0.1 关闭 https://bugs.llvm.org/show_bug.cgi?id=46225