我试图更多地了解vtable和vpointers的内部工作原理,所以我决定尝试使用一些技巧直接访问vtable。我创建了两个类Base
和Derv
,每个类都有两个virtual
函数(Derv
覆盖Base
的函数。)
class Base
{
int x;
int y;
public:
Base(int x_, int y_) : x(x_), y(y_) {}
virtual void foo() { cout << "Base::foo(): x = " << x << '\n'; }
virtual void bar() { cout << "Base::bar(): y = " << y << '\n'; }
};
class Derv: public Base
{
int x;
int y;
public:
Derv(int x_, int y_) : Base(x_, y_), x(x_), y(y_) {}
virtual void foo() { cout << "Derived::foo(): x = " << x << '\n'; }
virtual void bar() { cout << "Derived::bar(): y = " << y << '\n'; }
};
现在,编译器为每个类添加一个vtable指针,占用内存中的前4个字节(32位)。我通过将对象的地址转换为size_t*
来访问此指针,因为指针指向另一个大小为sizeof(size_t)
的指针。现在可以通过索引vpointer并将结果转换为适当类型的函数指针来访问虚函数。我将这些步骤封装在一个函数中:
template <typename T>
void call(T *ptr, size_t num)
{
typedef void (*FunPtr)();
size_t *vptr = *reinterpret_cast<size_t**>(ptr);
FunPtr fun = reinterpret_cast<FunPtr>(vptr[num]);
//setThisPtr(ptr); added later, see below!
fun();
}
当以这种方式调用其中一个成员函数时,例如, call(new Base(1, 2), 0)
调用Base :: foo(),很难预测会发生什么,因为它们是在没有this
指针的情况下调用的。我通过添加一个模板化的函数来解决这个问题,知道g ++将this
指针存储在ecx
寄存器中(这会强制我使用-m32
编译器标志进行编译):
template <typename T>
void setThisPtr(T *ptr)
{
asm ( mov %0, %%ecx;" :: "r" (ptr) );
}
取消注释上面代码段中的setThisPtr(ptr)
行现在使其成为一个有效的程序:
int main()
{
Base* base = new Base(1, 2);
Base* derv = new Derv(3, 4);
call(base, 0); // "Base::foo(): x = 1"
call(base, 1); // "Base::bar(): y = 2"
call(derv, 0); // "Derv::foo(): x = 3"
call(derv, 1); // "Derv::bar(): y = 4"
}
我决定分享这个,因为在编写这个小程序的过程中,我对vtable如何工作有了更多的了解,并且可能会帮助其他人更好地理解这些材料。
不过我还有一些问题:
1.在编译64位二进制文件时,使用哪个寄存器(gcc 4.x)来存储this-pointer?我尝试了所有64位寄存器,如下所示:http://developers.sun.com/solaris/articles/asmregs.html
2.何时/如何设置此指针?我怀疑编译器通过一个对象在每个函数调用上设置this指针的方式与我刚才这样做的方式类似。这是多态实际工作的方式吗? (首先设置this-pointer,然后从vtable调用虚函数?)。
答案 0 :(得分:4)
在Linux x86_64上,我相信其他类UNIX操作系统,函数调用遵循System V ABI (AMD64),它本身跟随C ++的IA-64 C++ ABI。根据方法的类型,this
指针要么通过第一个参数或第二个参数隐式传递(当返回值具有非平凡的复制构造函数或析构函数时,它必须作为临时堆栈存在,并且第一个参数隐含地是指向该空间的指针);否则,虚方法调用与C中的函数调用相同(%rdi
,%rsi
,%rdx
,%rcx
,%r8
,{{中的整数/指针参数1}},溢出到堆栈;整数/指针返回%r9
;浮动在%rax
- %xmm0
;等等。虚方法调度通过在vtable中查找指针然后像非虚方法一样调用它来工作。
我对Windows x64约定不太熟悉,但我认为它类似于C ++方法调用遵循与C函数调用完全相同的结构(使用与Linux不同的寄存器),只是隐式{{ 1}}争论第一。