全局偏移表 STM32 微控制器的 .got 和 .got.plt 必须为零初始化

Global Offset Table .got and .got.plt must be zero-initialized for STM32 microcontroller

提问人:rlakoda 提问时间:11/13/2023 更新时间:11/23/2023 访问量:62

问:

我正在为没有 STM32 IDE 的 ARM Cortex-M4 微控制器编译程序。我使用带有 newlib libc 的 arm-none-eabi 工具链、我为我的特定微控制器改编的链接器脚本以及我从 ST 获取的一些启动代码。

经过无数小时的调试,我发现特定的内存区域必须进行零初始化。否则。。。将包含 CPU 在某个时间点访问的随机数据,从而导致硬故障。 显示这对应于 和 部分:0x200006E80x2000071c 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),启动代码可以使用这些标签来初始化它,而且我真的不想对地址进行硬编码。.bssgotpltgotplt.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.pltobjcopy

我很困惑为什么 GOT 应该用零初始化才能工作,而不是使用某些地址。有没有可能在链接阶段,GOT 以某种方式入“到”.bss 中,即 GOT 部分真的应该是 .bss 的一部分?GOT 位于该部分的正前方(按预期初始化)。.bss

理想情况下,我只想对该部分进行零初始化,而不对启动代码进行任何修改。GOT 要么根本不使用,要么静态填充。.bss

关于这里可能发生的事情的任何想法都受到高度赞赏。

C GCC 接头 STM32

评论

0赞 old_timer 11/13/2023
got 是从 pic/pie 创建的。您可能不需要哪些?如果你在没有浮点库的情况下进行构建,你能隔离你喜欢的每个对象文件,并找到哪个使用 .got(或所有对象文件)吗?
0赞 old_timer 11/13/2023
您只需将 Flash 中的 .got 与 .text 等链接即可,它就可以工作,除非您想要位置独立性,否则您需要将其放入 RAM 并对其进行处理。否则,它将被编译和链接并准备使用。.bss 和 .data 也没有开始和结束标签,somone 在链接器脚本中创建了标签,但同样,您似乎不想为位置独立性构建,因此 .got 已准备好使用并存在于 Flash 中(它可以存在于 Flash 中,更改您的链接器脚本)。
0赞 rlakoda 11/13/2023
@old_timer 你是对的,.got/.got.plt 部分确实包含 ELF 中的一些值,所以我想问题是它们不是由启动代码加载的,因为它不打算与位置无关的代码一起使用(根据 ELF,GOT 在 RAM 中)。无论哪种方式,如果我明确对所有来源使用 -fno-PIC,.got 部分和我的问题都会消失,谢谢!
0赞 old_timer 11/13/2023
根据链接器脚本,got 在 ram 中,只需移动它即可。
0赞 old_timer 11/13/2023
或者是的,如果你能摆脱 pic/pie/PIC/PIE,那就这样做。

答:

0赞 old_timer 11/14/2023 #1

也许这是你已经知道的东西,我无法从这个问题中看出......

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 这让我头疼,你不是在这里做动态库,对吧?

评论

0赞 rlakoda 11/15/2023
非常感谢您提供详细的答案,它帮助我更好地理解了整个事情。正如你正确地假设的那样,我从某个地方借用了标志,但现在我删除了我认为不必要的所有内容:-mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=hard -fno-PIC -Wall -Werror -O0 -ggdb -DSTM32L432xx。这是针对调试版本的(目前只有配置)。对于链接器,我只提供 -T link.ld,没有别的。
0赞 rlakoda 11/15/2023
我的问题是实际上我缺少-fno-PIC。这样,最终二进制文件中就没有 .got 部分。我想我只将我的代码链接到 libc,它也没有 .got,所以幸运的是它不是来自那里。因此,我可以使用此标志完全删除 GOT/位置独立性的解决方案,因为我对代码的重新定位不感兴趣。但多亏了你的回答,我知道如果我想使用与位置无关的代码或动态链接,我必须做什么。