为什么使用存储在虚拟方法表中的地址调用虚拟函数的函数会返回垃圾?
Why is the function call to the virtual function using the address stored in the virtual method table returning garbage?
我从虚拟表中的地址调用虚函数作为练习,以测试我对概念的理解。然而,一旦我认为我对虚拟方法表的理解取得了突破,我就遇到了另一个我不明白的问题。
在下面的代码中,我创建了一个名为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: %pn", *mVtable);
printf("First Entry of VTable: %pn", (void*) mVtable[0][0]);
printf("Second Entry of VTable: %pn", (void*) mVtable[0][1]);
if(sizeof(void*) == 8){
printf("64 bitn");
}
int (*firstfunc)() = (int (*)()) mVtable[0][0];
int x = firstfunc();
int (*secondfunc)() = (int (*)()) mVtable[0][1];
int x2 = secondfunc();
printf("first: %dnsecond: %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
方法确实通常作为常规函数实现,但它们需要接收this
指针才能访问特定实例的数据 - 事实上,当您在实例上调用方法时,指向该实例的指针将作为隐藏参数传递。
在你的代码中,你没有传递它,所以该方法只是返回垃圾 - 它可能使用寄存器或堆栈中的任何内容,就好像它是实例指针一样;你很幸运,它没有明显崩溃。
您可以尝试更改原型以接受Car*
参数并向其传递&car
,但它可能会也可能不起作用,具体取决于编译器/平台使用的调用约定:
- 例如,在 Win32/x86/VC++ 上,方法使用
stdcall
调用约定(或可变数组的cdecl
(,但在ecx
中接收this
指针,您无法通过常规函数调用模拟; - 另一方面,x86 gcc 只是将它们作为
cdecl
函数处理,隐式传递this
,就好像它是最后一个参数一样。
方法是函数,但方法指针通常不是函数指针。
调用方法的调用约定并不总是与调用函数的调用约定一致。
我们可以解决这个问题。 还有更多未定义的行为,但这至少有时有效。
MSVC clang g++
法典:
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: %pn", *mVtable);
printf("First Entry of VTable: %pn", (void*)mVtable[0][0]);
printf("Second Entry of VTable: %pn", (void*)mVtable[0][1]);
if(sizeof(void*) == 8){
printf("64 bitn");
}
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: %dnsecond: %d", x, x2);
上面的代码依赖于方法指针,即一对函数指针和第二部分,如果所有 0 都是非虚拟调度,则 vtable 仅包含函数指针组件。
因此,我们可以从 vtable 中的数据重建方法指针,方法是用 0 填充缓冲区,然后将内存解释为方法指针。
为了使调用正常工作,我们使用与我们的签名匹配的方法创建一个假类型,然后将指针强制转换为该类型,并使用从原始类型的 vtable 重建的成员函数指针调用它。
我们希望这模仿编译器用于其他方法调用的调用约定。
在 clang/g++ 中,非虚拟方法指针是两个指针,第二个指针被忽略。 我相信,虚拟方法指针使用第二个指针大小的数据。
在 MSVC 中,非虚拟方法指针的大小相当于一个指针。 具有虚拟继承树的虚拟方法指针的大小不是一个指针的大小。 我认为这违反了标准(要求成员指针之间可铸造(。
在这两种情况下,vtable 似乎存储每个非虚拟方法指针的前半部分。
设置x = 2
的构造函数在直接调用 vtable 中的函数指针时不会运行。您正在从second
返回未初始化的内存,这可以是任何内容。
- C++无法定义虚拟函数 OUTER 类和头文件
- 用常见虚拟函数实现的任意组合来实现派生类的正确方法是什么
- 尝试将unique_ptrs推送到向量时使用纯虚拟函数错误
- 有没有比在库中添加一个并非由所有派生类实现的新虚拟函数更好的设计实践
- 重载 -> shared_ptr 个实例中的箭头运算符<interface>,接口中没有纯虚拟析构函数
- 当覆盖存在时调用基本虚拟"binded to object"函数
- 如何在C++中伪造虚拟可变参数函数模板?
- 类型擦除的std::function与虚拟函数调用的开销
- 重写虚拟函数和继承
- 是否可以使用函数指针调用虚拟析构函数?
- 在没有动态内存的世界中,我是否需要虚拟析构函数?
- 虚拟继承基构造函数消除
- "虚拟""覆盖"析构函数
- 类中的虚拟布尔函数参数不起作用
- 用纯虚拟函数兜圈子
- 将C++子类成员函数(虚拟实现)传递给 C 类型函数指针
- 尝试在 QLabel 上绘画失败(无法在没有对象的情况下调用成员函数"虚拟无效 QLabel::p aintEvent(QPaintEvent*)")
- 声明析构函数虚拟就足够了吗?
- 视觉 C++当我们在基类中使函数成为纯虚拟时,那么在子类中再次使相同的函数虚拟的必要性是什么
- 重载函数(虚拟/非虚拟)