解密vtable转储

12

我正在使用 C++ 中的虚拟继承,并想知道类对象是如何布局的。 我有以下三个类:

class A {
private:
    int a;
public:
    A() {this->a = 47;}
    virtual void setInt(int x) {this->a = x;}
    virtual int getInt() {return this->a;}
    ~A() {this->a = 0;}
};

class B {
private:
    int b;
public:
    B() {b = 48;}
    virtual void setInt(int x) {this->b = x;}
    virtual int getInt() {return this->b;}
    ~B() {b = 0;}
};

class C : public A, public B {
private:
    int c;
public:
    C() {c = 49;}
    virtual void setInt(int x) {this->c = x;}
    virtual int getInt() {return this->c;}
    ~C() {c = 0;}
};

我认为它们是正确的 :p

我使用了g++的-fdump-class-hierarchy选项,并得到了以下结果

Vtable for A
A::_ZTV1A: 4u entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI1A)
16    A::setInt
24    A::getInt

Class A
   size=16 align=8
   base size=12 base align=8
A (0x10209fb60) 0
    vptr=((& A::_ZTV1A) + 16u)

Vtable for B
B::_ZTV1B: 4u entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI1B)
16    B::setInt
24    B::getInt

Class B
   size=16 align=8
   base size=12 base align=8
B (0x1020eb230) 0
    vptr=((& B::_ZTV1B) + 16u)

Vtable for C
C::_ZTV1C: 8u entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI1C)
16    C::setInt
24    C::getInt
32    (int (*)(...))-0x00000000000000010
40    (int (*)(...))(& _ZTI1C)
48    C::_ZThn16_N1C6setIntEi
56    C::_ZThn16_N1C6getIntEv

Class C
   size=32 align=8
   base size=32 base align=8
C (0x1020f5080) 0
    vptr=((& C::_ZTV1C) + 16u)
  A (0x1020ebd90) 0
      primary-for C (0x1020f5080)
  B (0x1020ebe00) 16
      vptr=((& C::_ZTV1C) + 48u)

现在这些奇怪的东西是什么意思:(int (*)(...))-0x00000000000000010C::_ZThn16_N1C6setIntEi and (int (*)(...))0?有人能解释一下吗?

谢谢。


1
它是未定义的。每个编译器(甚至是编译器的版本)都会以不同的方式处理它。 - Martin York
您可以使用 c++filt 解码“_ZTI1C”。其他的位置可能会在编译器的一些后续阶段中用函数指针填充。 - Martin York
2个回答

7
我不确定这个答案是否正确,但这是我最好的猜测。
当你有一个继承多个类且非虚拟的类时,该类的布局通常是第一个基类类型的完整对象,然后是第二个基类类型的完整对象,然后是对象本身的数据。如果你看看B,你可以看到A对象的vtable指针,如果你看C,你会发现有指向A和B对象vtable的指针。
因为对象是按这种方式排列的,这意味着如果你有一个B*指针指向一个C对象,那么指针实际上不会在对象的基础上;相反,它将指向中间某个地方。这意味着如果你需要将对象转换为A*,你需要调整B*指针一些量以跳过它返回对象的起始位置。为了做到这一点,编译器需要在某个地方编码你需要跳过的字节数来回到对象的起始位置。我认为第一个(int(*)(...))实际上只是一个原始的字节数,你需要查看它以获取对象的起始位置。如果你注意到,对于A vtable,这个指针是0(因为A的vtable在对象的开头,同样适用于B vtable(因为它也位于对象的开头)。然而,请注意C vtable有两个部分——第一部分是A的vtable,它的第一个疯狂的条目也是零(因为如果你在A vtable,你不需要做任何调整)。然而,在这个表的前半部分之后,似乎是B vtable,注意它的第一个条目是十六进制值-0x10。如果你查看C对象的布局,你会发现B vtable指针在A vtable指针之后16字节。这个-0x10值可能是你需要跳过B vtable指针以返回对象根部的修正偏移量。
每个vtable的第二个疯狂的条目似乎是指向vtable本身的指针。请注意,它总是等于vtable对象的地址(比较vtable的名称和它所指向的内容)。如果你想要进行任何类型的运行时类型识别,这将是必要的,因为通常涉及查看vtable的地址(或者至少是靠近vtable前面的东西)。
最后,关于为什么在C vtable的末尾有一个名字晦涩的setInt和getInt函数,我相信这是因为C类型继承了两组不同的名为setInt和getInt的函数 - 一组通过A,一组通过B。如果我必须猜测,这里的混淆是为了确保编译器内部可以区分这两个虚函数。
希望这可以帮助你!

2
在第一个数字“-0x10”中,我也认为它是最终对象中子对象的偏移量。关于为什么会有这个...我不太同意你的推理,因为编译器在执行强制转换(无论是隐式还是显式)时会看到所有类定义,所以这不是对编译器的提示。然后我想到了其他可能的原因,唯一能想到的就是当通过指向B的指针进行删除时,编译器可以获得指向开头的指针以释放内存。但我不确定。 - David Rodríguez - dribeas
2
在第二个条目中,那些很可能不是指向vtable的指针,而是指向与该特定实例相关联的typeinfo对象的指针。请注意具体值:A::_ZTV1A vs _ZTI1A,以及A对象中的vptr设置为(& A::_ZTV1A - 16u)...它们不重合。 - David Rodríguez - dribeas
这两点都非常好。我相信你在这两个方面都是正确的。 - templatetypedef

6

这是您的转储通过c++filt运行后的结果:

Vtable for A
A::vtable for A: 4u entries
0     (int (*)(...))0
8     (int (*)(...))(& typeinfo for A)
16    A::setInt
24    A::getInt

Class A
   size=16 align=8
   base size=12 base align=8
A (0x10209fb60) 0
    vptr=((& A::vtable for A) + 16u)

Vtable for B
B::vtable for B: 4u entries
0     (int (*)(...))0
8     (int (*)(...))(& typeinfo for B)
16    B::setInt
24    B::getInt

Class B
   size=16 align=8
   base size=12 base align=8
B (0x1020eb230) 0
    vptr=((& B::vtable for B) + 16u)

Vtable for C
C::vtable for C: 8u entries
0     (int (*)(...))0
8     (int (*)(...))(& typeinfo for C)
16    C::setInt
24    C::getInt
32    (int (*)(...))-0x00000000000000010
40    (int (*)(...))(& typeinfo for C)
48    C::non-virtual thunk to C::setInt(int)
56    C::non-virtual thunk to C::getInt()

Class C
   size=32 align=8
   base size=32 base align=8
C (0x1020f5080) 0
    vptr=((& C::vtable for C) + 16u)
  A (0x1020ebd90) 0
      primary-for C (0x1020f5080)
  B (0x1020ebe00) 16
      vptr=((& C::vtable for C) + 48u)

我不知道(int (*)(...))-0x00000000000000010(int (*)(...))0是什么。
C::_ZThn16_N1C6setIntEi/C::non-virtual thunk to C::setInt(int)部分是一种“在存在多重或虚拟继承的情况下优化虚函数调用”的方法,可以在这里找到详细描述。

+1 你比我快 :) 这是我唯一清楚的问题部分 -- 如果你同时得到类转储和汇编,那么很简单:将“this”指针偏移16并跳转到“setInt/getInt”(在每种情况下)。 - David Rodríguez - dribeas
@David:如果有其他部分变得清晰,请将它们添加到答案中。 - Eugen Constantin Dinca
虚拟thunk和非虚拟thunk有什么区别? - choxsword

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