我正在从虚拟表中的地址调用虚拟函数,以测试我对该概念的理解。但是,当我以为自己对虚拟方法表的理解有了突破时,就遇到了另一个我不了解的问题。
在下面的代码中,我创建了一个名为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;
}
如果有人可以指出我在做什么,那将不胜感激。另外,由于跨编译器的工作方式不同,因此我正在使用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
答案 0 :(得分:6)
方法通常确实是作为常规函数实现的,但是它们需要接收this
指针以访问特定实例的数据-实际上,当您在实例上调用方法时,指向该实例的指针会作为隐藏参数传递。
在您的代码中您没有传递它,因此该方法仅返回垃圾-它可能正在使用寄存器或堆栈中的任何东西,就像它是实例指针一样;您很幸运,它不会突然崩溃。
您可以尝试更改原型以接受Car*
参数并将&car
传递给它,但它可能会或可能不会起作用,具体取决于编译器/平台使用的调用约定:>
stdcall
调用约定(对于变数,则使用cdecl
),但是在this
中收到ecx
指针,无法通过常规函数调用来模仿的东西; cdecl
函数来处理,隐式传递this
就像是最后一个参数一样。 答案 1 :(得分:2)
方法是函数,但是方法指针通常不是函数指针。
调用方法的调用约定并不总是与调用函数的调用约定一致。
我们可以解决这个问题。行为更多,但至少在某些情况下有效。
https://jxjxjx.com/wp-admin/edit.php MSVC clang
代码:
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仅包含函数指针组件。
因此,我们可以通过以下方式从vtable中的数据重建方法指针:将缓冲区填充为0,然后将内存解释为方法指针。
要开始工作,我们用与签名匹配的方法创建一个假类型,然后将指针转换为该类型,然后使用从原始类型的vtable重构的成员函数指针进行调用。
我们希望这模仿了编译器用于其他方法调用的调用约定。
在clang / g ++中,非虚拟方法指针是两个指针,第二个指针被忽略。我相信虚拟方法的指针使用第二个指针大小的数据。
在MSVC中,非虚拟方法指针是一个指针的大小。具有虚拟继承树的虚拟方法指针不是一个指针的大小。我认为这违反了标准(要求成员指针之间可以相互广播)。
在两种情况下,vtable似乎都存储了每个非虚拟方法指针的前半部分。
答案 2 :(得分:0)
当您直接向vtable中调用函数指针时,设置x = 2
的构造函数不会运行。您正在从second
返回未初始化的内存,该内存可以是任何值。