我曾经认为指向虚拟函数表(VFT,也称为虚拟方法表,VMT)的指针是对象二进制表示的第一个 32 位字。
但是现在我看到一个VFT,它的索引是13(!!!!),也就是offset=0x34。(我写“索引”是因为调用 Qt 函数的代码o.metaObject()
是((func***)o)[13][0](o)
)。OMG,这是怎么回事?为什么VFT地址位于...在哪里?
编辑(在抱怨问题不清楚之后):
每个具有虚函数的对象都有一个指向虚函数表的指针。通常,这是对象二进制表示中的第一个 32 位值(并且可以作为 访问((void**)objAddr)[0]
)。但是在下面的例子中,VMT 指针的偏移量不是 0!(函数名可能会被c++filt
; 为了可读性起见,类名已缩短为Abc
和Xyz
):
.text:02EF171C _ZN3XyzC2EP7QObject ; constructor Xyz::Xyz(QObject*), r0 = objAddr, r1 = QObject addr
.text:02EF171C PUSH.W {R4-R8,LR}
.text:02EF1720 MOV R4, R0
.text:02EF1722 LDR R5, =(_GLOBAL_OFFSET_TABLE_ - 0x02EF1730)
.text:02EF1724 MOV R7, R1
.text:02EF1726 BL.W _ZN4AbcdC2EP7QObject ; superclass_constructor(objAddr)
.text:02EF172A ; ---------------------------------------------------------------------------
.text:02EF172A LDR R3, =(_ZTVN3XyzE_ptr - 0x27E4BE0) ; vtable for Xyz
.text:02EF172C ADD R5, PC ; _GLOBAL_OFFSET_TABLE_
.text:02EF172E MOV R6, R4
.text:02EF1730 MOV R1, R7
.text:02EF1732 LDR R3, [R5,R3] ; _ZTVN3XyzE_ptr ; pointer to vtable for Xyz
.text:02EF1734 ADDS R3, #8 ; *_ptr points to the (-2)nd element of VMT
.text:02EF1736 STR.W R3, [R6],#0x34 ; OOPS! the offset is 0x34 !!!
我希望能够为任何对象找到指向 VMT 的指针,但如上例所示,指向 VMT 的指针不一定是((void**)objAddr)[0]
。
所以问题是:
1)为什么 VMT 指针在对象二进制表示的中间?这个地方一定有什么特别之处。
2)如何找出 VMT 指针的实际位置?(理想情况下,在运行时给定对象地址。我有代码来区分有效地址和无效地址。我对 Android/ARM 的 GCC 感兴趣,尽管适用于不同平台的技术可能适用。)
PS在Android上检测有效地址的代码是:
#include <unistd.h>
#include <fcntl.h>
int isValidPtr(const void*p, int len) {
if (!p) { return 0; }
int ret = 1;
int nullfd = open("/dev/random", O_WRONLY); // does not work with /dev/null !!!
if (write(nullfd, p, len) < 0) {
ret = 0; /* Not OK */
}
close(nullfd);
return ret;
}
更新
在以下示例中,VMT 偏移量为 0:
class Base {
public:
int x,y;
};
class Derived: public Base {
public:
int z;
Derived();
virtual int func();
virtual int func2();
};
来自Base*
to 的强制Derived*
编译为:SUBS R0, #4
int test3(Base*b) {
Derived*d = (Derived*)b;
int r = addDerived(*d);
return r;
}
; test3(Base *)
_Z5test3P4Base
CBZ R0, loc_1C7A
SUBS R0, #4
B.W _Z10addDerivedR7Derived ;
更新2
我试过
struct Cls2 {
unsigned x[13];
Derived d;
Cls2();
};
这是拆卸:
.text:00001CE2 _ZN4Cls2C2Ev ; Cls2::Cls2(void)
.text:00001CE2 PUSH {R4,LR}
.text:00001CE4 MOV R4, R0
.text:00001CE6 ADD.W R0, R0, #0x34
.text:00001CEA BL _ZN7DerivedC2Ev ; Derived::Derived(void)
.text:00001CEE MOV R0, R4
.text:00001CF0 POP {R4,PC}
也就是说, 的 VFT 指针Cls2::d
确实会在偏移量 0x34 处,但是没有STR.W R3,[R6],#0x34
,因此它不是 Willem Hengeveld 建议的 #2。
但是如果我们注释掉构造函数,
struct Cls2 {
unsigned x[13];
Derived d;
// Cls2();
};
在
int testCls2() {
Cls2 c;
return c.d.func2();
}
我们得到
.text:00001C9E _Z8testCls2v
.text:00001C9E var_18 = -0x18
.text:00001C9E PUSH {LR}
.text:00001CA0 SUB SP, SP, #0x4C
.text:00001CA2 ADD R0, SP, #0x50+var_18
.text:00001CA4 BL _ZN7DerivedC2Ev ; Derived::Derived(void)
.text:00001CA8 ADD R0, SP, #0x50+var_18
.text:00001CAA BL _ZN7Derived5func2Ev ; Derived::func2(void)
.text:00001CAE ADD SP, SP, #0x4C
.text:00001CB0 POP {PC}
这与原始代码非常相似,但在我的情况下,VMTvtable for Xyz
是Xyz::Xyz()
从封闭函数而不是从封闭函数写入的。