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

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

本文关键字:vtable 程序 nm 什么 告诉我 编译 Linux 于这个 gcc      更新时间:2023-10-16

我将一个基类更改为抽象(即我将其方法之一设为纯虚拟(并重新编译了它。当我去将它与派生类链接时,链接器抱怨 vtable。我用nm调查了事情,但我不确定nm到底在告诉我什么。我只是通过删除 *.o 文件并重新编译 Derived 类来解决问题,但我想了解这里的 vtable 到底发生了什么。

我最初的代码是这样的:

基数.h

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

基地.cpp

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

派生.h

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

派生.cpp

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

生成文件包含以下内容:

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 的 typeinfo未定义,但这一切似乎对此都足够满意。

此外,在 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如下所示以使类抽象:

基数.h

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

基地.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 文件并再次运行 make,一切都很好。然后我运行 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. 为什么重新编译派生类可以解决问题,这做了什么 修复关于 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

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

您需要修复Makefile以添加适当的依赖项,以便Derive.cppBase.h

vtable 存储已实现的虚拟方法的地址。如果一个类的所有方法都是纯虚拟的,并且没有一个被实现,那么现在还不需要生成 vtable*,因为没有办法单独实例化这样的类(在调试模式下,vtable 仍然可以生成,将所有内容指向陷阱函数(。

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

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

当你重建Derived.o时,编译器看到Base是一个纯虚拟类,并在Derived.o本身中为它生成一个vtable,因为它知道Base.o中没有一个。

在基类中对虚函数重新排序后会出现另一个潜在问题。然后,派生类(如果不重新生成(最终可能会在其父类中调用错误的函数。

这就是为什么正确获取依赖项链以确保在必要时重新生成依赖对象文件很重要的原因。

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

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