首页 > 解决方案 > 链接错误:在废弃部分中定义的构造 vtable

问题描述

最初的问题分布在来自不同项目的数十万个 LoC 中。它包含很多成分:内联汇编、虚拟继承、间接级别、不同的编译器和编译器选项。(这就像一部惊悚片。)我很难简化为这个 SSCCE:

// a.hpp
struct A {
    int i;
    ~A() { asm("" : "=r"(i)); }
};

struct B : public virtual A { };

struct C : public B { };

struct D {
    D(C);
};

// a.cpp
#include "a.hpp"

void f(C) {
}

D::D(C c) {
    f(c);
}

// main.cpp
#include "a.hpp"

int main() {
    C c;
    D d(c);
}

使用这些命令行构建:

g++ -O3 -fPIC -c a.cpp
clang++ -O3 -fPIC -c main.cpp
clang++ -fuse-ld=gold main.o a.o -o main

链接器输出是:

a.o:a.cpp:function D::D(C) [clone .cold]: error: relocation refers to global symbol "construction vtable for B-in-C", which is defined in a discarded section
  section group signature: "_ZTV1C"
  prevailing definition is from main.o
clang-10: error: linker command failed with exit code 1 (use -v to see invocation)

我相信 gcc、clang 或 gold 都存在错误。我的问题是它在哪里?(我想这是黄金,但我想在报告错误之前确定。)

FWIW:正如我所说,所有的成分都很重要,例如,如果asm删除了,问题就会消失。使问题消失的更显着的变化是:

  1. 对所有 TU 使用相同的编译器,(无论是 g++ 还是 clang++。)
  2. 与 ld 链接(即删除-fuse-ld=gold
  3. 编译main.cpp不带-O3.
  4. 编译main.cpp不带-fPIC.
  5. 在链接器命令行中交换a.o和。main.o

标签: c++linker-errorsgold-linker

解决方案


这似乎是 GCC 中的一个错误,但在 ABI 之外的内联 asm 可能只会导致 GCC 和 Clang 之间不幸的不兼容。

问题是内联汇编使 GCC 认为它~A::A()可以引发异常,因此它在 中创建异常处理路径D::D(),这需要为 B-in-C 构建 vtable,它放置在还包含 vtable 的 COMDAT 组中对于 C ( _ZV1C)。

由于 Clang 不会在_ZV1CCOMDAT 组中生成构造 vtable,但 GCC 会生成,因此您最终会遇到链接器可能会保留 Clang 生成的 COMDAT 组并丢弃 GCC 生成的具有构造 vtable 的版本的情况。如果您链接期望额外符号定义的 GCC 生成的代码,则会收到此错误。

在你的链接中反转 main.o 和 ao 也可以解决这个问题,因为所有三个链接器都会阻止 ao 的 COMDAT 组,它是第一个看到的。

这是 GCC 从 ao 为 D::D() 生成的代码:

0000000000000002 <_ZN1DC1E1C>:
   2:   48 83 ec 18             sub    $0x18,%rsp
   6:   48 8b 06                mov    (%rsi),%rax
   9:   48 8b 40 e8             mov    -0x18(%rax),%rax
   d:   8b 04 06                mov    (%rsi,%rax,1),%eax
  10:   89 44 24 08             mov    %eax,0x8(%rsp)
  14:   48 8b 05 00 00 00 00    mov    0x0(%rip),%rax
            17: R_X86_64_REX_GOTPCRELX  _ZTV1C-0x4
  1b:   48 8d 40 18             lea    0x18(%rax),%rax
  1f:   48 89 04 24             mov    %rax,(%rsp)
  23:   48 89 e7                mov    %rsp,%rdi
  26:   e8 00 00 00 00          callq  2b <_ZN1DC1E1C+0x29>
            27: R_X86_64_PLT32  _Z1f1C-0x4
  2b:   eb 17                   jmp    44 <_ZN1DC1E1C+0x42>
  2d:   48 89 c7                mov    %rax,%rdi
  30:   48 8d 05 00 00 00 00    lea    0x0(%rip),%rax
            33: R_X86_64_PC32   _ZTC1C0_1B+0x14
  37:   48 89 04 24             mov    %rax,(%rsp)
  3b:   89 44 24 08             mov    %eax,0x8(%rsp)
  3f:   e8 00 00 00 00          callq  44
            40: R_X86_64_PLT32  _Unwind_Resume-0x4
  44:   48 83 c4 18             add    $0x18,%rsp
  48:   c3                      retq   

从偏移量 0x2d 到callqat 0x3f 的代码是异常处理路径,在调用f(c). 0x30 处的lea指令引用 B-in-C ( _ZTC1C0_1B) 的构造 vtable 中的条目。

如果没有该内联汇编,GCC 将生成与 clang 相同的代码,没有异常处理路径,也不需要构造 vtable。

编译--no-exceptions,问题也消失了。

无论在-O0-O1-O2-O3.

至少 GCC 在编译 a.cpp 和 main.cpp 时是一致的,因此可以说这种情况根本不包括在 C++ ABI 中,并且 GCC 和 Clang 可以自由地区别对待。我做了一些简单的尝试,用内联 asm 以外的东西进行复制,但不能。

至于为什么你从 gold 中得到一个错误,而不是来自 bfd 或 lld,gold 正在报告可能是一个真正的错误,但在这种特殊情况下,由于永远不会抛出异常,异常处理代码会从未执行过。但是,当您使用 bfd ld 或 lld 链接时,lea0x30 处的指令不会被重新定位,没有任何警告,并且在调用f().


推荐阅读