问题 为什么vtable有sizeof(void *)* 2个字节的0x00填充?


我想这是特定于实现的,但是对于使用libstdc ++和libc ++(gcc或clang)的armv7,arm64和x86_64构建,似乎vtable在开头总是有8个字节(64位上16个)的填充,并且取出vtable通常看起来像这样:

ldr.w r0, <address of vtable>
adds  r0, 0x8
str   r0, [r1] ; where r1 is the instance

而vtable看起来像这样:

vtable+0x00: 0x00000000
vtable+0x04: 0x00000000
vtable+0x08: 0xfirstfunc
vtable+0x0c: 0xsecondfunc
vtable+0x10: 0xthirdfunc

等等...

谁知道为什么会这样?


8443
2017-11-27 05:46


起源

检查表以查找派生类。我打赌有 不 前面是零。 - wallyk
不,对于我见过的每个vtable,前面都有一个0。派生类与否,vtable总是+8。 - Ryan Terry


答案:


开头应该只有一个零(大小为void *)(除非没有RTTI编译)。它实际上不一定是,但它通常是,我稍后会解释。

用于(至少gcc发起的)ABI的VTable看起来像:

class_offset
type_info
first_virtual_function
second_virtual_function
etc.

type_info可以是 NULL (0)如果代码是在没有RTTI的情况下编译的。

class_offset 从上面解释了为什么你看到零。这是拥有类中的类偏移量。即有:

class A { virtual meth() {} };
class B { virtual meth() {} };
class C: public A, public B { virtual meth() {} };

会导致主要课程 CA 从位置开始 0 在课堂上 C 和 B 从位置开始 4 (要么 8在课堂上 C

指针在那里,所以你可以从任何类指针找到指向拥有对象的指针。因此,对于任何“主要”类,它将始终如此 0 但对于 B 类虚拟表有效 C 这将是背景 -4 要么 -8。实际上你需要检查VTable for C(下半部分),因为编译器通常不会单独生成VTable:

_ZTV1C:
    // VTable for C and A within C
    .quad   0
    .quad   _ZTI1C
    .quad   _ZN1CD1Ev
    .quad   _ZN1CD0Ev
    .quad   _ZN1C4methEv
    // VTable for B within C
    .quad   -8
    .quad   _ZTI1C
    .quad   _ZThn8_N1CD1Ev
    .quad   _ZThn8_N1CD0Ev
    .quad   _ZThn8_N1C4methEv

在早期的编译器中,偏移量用于在调用方法之前计算指向拥有类的实际指针。但是当它直接在拥有类上调用方法时减慢了这种情况,现代编译器会生成stub,它直接减去偏移量并跳转到方法的主要实现(正如你可以从方法名称猜测的那样 - 注意 8):

_ZThn8_N1C4methEv:
    subq    $8, %rdi
    jmp     _ZN1C4methEv

10
2017-11-27 06:17



嗯,非常感谢。这也有助于我理解为什么在多重继承的情况下存在非虚拟thunk。无论如何,也许是因为在我拆解的二进制文件的情况下,vtable上面有2个void *是没有RTTI? - Ryan Terry
@RyanTerry:是的,这可能就是原因。如果在没有RTTI的情况下编译代码,则保留结构,但类型info成员为NULL。我更新了答案。 - Zbynek Vyskovsky - kvr000