为什么虚表会有大小为sizeof(void*) * 2字节的0x00填充?

11

我想这取决于具体的实现,但对于使用libstdc++和libc++(gcc或clang)的armv7、arm64和x86_64构建,似乎虚函数表始终在开头有8个字节(64位系统上为16个字节)的填充,获取虚函数表通常是这样的:

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

而虚函数表看起来像这样:

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

有人知道为什么会这样吗?


检查派生类的表格。我敢打赌前面没有零。 - wallyk
不,就我见过的每个虚函数表而言,开头都有一个0。无论是派生类还是基类,虚函数表始终在+8处。 - Ryan Terry
1个回答

10

在开始时应该只有一个大小为void *的零(除非没有使用RTTI编译)。实际上不必这样,但通常是这样的,稍后我会解释。

(至少gcc源自的)ABI的虚表看起来像:

class_offset
type_info
first_virtual_function
second_virtual_function
etc.

如果编译时没有启用RTTI,则type_info可能为NULL0)。

上面的class_offset解释了为什么在那里看到零。这是所属类中的类偏移量。也就是说,有:

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

这将导致在主类 C 中,A 从位置 0 开始,B 从位置 4(或 8)开始。

指针的作用是让您能够从任何类指针中找到拥有对象的指针。因此,对于任何“主”类,它将始终为 0,但对于在 C 上下文中有效的 B 类虚表,它将为 -4-8。您实际上需要检查 C 的 VTable(后半部分),因为编译器通常不会单独生成 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

在早期的编译器中,使用偏移量来计算调用方法之前所属类的实际指针。但由于直接在所属类上调用方法时会减慢速度,现代编译器更倾向于生成存根,直接减去偏移量并跳转到方法的主要实现(从方法名称可以猜出-注意 8 ):

<code>_ZThn8_N1C4methEv:
    subq    $8, %rdi
    jmp     _ZN1C4methEv
</code>

哦,非常感谢。这也帮助我理解了为什么在多重继承的情况下会有非虚拟thunk。无论如何,也许我正在反汇编的二进制文件中,上面有2个void*是因为没有RTTI? - Ryan Terry
@RyanTerry:是的,那可能就是原因了。如果代码没有启用运行时类型识别(RTTI),则结构将被保留,但类型信息成员为NULL。我已经更新了答案。 - Zbynek Vyskovsky - kvr000

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