为啥使用存储在虚方法表中的地址对虚函数的函数调用返回垃圾?

Posted

技术标签:

【中文标题】为啥使用存储在虚方法表中的地址对虚函数的函数调用返回垃圾?【英文标题】:Why is the function call to the virtual function using the address stored in the virtual method table returning garbage?为什么使用存储在虚方法表中的地址对虚函数的函数调用返回垃圾? 【发布时间】:2018-09-06 18:03:16 【问题描述】:

我从虚拟表中的地址调用虚拟函数作为一个练习,以测试我对这个概念的理解。然而,当我以为我对虚拟方法表的理解有了突破时,我又遇到了一个我只是不明白的问题。

在下面的代码中,我创建了一个名为Car 的类,其中包含一个成员变量x 和两个虚函数,first 和second。现在,我通过破解虚拟表来调用这两个虚拟方法。第一个函数返回正确答案,但第二个函数返回一些随机值或垃圾,而不是初始化的值。

#include <cstdio>

class Car

private:
    int x;

    virtual int first()
    
        printf("IT WORKS!!\n");
        int num = 5;
        return num;
    
    virtual int second()
    
        printf("IT WORKS 2!!\n");
        //int num  = 5;
        return x;
    


public:

    Car()
        x = 2;
    
;

int main()

    Car car;
    void* carPtr = &car;
    long **mVtable =(long **)(carPtr);

    printf("VTable: %p\n", *mVtable);
    printf("First Entry of VTable: %p\n", (void*) mVtable[0][0]);
    printf("Second Entry of VTable: %p\n", (void*) mVtable[0][1]);

    if(sizeof(void*) == 8)
        printf("64 bit\n");
    

    int (*firstfunc)() = (int (*)()) mVtable[0][0];
    int x = firstfunc();    

    int (*secondfunc)() = (int (*)()) mVtable[0][1];
    int x2 = secondfunc();

    printf("first: %d\nsecond: %d", x, x2);
    return 0;

如果有人能指出我做错了什么,那将不胜感激。此外,由于这在编译器中的工作方式不同,我正在使用 c++14 在http://cpp.sh/ 上对其进行测试。

该代码输出输出,其中“垃圾”第二个输出可能会发生变化:

VTable: 0x400890
First Entry of VTable: 0x400740
Second Entry of VTable: 0x400720
64 bit
IT WORKS!!
IT WORKS 2!!
first: 5
second: -888586240 

【问题讨论】:

对于Car 的哪个实例,您通过指针secondfunc 调用函数second 你能转换成成员函数指针而不是函数指针吗? 在调用对象的方法之前,您没有提供对象的地址(通过 x86/AMD ABI,前六个参数放在寄存器中)。 好的。我明白为什么它现在不起作用了。但是,现在我需要弄清楚如何才能真正做到这一点。我不知道这是否可能,但维基建议它是。 en.wikipedia.org/wiki/Virtual_method_table#Invocation 【参考方案1】:

方法确实通常作为常规函数实现,但它们需要接收this 指针来访问特定实例的数据 - 事实上,当您在实例上调用方法时,指向实例的指针被传递为一个隐藏参数。

在您的代码中,您没有将其传入,因此该方法只会返回垃圾 - 它可能使用寄存器或堆栈中发生的任何内容,就好像它是实例指针一样;你很幸运,它没有明显崩溃。

您可以尝试更改您的原型以接受Car* 参数并将&amp;car 传递给它,但这可能会或可能不会起作用,具体取决于您的编译器/平台使用的调用约定:

在 Win32/x86/VC++ 上,例如,方法使用stdcall 调用约定(或cdecl 用于可变参数),但在ecx 中接收this 指针,这是您无法通过常规函数模拟的打电话; 另一方面,x86 gcc 只是将它们作为cdecl 函数处理,隐式传递this,就好像它是最后一个参数一样。

【讨论】:

少数几个可以礼貌地说“这是垃圾”的情况之一 我引用的 wiki 表明它可以以一种奇怪的方式完成。不确定它是否在语法上是正确的。 en.wikipedia.org/wiki/Virtual_method_table#Invocation【参考方案2】:

方法是函数,但方法指针一般不是函数指针。

调用方法的调用约定并不总是与调用函数的调用约定一致。

我们可以解决这个问题。还有更多未定义的行为,但至少有时会起作用。

MSVCclangg++

代码:

template<class Sig>
struct fake_it;

template<class R, class...Args>
struct fake_it<R(Args...)>
    R method(Args...);

    using mptr = decltype(&fake_it::method);
;
template<class R, class...Args>
struct fake_it<R(Args...) const> 
    R method(Args...) const;

    using mptr = decltype(&fake_it::method);
;

template<class Sig>
using method_ptr = typename fake_it<Sig>::mptr;

template<class Sig>
struct this_helper 
    using type=fake_it<Sig>*;
;
template<class Sig>
struct this_helper<Sig const>
    using type=fake_it<Sig> const*;
;

template<class Sig>
using this_ptr = typename this_helper<Sig>::type;

现在这个测试代码:

Car car;
void* carPtr = &car;
auto **mVtable = (uintptr_t **)(carPtr);
printf("VTable: %p\n", *mVtable);
printf("First Entry of VTable: %p\n", (void*)mVtable[0][0]);
printf("Second Entry of VTable: %p\n", (void*)mVtable[0][1]);

if(sizeof(void*) == 8)
    printf("64 bit\n");


auto firstfunc = to_method_ptr<int()>(mVtable[0][0]);
int x = (this_ptr<int()>(carPtr)->*firstfunc)();    

auto secondfunc = to_method_ptr<int()>(mVtable[0][1]);
int x2 = (this_ptr<int()>(carPtr)->*secondfunc)();

printf("first: %d\nsecond: %d", x, x2);

上面的代码依赖于方法指针是一对函数指针和第二个部分,如果全 0 是非虚拟分派,并且 vtable 只包含函数指针组件。

所以我们可以通过用 0 填充缓冲区来从 vtable 中的数据重建方法指针,然后将内存解释为方法指针。

为了使调用生效,我们使用与我们的签名匹配的方法创建一个假类型,然后将指针转换为该类型,并使用从原始类型的 vtable 重构的成员函数指针调用它。

我们希望,这模仿编译器用于其他方法调用的调用约定。


在 clang/g++ 中,非虚拟方法指针是两个指针,第二个被忽略。我相信虚方法指针使用第二个指针大小的数据。

在 MSVC 中,非虚拟方法指针是一个指针的大小。具有虚拟继承树的虚拟方法指针不是一个指针的大小。我认为这违反了标准(要求成员指针之间可以相互转换)。

在这两种情况下,vtable 似乎都存储了每个非虚拟方法指针的前半部分。

【讨论】:

看起来非常h​​acky,但我喜欢它。稍后会尝试一下。然而,老实说,我对他们在 wiki 上采取的方法特别感兴趣。不确定这是否只是玩世不恭的不完整 bs 。 en.wikipedia.org/wiki/Virtual_method_table#Invocation @legendaryz 您不能通过函数调用复制 MSVC 调用约定;您可以使用此 hack 复制它。它还使用相同的 hack 复制了 clang/g++ 调用约定。 哦,好的。我懂了。谢谢。【参考方案3】:

设置x = 2 的构造函数在您将函数指针直接调用到vtable 时不会运行。您正在从 second 返回未初始化的内存,它可以是任何东西。

【讨论】:

不,ctor 直接在 main 中运行。 @o11c 该函数被直接调用,没有任何引用它被调用的类的实例(即this),因此x未初始化,因为构造函数没有运行在调用mVTable[0][1]的上下文中 Car car 被构造,但被调用的函数没有被赋予指向carthis 指针。我们处于未定义的行为领域 @TimRandall 此处未定义的行为开始于未通过 this... ? 很确定这一切都非常规,但必须有办法做到这一点。我可以以类似的方式从对象中检索 x 的值。所以我认为,@TimRandall 的解释是有道理的。它也与其他答案一致。附言我引用的 wiki en.wikipedia.org/wiki/Virtual_method_table#Invocation

以上是关于为啥使用存储在虚方法表中的地址对虚函数的函数调用返回垃圾?的主要内容,如果未能解决你的问题,请参考以下文章

C++自问自答

探索c++虚函数表

虚函数表指针与多态

虚函数表指针与多态

对虚函数进行重载是啥意思?

Swift中结构体的方法调度&内存分区