g++ -fdump-class-hierarchy输出中的第一个(int (*)(...))0虚函数表条目是什么?

54

以下是原始代码:

class B1{
public:  
  virtual void f1() {}  
};

class D : public B1 {
public:
  void f1() {}
};

int main () {
    B1 *b1 = new B1();
    D  *d  = new D();

    return 0;
}

编译后,使用g++ -fdump-class-hierarchy得到的虚函数表为:

Vtable for B1
B1::_ZTV2B1: 3u entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI2B1)
16    B1::f1


Vtable for D
D::_ZTV1D: 3u entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI1D)
16    D::f1

我不理解像(int ()(...))0*这样的条目表示什么。当然,它意味着这是一个返回int并接受无限数量参数的函数指针,但我不明白更多的细节。

这个函数指针对应哪个函数?你如何知道?我的机器是64位的。

第二个函数指针末尾有一个关联的地址吗?它对应于谁?

编辑

我使用的编译器是g++:

g++ -v
Using built-in specs.
Target: x86_64-suse-linux
Configured with: ../configure --prefix=/usr --infodir=/usr/share/info --mandir=/usr/share/man --libdir=/usr/lib64 --libexecdir=/usr/lib64 --enable-languages=c,c++,objc,fortran,obj-c++,java,ada --enable-checking=release --with-gxx-include-dir=/usr/include/c++/4.4 --enable-ssp --disable-libssp --with-bugurl=http://bugs.opensuse.org/ --with-pkgversion='SUSE Linux' --disable-libgcj --disable-libmudflap --with-slibdir=/lib64 --with-system-zlib --enable-__cxa_atexit --enable-libstdcxx-allocator=new --disable-libstdcxx-pch --enable-version-specific-runtime-libs --program-suffix=-4.4 --enable-linux-futex --without-system-libunwind --with-arch-32=i586 --with-tune=generic --build=x86_64-suse-linux
Thread model: posix
*gcc version 4.4.1 [gcc-4_4-branch revision 150839] (SUSE Linux)*

我的猜测是构造函数/析构函数...你尝试添加一些看看是否有所不同了吗? - forsvarir
1
@forsvarir,@Pixie:我添加了构造函数,但没有任何区别。顺便说一下,据说只有那些带有虚关键字的函数才会在虚表中得到一个条目。 - Aquarius_Girl
5
好的,我会尽力做到最好。翻译如下:出于好奇,你是怎样输出虚函数表的? - BlueRaja - Danny Pflughoeft
13
g++提供-fdump-class-hierarchy选项以显示虚函数表。 - Aquarius_Girl
5
@BlueRaja: 我认为自2008年起,Visual Studio也支持这个功能。编译器选项是/d1reportSingleClassLayout,在 此视频 中作为一部分进行了解释(接近结尾处)。 - Xeo
显示剩余3条评论
3个回答

58

这些是用于多重继承的偏移量到顶部和类型信息(RTTI)指针。

来自Itanium ABI(您没有使用Itanium编译器,但他们对此的描述非常好)

到顶部的偏移量以ptrdiff_t的形式保存了对象顶部与地址该虚拟表的对象内部位置之间的位移量。它始终存在。该偏移提供了一种从具有虚拟表指针的任何基本子对象找到对象顶部的方法,特别是对于dynamic_cast。
(在完整的对象虚拟表中,因此在其所有主要基类虚拟表中,此偏移的值将为零。[...])

类型信息指针指向用于RTTI的typeinfo对象。它始终存在。给定类的每个虚拟表中的所有条目都必须指向相同的typeinfo对象。typeinfo相等的正确实现是检查指针相等性,除了指向不完整类型的指针(直接或间接)。类型信息指针对于具有虚拟函数即多态类是有效的,并且对于非多态类为零。


更详细的向上偏移量 (按要求)

假设你有一个派生类D,它派生自一个基类B1。当你试图将一个D实例转换为类型B1时会发生什么?由于接受B1对象的函数不知道D的任何信息,因此D vtable的一部分也必须是有效的B1 vtable。这很容易 - 只需使D vtable的开头看起来像B1 vtable,并在此之后添加任何需要的附加条目。期望B1的函数将很高兴,因为他们不会使用超出B1所期望的vtable部分。

然而,如果D现在还从B2派生呢?指向D vtable的指针既不能是一个有效的B1 vtable,也不能是一个有效的B2 vtable!编译器通过在我们的组合D/B1 vtable结尾附加一个单独的B2 vtable来解决这个问题,并在我们尝试从D转换为B2时手动调整vtable指针。

然而,这导致了一个新问题——当我们尝试从B2D转换时会发生什么?编译器不能只是通过与之前相同的量向后调整虚表指针,因为它实际上并不确定我们给它的B2对象是否属于D类型!特别地,dynamic_cast<D>() 必须能够判断我们的对象是否属于D类型。为此,它需要访问对象的RTTI,并且为了那个,它需要知道原始对象的虚表的起始位置。这就是offset-to-top值的目的——它给出了到原始对象虚表起始位置的偏移量,我们可以获得对象的RTTI,C++的复仇神允许我们的庄稼再次生长。 这个页面有一些好的虚表布局示例(在Table 1c下)。请注意,由于使用了虚继承,它们略微复杂,并为每个子类的虚表添加了额外的偏移量。

3
@Anisha Kaul:从你的问题中可以看出,指向f1的指针在该特定实现中作为vtable的第三个元素存储:16 B1::f1 - David Rodríguez - dribeas
1
关于这两个指针的解释,typeinfo 指针是指向一个包含有关此特定对象信息的 typeinfo 实例(常量)的指针。当您在运行时类型信息的对象指针上调用 typeid 时,您将获得 typeinfo const & - David Rodríguez - dribeas
2
将顶部的偏移量用通俗易懂的语言表达出来有点困难...基本上,在多重继承的情况下,对不同基类进行强制类型转换会导致指向不同子对象的指针,这些子对象与指向最派生对象的指针相偏移。该偏移量允许您(也就是dynamic_cast)恢复该偏移并获得指向对象开头的指针。 - David Rodríguez - dribeas
2
结构体A { int x; }; 结构体B { int y; virtual void foo() {} }; 结构体C : A,B {}; int main() { C c; C *cp = &c; A ap = &c; B bp = &c; cout << cp << "," << ap << "," << bp << std::endl; } 这个测试展示了向上转型对指针的影响。直观地说,为了能够从“B”向下转型为“C”,编译器需要“typeinfo”对象来知道转换是否可能,并且需要偏移量以将指针移回原始位置。(实际上,在这种情况下并不严格需要,但在虚继承的情况下是必需的,但那更加复杂。 - David Rodríguez - dribeas
1
手动调整vtable指针对象指针。 - curiousguy
显示剩余6条评论

4
也许第一个条目是用于虚析构函数,而第二个条目是用于RTTI支持?但这只是猜测。

1
第二个条目看起来确实像是RTTI支持,但是这些类的析构函数不是虚拟的。 - Gorpik

2
我认为引用Itanium ABI的答案过于繁琐难懂。我认为由Ruhr-Universität Bochum和其他人撰写的这篇论文(https://www.syssec.ruhr-uni-bochum.de/media/emma/veroeffentlichungen/2019/10/02/ACSAC19-VPS.pdf)以更友好的方式描述了RTTI和Offset-To-Top。
从论文中摘录如下:

enter image description here

RTTI保存了一个指向类的类型信息的指针。除此之外,这个类型信息还包含了类名和它的基类。然而,RTTI是可选的,编译器经常会忽略它。只有当程序员使用dynamic_cast或type_info等功能时才需要它。因此,可靠的静态分析不能依赖于这些信息。不包含RTTI的类的RTTI字段被设置为零。
在一个类使用多重继承(因此具有基础vtable和一个或多个子vtable)时,Offset-to-Top是必需的,就像C类一样。Offset-to-Top指定了子vtable自己的vtblptr与对象开头处的基础vtblptr之间的距离。在我们的例子中,类C的子vtable的vtblptr位于对象的偏移量0x10处,而基础vtable的vtblptr位于偏移量0x0处。因此,在子vtable C的Offset-to-Top字段中存储的两者之间的距离为-0x10。如果vtable是类的基础vtable或者没有使用多重继承,则Offset-to-Top为0。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接