3

我试图了解更多关于 vtables 和 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 位)。我通过将对象的地址转换为 a 来访问该指针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-pointer 的情况下调用的。我通过添加一个小模板化函数解决了这个问题,知道 g++ 将this-pointer 存储在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"
}

我决定分享这个,因为在编写这个小程序的过程中,我对 vtables 的工作原理有了更多的了解,它可能有助于其他人更好地理解这些材料。但是我还有一些问题:
1. 编译 64 位二进制文​​件时使用哪个寄存器(gcc 4.x)来存储 this 指针?我尝试了这里记录的所有 64 位寄存器:http: //developers.sun.com/solaris/articles/asmregs.html
2. this-pointer 何时/如何设置?我怀疑编译器会通过一个对象在每个函数调用上设置 this 指针,就像我刚刚做的那样。这是多态性实际工作的方式吗?(通过先设置 this 指针,然后从 vtable 调用虚函数?)。

4

1 回答 1

4

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

我对 Windows x64 约定不太熟悉,但我相信它是相似的,因为 C++ 方法调用遵循与 C 函数调用完全相同的结构(它使用与 Linux 不同的寄存器),只是this首先带有一个隐式参数。

于 2012-08-15T05:58:13.513 回答