我想学习更多关于虚函数表和虚函数指针的内部工作原理,所以我决定尝试使用一些技巧直接访问虚函数表。我创建了两个类,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"
}
我决定分享这个小程序,因为在编写过程中我对虚函数表的工作原理有了更深入的了解,这可能有助于其他人更好地理解这个知识点。但是我仍然有一些问题:
1. 在编译64位二进制文件时,使用哪个寄存器(gcc 4.x)来存储this指针?我尝试了所有64位寄存器,如此文档所述:http://developers.sun.com/solaris/articles/asmregs.html
2. this指针是何时/如何设置的?我怀疑编译器会通过一个类似我刚才所做的方式,在每次调用对象的函数时设置this指针。这就是多态实际上的工作方式吗?(首先设置this指针,然后从虚函数表中调用虚函数?)