Warning: file_get_contents(/data/phpspider/zhask/data//catemap/0/docker/9.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
C++ 为什么使用虚拟方法表中存储的地址调用虚拟函数会返回垃圾?_C++_C++14_Member Function Pointers_Virtual Functions_Virtual Table - Fatal编程技术网

C++ 为什么使用虚拟方法表中存储的地址调用虚拟函数会返回垃圾?

C++ 为什么使用虚拟方法表中存储的地址调用虚拟函数会返回垃圾?,c++,c++14,member-function-pointers,virtual-functions,virtual-table,C++,C++14,Member Function Pointers,Virtual Functions,Virtual Table,我从虚拟表中的地址调用虚拟函数是为了测试我对这个概念的理解。然而,当我认为我在对虚拟方法表的理解上取得了突破时,我就遇到了另一个我不理解的问题 在下面的代码中,我创建了一个名为Car的类,它包含一个成员变量x和两个虚拟函数,第一个和第二个。现在,我通过破解虚拟表来调用这两个虚拟方法。第一个函数返回正确答案,但第二个函数返回一些随机值或垃圾,而不是它初始化为的值 #include <cstdio> class Car { private: int x; virtua

我从虚拟表中的地址调用虚拟函数是为了测试我对这个概念的理解。然而,当我认为我在对虚拟方法表的理解上取得了突破时,我就遇到了另一个我不理解的问题

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

#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;
}

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

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

方法通常作为常规函数实现,但它们需要接收
指针以访问特定实例的数据-事实上,在实例上调用方法时,会将指向该实例的指针作为隐藏参数传递

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

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

  • 例如,在Win32/x86/VC++上,方法使用
    stdcall
    调用约定(或变量的
    cdecl
    ),但在
    ecx
    中接收
    this
    指针,这是通过常规函数调用无法模拟的
  • 另一方面,x86gcc只将它们作为
    cdecl
    函数处理,隐式地传递
    这个
    ,就好像它是最后一个参数一样
方法实际上通常是作为常规函数实现的,但是它们需要接收
这个
指针来访问特定实例的数据-事实上,当您在实例上调用方法时,指向实例的指针会作为隐藏参数传递

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

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

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

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

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

代码:

模板
构造并伪造它;
样板
结构伪造{
R方法(Args…);
使用mptr=decltype(&fake_it::method);
};
样板
结构伪造{
R方法(参数…)常数;
使用mptr=decltype(&fake_it::method);
};
样板
使用方法\u ptr=typename伪\u it::mptr;
样板
构造此\u帮助程序{
使用type=false_it*;
};
样板
构造此\u帮助程序{
使用type=false_it const*;
};
样板
使用this_ptr=typename this_helper::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);
Car;
void*carPtr=&car;
自动**mVtable=(uintptr\u t**)(CARTR);
printf(“VTable:%p\n”,*mVtable);
printf(“VTable的第一个条目:%p\n”,(void*)mVtable[0][0]);
printf(“VTable的第二个条目:%p\n”,(void*)mVtable[0][1]);
if(sizeof(void*)==8){
printf(“64位\n”);
}
auto firstfunc=to_method_ptr(mVtable[0][0]);
intx=(这个函数(carPtr)->*firstfunc)();
auto secondfunc=to_method_ptr(mVtable[0][1]);
intx2=(这个ptr(carPtr)->*secondfunc)();
printf(“第一个:%d\n第二个:%d”,x,x2);
上面的代码依赖于方法指针,方法指针是一对函数指针和第二部分,如果所有0都是非虚拟分派,那么vtable只能包含函数指针组件

因此,我们可以从vtable中的数据重构方法指针,方法是在缓冲区中填充0,然后将内存解释为方法指针

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

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


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

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

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);