为什么register_tm_clones和deregister_tm_clones引用.bss段之外的地址?这段内存分配在哪里?

12

register_tm_clonesderegister_tm_clones引用了超出我的RW段结束地址的内存地址。这个内存是如何被追踪的?

示例:在下面的示例中,deregister_tm_clones引用了内存地址0x601077,但我们分配的最后一个RW段,.bss从地址0x601069开始,大小为0x7,加起来得到0x601070。所以该引用明显超出了为.bss段分配的空间,应该在我们的堆空间中,但是谁在管理它呢?

objdump -d main
...
0000000000400540 <deregister_tm_clones>:
  400540:       b8 77 10 60 00          mov    $0x601077,%eax
  400545:       55                      push   %rbp
  400546:       48 2d 70 10 60 00       sub    $0x601070,%rax
  40054c:       48 83 f8 0e             cmp    $0xe,%rax
...

readelf -S main
...
[25] .data             PROGBITS         0000000000601040  00001040
   0000000000000029  0000000000000000  WA       0     0     16
[26] .bss              NOBITS           0000000000601069  00001069
   0000000000000007  0000000000000000  WA       0     0     1
[27] .comment          PROGBITS         0000000000000000  00001069
   0000000000000058  0000000000000001  MS       0     0     1
[28] .shstrtab         STRTAB           0000000000000000  000019f2
   000000000000010c  0000000000000000           0     0     1
[29] .symtab           SYMTAB           0000000000000000  000010c8
   00000000000006c0  0000000000000018          30    47     8
[30] .strtab           STRTAB           0000000000000000  00001788
   000000000000026a  0000000000000000           0     0     1
请注意,引用从.bss节的末尾开始。当我使用gdb检查分配的内存时,我看到有足够的空间,所以它可以正常工作,但我不知道这个内存是如何管理的。
Start Addr         End Addr          Size        Offset objfile
0x400000           0x401000          0x1000      0x0 /home/nobody/main
0x600000           0x601000          0x1000      0x0 /home/nobody/main
0x601000           0x602000          0x1000      0x1000 /home/nobody/main
0x7ffff7a17000     0x7ffff7bd0000    0x1b9000    0x0 /usr/lib64/libc-2.23.so

我在其他部分找不到对它的任何引用。在加载给.bss段的部分中也没有为它保留空间:

LOAD         0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
             0x0000000000000259 0x0000000000000260  RW     200000

有人可以解释一下这些函数吗?源代码在哪里?我已经阅读了所有关于事务性内存的参考资料,但它们只涵盖编程而不是实现。我找不到一个编译器选项来删除这些代码,除了当然会留下什么都没有的-nostdlibs

也许这些函数是malloc的一部分吗?但对于那些没有使用malloc、线程或STM的代码,我不确定是否应该将它们链接到我的代码中。

另请参见gcc添加到linux ELF的函数有哪些?

更多细节:

$ make main
cc -c -o main.o main.c
cc -o main main.o

$ which cc
/usr/bin/cc

$ cc --version
    cc (GCC) 6.2.1 20160916 (Red Hat 6.2.1-2)
Copyright (C) 2016 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

$ cc --verbose
Using built-in specs.
COLLECT_GCC=cc
COLLECT_LTO_WRAPPER=/usr/libexec/gcc/x86_64-redhat-linux/6.2.1/lto-wrapper
Target: x86_64-redhat-linux
Configured with: ../configure --enable-bootstrap
 --enable-languages=c,c++,objc,obj-c++,fortran,ada,go,lto
 --prefix=/usr --mandir=/usr/share/man --infodir=/usr/share/info
 --with-bugurl=http://bugzilla.redhat.com/bugzilla --enable-shared
 --enable-threads=posix --enable-checking=release --enable-multilib
 --with-system-zlib --enable-__cxa_atexit --disable-libunwind-exceptions
 --enable-gnu-unique-object --enable-linker-build-id
 --with-linker-hash-style=gnu --enable-plugin --enable-initfini-array
 --disable-libgcj --with-isl --enable-libmpx --enable-gnu-indirect-function
 --with-tune=generic --with-arch_32=i686 --build=x86_64-redhat-linux
Thread model: posix
gcc version 6.2.1 20160916 (Red Hat 6.2.1-2) (GCC)

你能否添加你正在使用的具体gcc版本?我发现在binutils测试中有一个引用了__TMC_END__+7的地方,这与你看到的值相匹配,所以我想知道gcc历史上是否有一些无意义的东西,比如在文件中可能是(char *)(__TMC_END__+1)-1 - R.. GitHub STOP HELPING ICE
这是另一个有趣的例子,似乎表明链接器或gcc中存在错误。 - brookbot
这是另一个有趣的示例,似乎表明链接器或gcc中存在错误:请注意,.bss部分受到特殊处理,因为它在文件中不需要空间,它使用与前一个部分.data相同的文件偏移量。.data 00000000006050d8 000050d8 0000000000000004 .bss 00000000006050e0 000050dc 0000000000000030,.dynsym显示__start_bss作为基于文件偏移而非地址的地址 86:00000000006050dc 0 NOTYPE GLOBAL DEFAULT 27 __bss_start。���自/bin/getconf的数据 - 是否有某种方法确定编译此代码的gcc版本? - brookbot
同样可以在gcc 4.8.5以及Red Hat上看到这些。它是从_dl_init_internal调用的,并且似乎是在libpthread中实现的。 - Leeor
btpw(我猜还有@Leeor,因为你确认了OP的发现)声称:“在下面的示例中,deregister_tm_clones引用内存地址0x601077”,但是反汇编并没有显示出来:它只显示了指针算术运算,没有实际的内存访问。请提供deregister_tm_clones函数的完整反汇编代码,以便我们可以验证它是否与我的答案匹配。 - Nominal Animal
1个回答

15

这只是gcc为生成的非常愚蠢的指针算术代码。它实际上并不访问那些地址的内存。

摘要

没有在这些指针处进行访问; 它们只充当地址标签,而GCC在比较这两个(重定位的)地址时表现得很愚蠢。

这两个函数是C和C ++中transaction support的一部分。有关更多详细信息,请参见GNU libitm

背景

我正在运行 Ubuntu 16.04.3 LTS (Xenial Xerus) 的 x86-64 版本,安装了 GCC 的 4.8.5、4.9.4、5.4.1、6.3.0 和 7.1.0 版本。函数 register_tm_clones()deregister_tm_clones() 是从 /usr/lib/gcc/x86-64/VERSION/crtbegin.o 中编译而来的。对于所有版本,register_tm_clones() 没有问题(没有奇怪的地址)。对于版本 4.9.4、5.4.1 和 6.3.0,deregister_tm_clones() 的代码相同,包括一个非常奇怪的指针比较测试。在 7.1.0 中修复了 deregister_tm_clones() 的代码,它是一个简单的地址测试。

这两个函数的源代码在 GCC 源代码中的 libgcc/crtstuff.c 中。

在这台机器上,运行 objdump -t /usr/lib/gcc/ARCH/VERSION/crtbegin.o 命令可以显示出对于我之前提到的所有 GCC 版本,都有 .tm_clone_table__TMC_LIST____TMC_END__,因此,在 GCC 源代码中,同时定义了 USE_TM_CLONE_REGISTRYHAVE_GAS_HIDDEN 两个标识符。因此,我们可以用 C 语言描述这两个函数。

typedef void (*func_ptr) (void);

extern void _ITM_registerTMCloneTable(void *, size_t); 
extern void _ITM_deregisterTMCloneTable(void *); 

static func_ptr __TMC_LIST__[] = { };
extern func_ptr __TMC_END__[];

void deregister_tm_clones(void)
{
    void (*fn)(void);
    if (__TMC_LIST__ != __TMC_END__) {
        fn = _ITM_deregisterTMCloneTable;
        if (fn != NULL)
            fn(__TMC_LIST__);
    }
}

void register_tm_clones(void)
{
    void (*fn)(void);
    size_t size;

    size = (__TMC_END__ - __TMC_LIST__) / 2;

    if (size > 0) {
        fn = _ITM_registerTMCloneTable;
        if (fn != NULL)
            fn(__TMC_LIST__, size);
     }
}

基本上,__TMC_LIST__ 是一个函数指针数组,而 size 是数组中函数指针对的数量。如果该数组不为空,则会调用一个名为 _ITM_registerTMCloneTable()_ITM_deregisterTMCloneTable() 的函数,它们在 libitm.a 中定义,GNU libitm。当未定义 _ITM_registerTMCloneTable/_ITM_deregisterTMCloneTable 符号时,重定位代码将它们的地址作为零返回。
因此,当数组为空和/或未定义 _ITM_registerTMCloneTable/_ITMderegisterTMCloneTable 符号时,代码什么也不做:只进行一些花哨的指针算术运算。
请注意,该代码不会从任何内存地址加载指针值。链接器/重定位器提供了 __TMC_LIST____TMC_END___ITM_registerTMCloneTable_ITM_deregisterTMCloneTable 的地址,作为代码中的立即 32 位文字。(这就是为什么,如果您查看对象文件的反汇编,您会看到地址为零。) 调查 deregister_tm_clones 的问题代码出现在开头处:
004008c0 <deregister_tm_clones>:
  4008c0: b8 57 bb 6c 00          mov    $0x6cbb57,%eax
  4008c5: 55                      push   %rbp
  4008c6: 48 2d 50 bb 6c 00       sub    $0x6cbb50,%rax
  4008cc: 48 83 f8 0e             cmp    $0xe,%rax
  4008d0: 48 89 e5                mov    %rsp,%rbp
  4008d3: 76 1b                   jbe    4008f0 <deregister_tm_clones+0x30>
  4008d5: b8 00 00 00 00          mov    $0x0,%eax
  4008da: 48 85 c0                test   %rax,%rax
  4008dd: 74 11                   je     4008f0 <deregister_tm_clones+0x30>
  4008df: 5d                      pop    %rbp
  4008e0: bf 50 bb 6c 00          mov    $0x6cbb50,%edi
  4008e5: ff e0                   jmpq   *%rax
  4008e7:        (9-byte NOP)
  4008f0: 5d                      pop    %rbp
  4008f1: c3                      retq   
  4008f2:       (14-byte NOP)
  400900:

(这个例子是使用gcc-6.3.0在x86-64上静态编译C语言的Hello World示例生成的。)

如果我们查看相同二进制文件的节标题(objdump -h),我们会发现地址0x6cbb500x6cbb5f实际上没有映射到任何段;那么

24 .data  00001ad0  00000000006ca080  00000000006ca080  000ca080  2**5
25 .bss   00001878  00000000006cbb60  00000000006cbb60  000cbb50  2**5

即,.data 覆盖地址 0x6ca0800x6cbb4f,而 .bss 覆盖 0x6cbb600x6cd3d8

似乎汇编代码正在使用无效的地址!

然而,0x6cbb50 地址是非常有效的,因为在该地址有一个零大小的隐藏符号(objdump -t):

006cbb50 g     O .data 0000000000000000 .hidden __TMC_END__

因为我静态编译了二进制文件,所以在这里__TMC_END__符号是.data段的一部分;通常它在.bss中。无论如何,这并不重要,因为__TMC_END__符号大小为零:我们可以将其地址用作我们想要的任何计算的一部分,我们只是不能对其进行解引用,因为它不包含数据,大小为零。

这使得deregister_tm_clones函数中的第一个重定位地址,即在此情况下的0x0x6cbb57,成为了关键。

如果我们看看代码实际上对该值执行了什么操作,结果发现由于某种愚蠢的原因,编译二进制代码基本上是在计算

long temporary = relocated__TMC_LIST__address + 7;
long difference = temporary - relocated__TMC_END__address;
if (difference <= 14)
    return;

由于使用的比较函数是带符号的比较,因此上述行为与以下完全相同。
long temporary = relocated__TMC_LIST__address;
long difference = temporary - relocated__TMC_END__address;
if (difference <= 7)
    return;

无论如何,显而易见的是__TMC_LIST__ == __TMC_END__,并且重新定位的地址在OP的二进制文件和上面的二进制文件中是相同的。 附录

我不确切地知道为什么GCC会生成

if ((__TMC_END__ + 7) - __TMC_LIST <= 14)

而不是

if (__TMC_END__ <= __TMC_LIST__)

但是在GCC bug 77813中,Marc Glisse确实提到了(上面的内容)是GCC最终生成的内容。(这个错误本身与此无直接关系,因为它是关于GCC将表达式优化为零的问题,仅影响libitm用户,并且很容易修复。)

此外,在gcc-6.3.0和gcc-7.1.0之间,当生成的代码放弃那种愚蠢行为时,函数的C源文件并没有改变。发生变化的是GCC如何为这种指针比较生成代码(在某些情况下)。


谢谢,非常好的答案。我猜这与我的情况无关(在我的情况中,我确实看到了未解释的写入BSS部分),但我仍然学到了有用的东西。 - Leeor
@Leeor:你能上传一个小二进制文件(例如一个“Hello World”程序),然后我检查一下吗?或者甚至通过电子邮件将其发送给我(我的地址显示在我的主页上,从我的个人资料中链接)?你知道,我有点难以接受你的情况是不同的——你的问题没有显示任何与我回答相反的东西——;我想要能够验证我是错的。 - Nominal Animal
很遗憾,我不能提供代码。这是一个使用OpenMP的简单光线追踪器(smallpt),但在某个系统和特定的gcc版本上,由于位于BSS部分的只读数组发生冲突,它突然变慢了。分析显示,有大量的嗅探表明有人正在写入这些共享行,并使它们从所有核心中失效。我希望这个问题能解释为什么会出现这种情况(但不幸的是我没有仔细阅读代码...)。尽管如此,奖励是应得的 :) - Leeor
@Leeor:啊,我现在明白了;在你的情况下,问题并不在deregister_tm_clones()函数中,而是在其他地方。 (我同意,由于缓存行乒乓而导致的减速令人懊恼。我基本上已经接受使用匿名内存映射来共享并行模拟器线程的只读数据,这样我就可以调用mprotect(map,len,PROT_READ)来捕获任何写入尝试。) - Nominal Animal
构造 if ((__TMC_END__ + 7) - __TMC_LIST <= 14) 是试图强制将东西落在8字节边界上的典型方式。如果没有这种风格,东西有时会落在4字节边界上,甚至完全不对齐,这取决于目标CPU。缺乏对齐然后会导致后续问题不断累积。 - Linas
哇,你真是深入其中的人啊,太棒了! - undefined

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