虚函数表和this指针

3

我想学习更多关于虚函数表和虚函数指针的内部工作原理,所以我决定尝试使用一些技巧直接访问虚函数表。我创建了两个类,BaseDerv,每个类都有两个 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指针,然后从虚函数表中调用虚函数?)

这篇文章有点长,不太适合作为一个典型的SO问题。你能否把它压缩到核心问题上?(实际上,你的帖子中是否有任何与最后的问题直接相关的内容?) - Oliver Charlesworth
@Oli Charlesworth 对我来说是这样,因为这就是我提出这些问题的方式。第二个问题询问编译器是否使用类似的方法来实现多态性,所以我认为我应该加入我的想法。您会建议我通过其他渠道分享这些信息并提出问题吗? - JorenHeit
你应该在这里提出问题,但要提供足够的上下文以使它们有意义(对于这些特定的问题,似乎您不需要任何上下文)。如果你想分享自己发现的东西,最好建立一个博客。 - Oliver Charlesworth
尝试一下这个 - http://www.codeproject.com/Articles/10900/Polymorphism-in-C - Superman
1个回答

4
在Linux x86_64上,以及其他类Unix操作系统上,函数调用遵循System V ABI (AMD64),该ABI本身遵循C++的IA-64 C++ ABI。根据方法的类型,this指针通过第一个参数或第二个参数隐式传递(当返回值具有非平凡的复制构造函数或析构函数时,它必须作为堆栈上的临时变量存在,并且第一个参数隐含地是指向该空间的指针);否则,虚拟方法调用与C中的函数调用相同(整数/指针参数在%rdi%rsi%rdx%rcx%r8%r9中溢出到堆栈;整数/指针返回在%rax中;浮点数在%xmm0-%xmm7中;等等)。虚拟方法分派通过查找vtable中的指针然后像非虚拟方法一样调用它来工作。

我对Windows x64的惯例不太熟悉,但我相信它与C函数调用遵循完全相同的结构(使用不同于Linux的寄存器),只是首先隐式使用this参数。


对于 Microsoft 兼容的 C++ 实现,vtable 在 Don Box 的 Essential COM & 中的介绍部分有解释(我听说还有另一本书 Inside COM)。 - Chawathe Vipul S

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