提问人:rlakoda 提问时间:11/13/2023 更新时间:11/23/2023 访问量:62
全局偏移表 STM32 微控制器的 .got 和 .got.plt 必须为零初始化
Global Offset Table .got and .got.plt must be zero-initialized for STM32 microcontroller
问:
我正在为没有 STM32 IDE 的 ARM Cortex-M4 微控制器编译程序。我使用带有 newlib libc 的 arm-none-eabi 工具链、我为我的特定微控制器改编的链接器脚本以及我从 ST 获取的一些启动代码。
经过无数小时的调试,我发现特定的内存区域必须进行零初始化。否则。。。将包含 CPU 在某个时间点访问的随机数据,从而导致硬故障。 显示这对应于 和 部分:0x200006E8
0x2000071c
readelf -S
.got
.got.plt
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .isr_vector PROGBITS 08000000 001000 00018c 00 A 0 0 1
[ 2] .text PROGBITS 0800018c 00118c 00c4c4 00 AX 0 0 4
[ 3] .rodata PROGBITS 0800c650 00d650 0000a4 00 A 0 0 4
[ 4] .ARM.extab PROGBITS 0800c6f4 00d6f4 000000 00 A 0 0 1
[ 5] .ARM ARM_EXIDX 0800c6f4 00d6f4 000008 00 AL 2 0 4
[ 6] .preinit_array PREINIT_ARRAY 0800c6fc 00e71c 000000 04 WA 0 0 1
[ 7] .init_array INIT_ARRAY 0800c6fc 00d6fc 000004 04 WA 0 0 4
[ 8] .fini_array FINI_ARRAY 0800c700 00d700 000004 04 WA 0 0 4
[ 9] .data PROGBITS 20000000 00e000 0006e8 00 WA 0 0 8
[10] .got PROGBITS 200006e8 00e6e8 000028 00 WA 0 0 4
[11] .got.plt PROGBITS 20000710 00e710 00000c 04 WA 0 0 4
[12] .sram2 PROGBITS 2000c000 00e71c 000000 00 W 0 0 1
[13] .bss NOBITS 2000071c 00e71c 000500 00 WA 0 0 4
启动代码仅对该部分进行零初始化,并且不提及 或 。链接器脚本也没有对 或 的引用。
这意味着 GOT 没有开始和结束标签(如 for),启动代码可以使用这些标签来初始化它,而且我真的不想对地址进行硬编码。.bss
got
plt
got
plt
.bss
我使用以下标志来编译 GCC:
-mcpu=cortex-m4 -mfpu=fpv4-sp-d16 -mfloat-abi=hard -fno-common -ffunction-sections -fdata-sections -Wl,--gc-sections -specs=nano.specs -ffreestanding -Wall -O0 -ggdb
并链接:
-T link.ld -lc -lgcc
据我了解,GOT仅用于动态链接。为什么在链接器脚本没有指定的情况下插入它?我试图删除带有 的 和 部分,但这对问题没有影响。.got
.got.plt
objcopy
我很困惑为什么 GOT 应该用零初始化才能工作,而不是使用某些地址。有没有可能在链接阶段,GOT 以某种方式入“到”.bss 中,即 GOT 部分真的应该是 .bss
的一部分?GOT 位于该部分的正前方(按预期初始化)。
.bss
理想情况下,我只想对该部分进行零初始化,而不对启动代码进行任何修改。GOT 要么根本不使用,要么静态填充。.bss
关于这里可能发生的事情的任何想法都受到高度赞赏。
答:
也许这是你已经知道的东西,我无法从这个问题中看出......
unsigned int x;
void fun ( void )
{
x=5;
}
MEMORY {
one : ORIGIN = 0x000, LENGTH = 256
two : ORIGIN = 0x100, LENGTH = 256
}
SECTIONS {
.text : { *(.text) } > one
.bss : { *(.bss) } > two
}
arm-none-eabi-gcc -O2 -c -mcpu=cortex-m4 so.c -o so.o
arm-none-eabi-ld -Tso.ld so.o -o so.elf
arm-none-eabi-objdump -D so.elf
Disassembly of section .text:
00000000 <fun>:
0: 4b01 ldr r3, [pc, #4] ; (8 <fun+0x8>)
2: 2205 movs r2, #5
4: 601a str r2, [r3, #0]
6: 4770 bx lr
8: 00000100 andeq r0, r0, r0, lsl #2
Disassembly of section .bss:
00000100 <x>:
100: 00000000 andeq r0, r0, r0
IMO 这是正常/典型。(意思是,优化和没有图片/馅饼的东西等)(稍后将进入 GC 部分)。你可以看到编译器在池中生成了一个值,供链接器填充 x 的地址。
请注意,我从不使用这些,因为我做了很多这种裸机嵌入式的东西,但是 man gcc 并寻找 -fPIC -fpic 和 PIE 以查看差异,这是一个有趣的读物。在本例中,对于此代码,这四种组合将产生相同的结果。
Disassembly of section .text:
00000000 <fun>:
0: 4b03 ldr r3, [pc, #12] ; (10 <fun+0x10>)
2: 4a04 ldr r2, [pc, #16] ; (14 <fun+0x14>)
4: 447b add r3, pc
6: 589b ldr r3, [r3, r2]
8: 2205 movs r2, #5
a: 601a str r2, [r3, #0]
c: 4770 bx lr
e: bf00 nop
10: 00000008 andeq r0, r0, r8
14: 00000000 andeq r0, r0, r0
这是一个双重间接,或者让我们说它增加了一定程度的间接。现在注意问题
4: 447b add r3, pc
池中的值不是 GOT 的地址,而是 GOT 的相对偏移量。编译器将为每个函数生成这个,这就是首先使用 got 的整个处理,您不希望在每个函数的池中使用固定地址来寻址数据。
现在,从理论上讲,位置独立性也将是大多数情况下,您可以尝试一下,使二进制文件与位置无关,是的,与所有数据访问一样,这会使代码更大。因此,只有在您绝对需要它时才使用 pic/pie,尤其是对于资源有限的 mcu。您保存的每个字节都是胜利。时钟周期的惩罚通常是可以衡量的。
如果您使用位置独立性,则可以使用它执行两项操作,或者同时执行两项操作:移动代码、移动数据或同时移动两者。如果你移动代码,那么就像它如何为此目标构建一样,你必须保持相对于代码的 got。如果 pic 用于共享库,那么这意味着基于 ram 的系统,将程序加载到 ram 中的操作系统。我们不在基于 ram 的系统上,除非您为 ram 构建并复制和跳转。如上所示(链接器不会更改/修复此问题),必须修复获取地址关系的代码(对于此编译器、版本、目标、示例等)(如果它发生在一个示例中,那么它可能会发生在您身上)。
但是,如果要移动数据,则必须更新get,这意味着它必须在ram中。所以 got 需要在 ram 中,但二进制文件假设在闪存中,所以如果你想从不同的位置(flash 或 ram)运行二进制文件,那么你必须将 got 移开相对距离。然后,如果您想移动数据(二进制移动与否),那么您必须转到 got 本身并将对每个条目的地址的相对更改添加到每个条目中。是的,在这两种情况下,您都需要知道得到的位置以及大小。
如果你什么都不动,那么 got 已经准备好了......从构建的角度来看。如果你在 MCU 中将其链接到 ram,那么除非你在引导程序中这样做,否则它不会被填充,就像你对 .data 或 .bss 所做的那样,这意味着像 .data 或 .bss 一样,你必须向链接器脚本添加变量(对于这个工具链)或其他一些你可以做的技巧。
这基本上是一些变化:
MEMORY {
one : ORIGIN = 0x000, LENGTH = 256
two : ORIGIN = 0x200, LENGTH = 256
}
SECTIONS {
.text : { *(.text) } > one
__LAB0__ = .;
.bss : {
__LAB1__ = .;
*(.bss)
__LAB2__ = .;
} > two AT > one
__LAB3__ = .;
.got :
{
__LAB4__ = .;
*(.got)
__LAB5__ = .;
} > two AT > one
}
.align
.word __LAB1__
.word __LAB2__
.word __LAB3__
.word __LAB4__
.word __LAB5__
我把变量放在里面和外面,因为虽然人们会认为它们应该总是相同的,但 gnu 链接器却不是。您还需要一些对齐并调整您的复制循环(在 asm 中,不要在 C 中引导 C)(永远不要使用 memset 或 memcpy,只要做对,使用 asm 引导 C),这样您就可以例如 ldm/stm 一次两个或四个寄存器,所以在 32 位或 64 位边界上对齐,然后复制 32 或 64 的倍数。(如果下一个地址不等于结束,则可以使用确切的地址,然后中断循环)
无论如何,就像你已经隔离了 bss 的大小和开始以及你如何处理 .data 一样,你只需要模仿 .data 链接器脚本并复制 .got 的代码(如果你想在 ram 中)。
如果你最终得到.got是因为你试图链接的其他一些已经构建的东西,那么把.got放在你的链接器脚本中。(还有 got.plt)。然后,当然,在尝试运行它之前,请使用 readelf 和 objdump 来确认事物在它们应该在的位置以及地址是否正确等。
所以你的问题有一些令人困惑的漏洞。
-mcpu=cortex-m4 -mfpu=fpv4-sp-d16 -mfloat-abi=hard -fno-common -ffunction-sections -fdata-sections -Wl,--gc-sections -specs=nano.specs -ffreestanding -Wall -O0 -ggdb
这看起来是借来的。这些部分是为了让您在链接器中执行 -gc-sections,但随后您指定了一个单独的链接器命令行
-T link.ld -lc -lgcc
-lc 非常非常可怕,让我不寒而栗。但是好吧,你可以问更多,所以我想问题...-lgcc 不会像链接器那样工作,你需要从 gcc 或添加路径,就像 gnu 是如何工作的。
-O0 不与 -gc-sections 配对,要么你想让它变小,要么你想让它变大,选择一个。或者是某些行业事物的 -O0,出于安全原因故意避免优化。
为了实际删除内容,您可以组合编译中的 -sections 选项和链接中的 -gc-sections。
arm-none-eabi-gcc -O2 -c -ffunction-sections -fdata-sections -mcpu=cortex-m4 so.c -o so.o
arm-none-eabi-ld -Tso.ld -gc-sections -print-gc-sections so.o -o so.elf
arm-none-eabi-objdump -D so.elf
so.elf:文件格式 elf32-littlearm
你想要 gc-print 在那里,这样你就可以看到被删除的内容,有人可能会认为嘿,这做得很好,然后也许在砖砌或对正在发生的事情感到困惑之后,发现有一个错误。
ENTRY(fun)
MEMORY {
one : ORIGIN = 0x000, LENGTH = 256
two : ORIGIN = 0x100, LENGTH = 256
}
SECTIONS {
.text : { *(.text) } > one
.bss : { *(.bss) } > two
}
链接器遵循所有代码路径和数据路径,如果它没有命中,那么它会删除内容,因此您必须非常小心要保留但未按名称引用的项目。
无论如何,这是固定的。
Disassembly of section .text:
00000000 <fun>:
0: 4b01 ldr r3, [pc, #4] ; (8 <fun+0x8>)
2: 2205 movs r2, #5
4: 601a str r2, [r3, #0]
6: 4770 bx lr
8: 00000100 andeq r0, r0, r0, lsl #2
Disassembly of section .bss:
00000100 <x>:
100: 00000000 andeq r0, r0, r0
它删除了我们未使用的数据和函数,为我们的程序节省了大量空间。它经过优化,节省了更多空间,运行速度更快。
您没有提供足够的信息来完全回答,但是我们在评论中介绍了一些提示。您的代码可能没有创建 .got,但您链接的某些代码可能已创建。在这种情况下,让链接器将 .got 项放在闪存中,你就完成了它们。如果你可以在没有图片/馅饼的情况下构建,那就这样做。例如,重新评估所有命令行选项 -fno-common 似乎是借来的和可怕的。除非你需要,否则没有必要用调试器的东西来膨胀代码,但你仍然想为发布和调试而构建,因为发布和调试是(可以)不同的二进制文件,可以有不同的结果,尤其是裸机。
从我们的问题中可以得出,如果您需要初始化 .got,那么您需要像 .data 一样使用变量将其包装在链接器中,并像 .data 一样执行复制循环。使用 .data 作为参考,剪切、粘贴和更改名称。如果您必须更改 .got 运行时。如果你不这样做,那么把.got放在闪存中,它就完成了,没有init。如果可能重新构建任何创建 .got 的东西,那就更好了(如果你不需要位置独立性),更小、更快、更容易处理。仅当您计划使用它并添加了运行时代码(不是由工具生成的,这是由您自己生成的)来完成使用它的工作时,才指定位置独立性。
对于这个平台来说,正确的 got 不会全是零。正如您可能已经了解的那样,它充满了地址,并且需要地址才能工作。除非,好吧,库的构建是为了让你不得不神奇地以某种方式弄清楚所有东西在哪里,然后自己填写 got 和 plt 这让我头疼,你不是在这里做动态库,对吧?
评论
上一个:链接器脚本中的正则表达式
评论