它只是 gcc 生成的非常愚蠢的指针算术代码deregister_tm_clones()。它实际上并不访问这些地址的内存。
概括
没有对这些指针进行访问;它们只是充当地址标签,而 GCC 对于如何比较两个(重新定位的)地址很傻。
这两个函数是C 和 C++中事务支持的一部分。有关更多详细信息,请参阅GNU libitm。
背景
我在 x86-64 上运行 Ubuntu 16.04.3 LTS (Xenial Xerus),安装了 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()是相同的,并且包括一个非常奇怪的指针比较测试。的代码deregister_tm_clones()在 7.1.0 中是固定的,它是一个简单的地址测试。
这两个函数的源代码位于 GCC 源代码 的libgcc/crtstuff.c中。
在这台机器上,对于我上面提到的所有 GCC 版本,objdump -t /usr/lib/gcc/ARCH/VERSION/crtbegin.o显示.tm_clone_table、__TMC_LIST__和__TMC_END__,所以在 GCC 源代码中,USE_TM_CLONE_REGISTRY和HAVE_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.aGNU libitm中的函数。当_ITM_registerTMCloneTable/_ITM_deregisterTMCloneTable符号未定义时,重定位代码产生零作为它们的地址。
因此,当数组为空和/或_ITM_registerTMCloneTable/_ITMderegisterTMCloneTable符号未定义时,代码什么也不做:只是一些花哨的指针运算。
请注意,代码不会从任何内存地址加载指针值。地址(的__TMC_LIST__、__TMC_END__、_ITM_registerTMCloneTable和_ITM_deregisterTMCloneTable)由链接器/重定位器提供,作为代码中的立即 32 位文字。(这就是为什么如果您查看目标文件的反汇编,您只会看到地址为零的原因。)
调查
有问题的代码deregister_tm_clones出现在最开始:
004008c0
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
4008d5: b8 00 00 00 00 mov $0x0,%eax
4008da: 48 85 c0 test %rax,%rax
4008dd: 74 11 je 4008f0
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:
(这个特定的例子来自于在 x86-64 上静态使用 gcc-6.3.0 用 C 编译一个基本的 Hello, World! 例子)。
objdump -h如果我们查看相同二进制文件的节头( ),我们会发现地址实际上没有映射到任何段0x6cbb50;0x6cbb5f那
24 .data 00001ad0 00000000006ca080 00000000006ca080 000ca080 2**5
25 .bss 00001878 00000000006cbb60 00000000006cbb60 000cbb50 2**5
即.data涵盖地址0x6ca080至0x6cbb4f,并.bss涵盖
0x6cbb60至0x6cd3d8。
汇编代码似乎使用了无效地址!
但是,该地址是非常有效的,因为在该地址 ( )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 错误 77813中,Marc Glisse 确实提到它(上面的前者)确实是 GCC 最终生成的。(错误本身与此没有直接关系,因为它是关于 GCC 将表达式优化为零,仅影响 libitm 用户,并且很容易修复。)
此外,在 gcc-6.3.0 和 gcc-7.1.0 之间,当生成的代码放弃了这种无聊时,函数的 C 源代码没有改变。改变的是 GCC 如何为这种指针比较生成代码(在某些情况下)。