关于在 Linux 上用 gcc 编译的这个程序中的 vtable,nm 告诉我什么?

What is nm telling me regarding the vtable in this program compiled with gcc on Linux?

我将一个 Base class 更改为抽象的(即我将其中一个方法设为纯虚拟的)并重新编译它。当我使用派生的 class 访问 link 时,linker 抱怨 vtable。我用 nm 调查了一些事情,但我不确定 nm 到底告诉了我什么。我只是通过删除 *.o 文件并重新编译 Derived class 来修复问题,但我想了解这里的 vtable 到底发生了什么。

我原来的代码是这样的:

Base.h

class Base {
    public:
       virtual void doSomething();
};

Base.cpp

#include <iostream>
#include <Base.h>
void Base::doSomething() {
    std::cout << "Base::doSomething()" << "\n";
}

Derived.h

#include <Base.h>
class Derived : public Base {
    public:
        Derived();
        void doSomething() override;
};

Derived.cpp

#include <iostream>
#include <Derived.h>
Derived::Derived() {
    std::cout << "Derived::Derived() constructor" << "\n";
}
void Derived::doSomething() {
    std::cout << "Derived::doSomething()" << "\n";
}

Makefile 包含以下内容:

CXXFLAGS = -std=c++14 -pedantic -Wall -Werror -I ./

default: build
clean:
    rm -f proggy
    rm -f *.o

proggy: proggy.o Base.o Derived.o 
    g++ -o proggy proggy.o Base.o Derived.o

Base.o: Base.cpp Base.h
Derived.o: Derived.cpp Derived.h

然后我 运行 制作,一切都很好。记录一下,我在这点上也是运行nm,如下:

$ nm -C Derived.o | grep Base
00000000 W Base::Base()
00000000 W Base::Base()
00000000 n Base::Base()
         U typeinfo for Base
         U vtable for Base

我看到 Base 的类型信息未定义,但似乎对此很满意。

另外,在Base.o这个阶段有这个:

nm -C Base.o | grep Base
000000d4 t _GLOBAL__sub_I__ZN4Base11doSomethingEv
00000000 T Base::doSomething()
00000044 R typeinfo for Base
0000004c R typeinfo name for Base
00000038 R vtable for Base

然后我改变 Base.h 和 Base.cpp 如下,使 class 摘要:

Base.h

class Base {
    public:
       virtual void doSomething() = 0; // pure virtual
};

Base.cpp

#include <iostream>
#include <Base.h>
// void Base::doSomething() {
//     std::cout << "Base::doSomething()" << "\n";
// }

然后当我 运行 make 我得到这个错误:

$ make proggy
g++ -std=c++14 -pedantic -Wall -Werror -I ./   -c -o Base.o Base.cpp
g++ -o proggy proggy.o Base.o Derived.o
Derived.o:(.rodata+0x5c): undefined reference to `typeinfo for Base'
Derived.o: In function `Base::Base()':
Derived.cpp:(.text._ZN4BaseC2Ev[_ZN4BaseC5Ev]+0x48): undefined reference to `vtable for Base'
collect2: error: ld returned 1 exit status
Makefile:17: recipe for target 'proggy' failed
make: *** [proggy] Error 1

其中 c++filt 告诉我以下内容:

$ c++filt _ZN4BaseC2Ev
Base::Base()

然后我 运行 nm 如下,它告诉我:

$ nm -C Derived.o | grep Base
00000000 W Base::Base()
00000000 W Base::Base()
00000000 n Base::Base()
         U typeinfo for Base
         U vtable for Base

我可以看到 Base 的 typeinfo 是未定义的,但它也是在最开始的时候,所以我认为这不是一个问题,但它是。另请注意,现在 Base.o 中没有提及 Base。

$ nm -C Base.o | grep Base  

即此 nm 和 grep 命令未找到任何内容。

最后,我删除了所有 *.o 文件并 运行 重新制作,一切都恢复正常了。然后我 运行 nm 看看 nm 报告了什么,它报告了以下内容:

$ nm -C Derived.o | grep Base
00000000 W Base::Base()
00000000 W Base::Base()
00000000 n Base::Base()
00000000 V typeinfo for Base
00000000 V typeinfo name for Base
00000000 V vtable for Base

这是什么意思?

我的问题是:

  1. nm 在这里告诉我什么以及之前的错误是什么 虚表?
  2. 派生需要什么它没有?
  3. 为什么重新编译派生的 class 可以解决问题,这是做什么的 修复有关 vtable 的问题?

来自日志

$ make proggy
g++ -std=c++14 -pedantic -Wall -Werror -I ./   -c -o Base.o Base.cpp
g++ -o proggy proggy.o Base.o Derived.o

这意味着您的 makefile 不会重新构建 Derived.o,而只会重新构建 Base.o。 这当然会导致问题。

您需要修复 Makefile 以将 Derive.cpp 的适当依赖项添加到 Base.h

vtable存储了实现的虚方法的地址。如果一个class的所有方法都是纯虚的并且实现了none,那么还不需要生成vtable*,因为没有办法自己实例化这样一个class(在调试模式下,vtable 可能仍会生成,将所有内容指向陷阱函数)。

当您使用具有非纯虚函数的 Base.h 编译 Derived.cpp 时,它会引用 Base 的 vtable。

当您随后将 Base.h 更改为仅具有纯虚函数并重建 Base.o 时,来自 Base.o 的 vtable 将消失。此时需要重建Derived.o,否则会一直引用不存在的vtable。

当你重建 Derived.o 时,编译器发现 Base 是一个纯虚拟的 class 并在 Derived.o 本身为它生成一个 vtable,因为它知道那里不在 Base.o.

在基 class 中重新排序虚函数后会出现另一个潜在问题。然后派生 classes,如果不重建,最终可能会在其父 class.

中调用错误的函数

这就是为什么获得正确的依赖链以确保在必要时重建依赖对象文件很重要。

Derived.o: Derived.cpp Derived.h Base.h

* 血淋淋的细节依赖于编译器,但 GCC 的做法是:因为不可能实例化纯虚拟 class,vtable 生成实际上是 推迟 直到至少有一个实现,因为只有这样才可能有一个 class 的实例。因此,vtable 是随每个派生实现生成的,并导出为 "weak" 对象(类型 V),以允许在 link 时间合并潜在的重复项。