2

我正在就如何调试一个我无法简化为最小示例的重大问题提出一些建议。

问题:我编译了链接到许多不同库的应用程序。标志包括: -static-libstdc++ -static-libgcc -pipe -std=c++1z -fno-PIC -flto=10 -m64 -O3 -flto=10 -fuse-linker-plugin -fuse-ld=gold -UNDEBUG -lrt -ldl

编译器是 gcc-7.3.0,针对 binutils-2.30 编译。Boost 使用与程序其余部分相同的标志进行编译,并静态链接。

当程序被链接时,我在我自己的代码和 boost 中都收到关于重定位引用丢弃部分的各种警告。例如:

/tmp/ccq2Ddku.ltrans13.ltrans.o:<artificial>:function boost::system::(anonymous namespace)::generic_error_category::message(int) const: warning: relocation refers to discarded section

然后当我运行程序时,它会在销毁时出现段错误,并带有回溯:

Program received signal SIGSEGV, Segmentation fault.
0x0000000000000000 in ?? ()
(gdb) bt
#0  0x0000000000000000 in ?? ()
#1  0x00007ffff7345a49 in __run_exit_handlers () from /lib64/libc.so.6
#2  0x00007ffff7345a95 in exit () from /lib64/libc.so.6
#3  0x00007ffff732eb3c in __libc_start_main () from /lib64/libc.so.6
#4  0x000000000049b3e3 in _start ()

试图调用的函数指针是 0x0。

如果我使用 static-libstdc++ 删除,链接器警告和运行时段错误就会消失。

如果我从 c++1z 更改为 c++14,链接器警告和运行时段错误就会消失。

如果我删除 -flto,链接器警告和运行时段错误就会消失。

如果我在编译标志中添加“-g”,链接器警告和运行时段错误就会消失。

我曾尝试通过指定 -Wl,--debug=all 来要求黄金进行额外的调试,但它似乎告诉我没有任何相关性。

如果我尝试使用一小部分看起来相关的代码,单独编译并将其链接到相同的 boost 库(即尝试生成最小示例),则没有链接器警告,并且程序运行完成没有问题.

帮助!我能做些什么来缩小问题的范围?

4

1 回答 1

9

此警告通常表示两个编译单元之间的 COMDAT 组的内容不一致。如果编译器发出一个 COMDAT 组 G,其中符号 A 定义在一个编译单元中,但发出相同的组 G,符号 A 和 B 定义在第二个编译单元中,链接器将保留第一个编译单元中的组 G 并丢弃组 G从第二个。在第二个编译单元中从组外部对符号 B 的任何引用都会产生此错误。

原因通常是编译器中的一个错误,使用 -flto 会使诊断变得更加困难。在这种情况下,您的第二个编译单元是链接时优化的结果(*.ltrans.o 文件名)。使用 LTO,您提到的许多更改都会使问题消失,这是非常可信的。

binutils git repo 的 master 分支上的最新版本 gold 有一个新[-Wl,]--debug=plugin选项,它将保存日志和所有临时 .ltrans.o 文件。拥有日志和这些文件,以及所有原始输入文件(您可以通过添加[-Wl,]-t选项获得列表),应该有助于更好地隔离问题。

最新版本的黄金也将打印搬迁所引用的符号。对于本地符号,它将显示符号索引;用于readelf -s获取有关符号的更多信息。对于全局符号,它将显示名称;您可以添加--no-demangle确切名称的选项。

如果它是本地符号,则几乎可以肯定问题出在编译器上。严格禁止从 comdat 组外部引用组中的本地符号。

如果它是全局符号,则可能是编译器问题或源代码中的单一定义规则 (ODR) 违规。您需要识别命名目标文件中的 comdat 组,找到其关键符号,然后找到提供链接器保存的定义的目标文件(-y 选项会有所帮助),并比较这些组中定义的符号由两个对象。这些步骤应该会有所帮助:

(1) 从错误信息开始:

b.o(.data+0x0): warning: relocation refers to symbol "two" defined in discarded section

(2) 在 bo 中寻找符号“二”:

$ readelf -sW b.o | grep two
     7: 0000000000000008     0 NOTYPE  WEAK   DEFAULT    6 two

倒数第二个字段(“6”)是定义“二”的部分编号。

(3) 验证第 6 节实际上是一个 comdat 组:

$ readelf -SW b.o
  [Nr] Name              Type            Address          Off    Size   ES Flg Lk Inf Al
  [ 6] .one              PROGBITS        0000000000000000 000058 000018 00 WAG  0   0  1

sh_flags 字段(“Flg”)中的“G”表示该部分属于 comdat 组。

(4) 找到包含该部分的 comdat 组:

$ readelf -g b.o
COMDAT group section [    1] `.group' [one] contains 1 sections:
   [Index]    Name
   [    6]   .one

这向我们表明,第 6 节是组第 1 节的成员。

(5) 找到该组的关键符号:

$ readelf -SW b.o
      [Nr] Name              Type            Address          Off    Size   ES Flg Lk Inf Al
      [ 1] .group            GROUP           0000000000000000 000040 000008 04      7   8  4

sh_info 字段(“Inf”)告诉我们关键符号是符号 #8,它是“一”。(这应该与步骤 4 中括号中显示的名称相匹配。)

$ readelf -sW b.o
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     8: 0000000000000000     0 NOTYPE  WEAK   DEFAULT    6 one

(6) 现在您可以在-y one链接中添加选项以查找哪些对象提供了“一个”的定义:

$ gcc -Wl,-y,one ...
a.o: definition of one
b.o: definition of one

列出的第一个(ao)是黄金保留的那个;它将丢弃所有具有相同键符号的后续 comdat 组。

如果您使用相同的技术检查在 ao 中定义“one”的 comdat 组,并将属于该组的符号与属于 bo 中的组的符号进行比较,那应该会给您更多线索。

于 2018-04-03T09:00:30.593 回答