gcc / ld: 在静态链接的 ELF 二进制文件中存在重叠的段 (.tbss, .init_array)。

6

我正在使用gcc版本4.8.2 (Debian 4.8.2-21)在x86_64机器上静态编译一个非常简单的hello-world代码:

gcc test.c -static -o test

我得到了一个可执行的ELF文件,其中包括以下几个部分:

[17] .tdata            PROGBITS         00000000006b4000  000b4000
     0000000000000020  0000000000000000 WAT       0     0     8
[18] .tbss             NOBITS           00000000006b4020  000b4020
     0000000000000030  0000000000000000 WAT       0     0     8
[19] .init_array       INIT_ARRAY       00000000006b4020  000b4020
     0000000000000010  0000000000000000  WA       0     0     8
[20] .fini_array       FINI_ARRAY       00000000006b4030  000b4030
     0000000000000010  0000000000000000  WA       0     0     8
[21] .jcr              PROGBITS         00000000006b4040  000b4040
     0000000000000008  0000000000000000  WA       0     0     8
[22] .data.rel.ro      PROGBITS         00000000006b4060  000b4060
     00000000000000e4  0000000000000000  WA       0     0     32

请注意,.tbss段分配在地址0x6b4020..0x6b4050(0x30字节),并且与.init_array段在0x6b4020..0x6b4030(0x10字节)和.fini_array段交叉分配在0x6b4030..0x6b4040(0x10字节),并且与.jcr段在0x6b4040..0x6b4048(8字节)交叉。
请注意,它不会与以下部分相交,例如.data.rel.ro,但这可能是因为.data.rel.ro的对齐方式为32,因此它不能放置在0x6b4060之前。
生成的文件可以正常运行,但我仍然不确定它是如何工作的。从我在glibc文档中阅读的内容来看,.tbss只是用于线程本地存储(即分配的内存临时空间,实际上未映射到物理文件)的.bss段。是否.tbss段非常特殊,以至于它可以重叠其他段?.init_array.fini_array.jcr是否非常无用(例如,当TLS相关代码运行时它们不再需要),因此它们可以被bss覆盖?还是这是某种错误?
基本上,如果我尝试读取地址0x6b4020,我会得到什么内容?.tbss的内容还是.init_array的指针?为什么?

很可能我不会帮助你。但也许你应该阅读AMD64 gcc ABI参考文档。http://www.x86-64.org/documentation/abi.pdf。据我记得,访问线程本地存储是由段寄存器FS或GS“前缀”的。如果我没记错的话,你的线程将寻址FS:0x6b4020。 - ibre5041
我记得在某个地方读到过,线程本地存储将使用FS段前缀操作,这纯粹是glibc的约定(不是平台ABI,也不是SysV调用约定的一部分)...我要去尝试阅读Drepper关于TLS的手册了... - GreyCat
该部分被标记为NOBITS,这意味着ELF文件内没有与其相关联的数据,只有大小。对于NOBITS部分,偏移字段被正式填充。另一个这样的部分的例子是.bss,即零初始化数据部分。只要我们知道该部分是零初始化的,就没有必要在文件中实际存储零,知道大小就足够了。 - ach
@AndreyChernyakhovskiy:我对没有意义的偏移量(即0xb4020)没有问题,但我对内存中地址范围重叠有问题(即0x6b4020)。 - GreyCat
2个回答

7

.tbss 段的虚拟地址是没有意义的,因为该段仅作为由 GLIBC 中的线程实现分配的 TLS 存储的模板。

这个虚拟地址的方式是 .tbss 在默认的链接器脚本中跟随 .tbdata

...
.gcc_except_table   : ONLY_IF_RW { *(.gcc_except_table .gcc_except_table.*) }
/* Thread Local Storage sections  */
.tdata          : { *(.tdata .tdata.* .gnu.linkonce.td.*) }
.tbss           : { *(.tbss .tbss.* .gnu.linkonce.tb.*) *(.tcommon) }
.preinit_array     :
{
  PROVIDE_HIDDEN (__preinit_array_start = .);
  KEEP (*(.preinit_array))
  PROVIDE_HIDDEN (__preinit_array_end = .);
}
.init_array     :
{
   PROVIDE_HIDDEN (__init_array_start = .);
   KEEP (*(SORT(.init_array.*)))
   KEEP (*(.init_array))
   PROVIDE_HIDDEN (__init_array_end = .);
}
...

因此,它的虚拟地址就是前面一节(.tbdata)的虚拟地址加上前一节的大小(最终需要一些填充以达到所需的对齐)。下一个是.init_array(如果存在,则为.preinit_array),其位置应该通过相同的方式确定,但.tbss非常特殊,因此在GNU LD中给出了深度硬编码处理:
/* .tbss sections effectively have zero size.  */
if ((os->bfd_section->flags & SEC_HAS_CONTENTS) != 0
    || (os->bfd_section->flags & SEC_THREAD_LOCAL) == 0
    || link_info.relocatable)
  dotdelta = TO_ADDR (os->bfd_section->size);
else
  dotdelta = 0;    // <----------------
dot += dotdelta;

.tbss 不可重定位,它设置了 SEC_THREAD_LOCAL 标志,并且没有内容(NOBITS),因此执行 else 分支。换句话说,无论 .tbss 有多大,链接器都不会推进其后面的部分(也被称为“点”)的位置。

还要注意,.tbss 位于一个不可加载的 ELF 段中:

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x00000000000b1f24 0x00000000000b1f24  R E    200000
  LOAD           0x00000000000b2000 0x00000000006b2000 0x00000000006b2000
                 0x0000000000002288 0x00000000000174d8  RW     200000
  NOTE           0x0000000000000158 0x0000000000400158 0x0000000000400158
                 0x0000000000000044 0x0000000000000044  R      4
  TLS            0x00000000000b2000 0x00000000006b2000 0x00000000006b2000 <---+
                 0x0000000000000020 0x0000000000000060  R      8              |
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000     |
                 0x0000000000000000 0x0000000000000000  RW     8              |
                                                                              |
 Section to Segment mapping:                                                  |
  Segment Sections...                                                         |
   00     .note.ABI-tag ...                                                   |
   01     .tdata .ctors ...                                                   |
   02     .note.ABI-tag ...                                                   |
   03     .tdata .tbss    <---------------------------------------------------+
   04

谢谢!这正是我在寻找的 - SEC_THREAD_LOCAL 标志使得这个部分特殊(不是 .tbss 名称 - 实际上,它可以有任何名称)。回答清晰简洁。很抱歉赏金来晚了... - GreyCat
你知道如何获取tbss的大小并将其放入变量中吗?我正在使用-static -nostdlib编译,并有一些__thread变量。我不知道需要分配多少内存,所以想知道是否有一种方法可以在运行时获取值。链接器是否将其放入某个变量中?我能否在tbss段之前和之后放置标签以计算大小(len = labelAfterTBSS - labelBeforeTBBS)?我该怎么办?我尝试过询问,但没有得到答案 https://stackoverflow.com/questions/69498912/how-do-i-setup-thread-when-i-use-static-nostdlib - Eric Stotch
@EricStotch 我认为链接器不会向最终可执行文件添加任何保存节信息的变量。而且你很难告诉编译器去修复 .tbss 中变量的顺序。你最好的选择是解析 ELF 文件结构,例如使用 BFD 库的与节相关的函数 - Hristo Iliev

2
这很简单,只要你理解以下两点:
1)什么是SHT_NOBITS
2)什么是tbss段
SHT_NOBITS表示该段在文件内不占用空间。
通常,NOBITS段(如bss)会放置在所有PROGBITS段之后,在已加载的段的末尾。
tbss是一个特殊的段,用于保存未初始化的线程本地数据,这些数据对程序的内存映像有贡献。请注意:此段必须为每个程序线程保存唯一数据。
现在让我们谈谈重叠。我们有两种可能的重叠-二进制文件内和内存内。
1)二进制文件偏移量:
在二进制文件中,在该段下没有数据可写入。在文件内它不占用空间,因此链接器在tbss声明后立即启动下一个init_array段。您可以将其大小视为代码的特殊服务信息,而不是大小。
if (isTLSSegment) tlsStartAddr += section->memSize();

因此它不会与文件内部的任何内容重叠。

2) 内存偏移

tdata和tbss段可能会在启动时被动态链接器执行重定位而发生修改,但之后该段数据将作为初始化镜像保存下来并且不再修改。对于每个线程(包括初始线程),都会分配新的内存,并将初始化镜像的内容复制到其中。这确保了所有线程具有相同的起始条件。

这就是使tbss(和tdata)如此特殊的地方。

不要将它们的内存偏移视为静态已知的-它们更像是每个线程工作的“生成模式”。因此,它们也不能与“正常”的内存偏移重叠-它们以其他方式进行处理。

您可以参考本文了解更多信息。


我并不是在抱怨文件偏移量,而是在抱怨重叠的平面内存地址。也就是说,.tbss.init_array.fini_array.jcr 相交。你提到了 .tbss 部分是“特殊”的 - 为什么会这样?它是由名称(.tbss)确定的吗?如果是这样,是否还有其他类似的“特殊”部分?谁决定最终映射到0x6b4020? - GreyCat
扩展答案以涵盖内存偏移量,并添加了参考链接。 - Konstantin Vladimirov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接