GCC LTO 胖目标文件上的 `nm` 输出不正确

Incorrect output of `nm` on GCC LTO fat object files

如果我有 tmp.c:

char constantFOO[0x12];
char constantBAR[0x34];

我看到 gcc -c tmp.c -o tmp.o && nm tmp.o 节目

0000000000000034 C constantBAR
0000000000000012 C constantFOO

但是如果我用 -flto -ffat-lto-objects 编译,nm 输出符号值的零:

00000000 C constantBAR
00000000 C constantFOO

我可以在两个 .o 文件的 hexdump 中得到 3412 值。

我的问题是

  1. nm 对 LTO fat 文件的行为是否符合预期?我只是给它输入了它不期望的输入,它正在输出垃圾吗?

  2. 原始输出(符号值匹配未初始化数组长度)的解释是什么? This question好像对数组的问题没有帮助,可能是我理解错了

我在 -S 模式(输出汇编语言)下使用 GCC 8.3 编译了你的 tmp.c 有无 -flto -ffat-lto-objects。在这两种情况下,都会发出相同的常量基本定义:

    .comm   constantFOO,18,16
    .comm   constantBAR,52,32

LTO 发出的大部分额外数据进入名为 .gnu.lto_.something 的 ELF 部分。 LTO 模式添加了一个额外的标记对象:

   .comm   __gnu_lto_v1,1,1

出现在 LTO 编译对象中,但不出现在没有 LTO 的对象中。

从表面上看,这根本不应该影响 nm 对这些符号的输出,较低级别的工具 readelf -s 会为它们生成匹配的输出:

$ readelf -s tmp-normal.o

Symbol table '.symtab' contains 9 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS test.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    2 
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    3 
     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 
     6: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 
     7: 0000000000000010    18 OBJECT  GLOBAL DEFAULT  COM constantFOO
     8: 0000000000000020    52 OBJECT  GLOBAL DEFAULT  COM constantBAR

$ readelf -s tmp-lto.o

Symbol table '.symtab' contains 17 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS test.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    2 
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    3 
     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 
     6: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 
     7: 0000000000000000     0 SECTION LOCAL  DEFAULT    6 
     8: 0000000000000000     0 SECTION LOCAL  DEFAULT    7 
     9: 0000000000000000     0 SECTION LOCAL  DEFAULT    8 
    10: 0000000000000000     0 SECTION LOCAL  DEFAULT    9 
    11: 0000000000000000     0 SECTION LOCAL  DEFAULT   10 
    12: 0000000000000000     0 SECTION LOCAL  DEFAULT   12 
    13: 0000000000000000     0 SECTION LOCAL  DEFAULT   11 
    14: 0000000000000010    18 OBJECT  GLOBAL DEFAULT  COM constantFOO
    15: 0000000000000020    52 OBJECT  GLOBAL DEFAULT  COM constantBAR
    16: 0000000000000001     1 OBJECT  GLOBAL DEFAULT  COM __gnu_lto_v1

因此我认为 nm 的行为是一个错误,应该报告给 GNU binutils 的维护者(参见 https://sourceware.org/binutils/)。

至于符号值与数组长度匹配的 "original output",发生的情况通常是 nm 所示的符号值是其在目标文件部分中的偏移量。但是,公共符号不在任何部分中并且没有偏移量,因此 nm 打印符号的大小作为其值。这是 IIRC 的历史行为,可以一直追溯到 System V 的任何一次迭代,它添加了对类似 FORTRAN 的公共数据的支持。请注意 readelf -s 如何将 18 和 52 打印为对象的 大小 ,并将 .comm 的第三个参数(每个符号的所需对齐方式)作为它们的值。

如果你用-fno-common编译你会看到不同的输出:

$ gcc -c -fno-common tmp.c -o tmp-nc.o
$ nm tmp-nc.o 
0000000000000020 B constantBAR
0000000000000000 B constantFOO
$ readelf -s tmp-nc.o | grep constant
     7: 0000000000000000    18 OBJECT  GLOBAL DEFAULT    3 constantFOO
     8: 0000000000000020    52 OBJECT  GLOBAL DEFAULT    3 constantBAR

因为现在您的数组位于 .bss 部分,并且在该部分中有定义的偏移量。

注意 char constantFOO[0x12]; 定义了一个 可写 0x12 char 数组。如果你想让它实际上保持不变,你需要说 const char。 (然后它将被放入目标文件的 .rodata 部分, nmreadelf 的输出将再次不同。)