Jos*_*man 17 c++ cross-platform
假设您有一个C++类,如:
class Foo {
public:
virtual ~Foo() {}
virtual DoSomething() = 0;
};
Run Code Online (Sandbox Code Playgroud)
C++编译器将调用转换为vtable查找:
Foo* foo;
// Translated by C++ to:
// foo->vtable->DoSomething(foo);
foo->DoSomething();
Run Code Online (Sandbox Code Playgroud)
假设我正在编写JIT编译器,并且我想获取类Foo的特定实例的DoSomething()函数的地址,因此我可以生成直接跳转到它的代码,而不是执行表查找和间接分支.
我的问题是:
有没有任何标准的C++方法来做到这一点(我几乎可以肯定答案是否定的,但是为了完整起见,我想问一下).
有没有任何独立于远程编译器的方法,比如有人实现了提供访问vtable的API的库?
如果他们能够工作,我会完全打开黑客.例如,如果我创建了自己的派生类并且可以确定其DoSomething方法的地址,我可以假设vtable是Foo的第一个(隐藏)成员并搜索其vtable直到找到我的指针值.但是,我不知道获得这个地址的方法:如果我写的话,&DerivedFoo::DoSomething我得到一个指向成员的指针,这是完全不同的东西.
也许我可以将指向成员的指针转换为vtable偏移量.当我编译以下内容时:
class Foo {
public:
virtual ~Foo() {}
virtual void DoSomething() = 0;
};
void foo(Foo *f, void (Foo::*member)()) {
(f->*member)();
}
Run Code Online (Sandbox Code Playgroud)
在GCC/x86-64上,我得到了这个程序集输出:
Disassembly of section .text:
0000000000000000 <_Z3fooP3FooMS_FvvE>:
0: 40 f6 c6 01 test sil,0x1
4: 48 89 74 24 e8 mov QWORD PTR [rsp-0x18],rsi
9: 48 89 54 24 f0 mov QWORD PTR [rsp-0x10],rdx
e: 74 10 je 20 <_Z3fooP3FooMS_FvvE+0x20>
10: 48 01 d7 add rdi,rdx
13: 48 8b 07 mov rax,QWORD PTR [rdi]
16: 48 8b 74 30 ff mov rsi,QWORD PTR [rax+rsi*1-0x1]
1b: ff e6 jmp rsi
1d: 0f 1f 00 nop DWORD PTR [rax]
20: 48 01 d7 add rdi,rdx
23: ff e6 jmp rsi
Run Code Online (Sandbox Code Playgroud)
我不完全理解这里发生了什么,但如果我可以对此进行逆向工程或使用ABI规范,我可以为每个单独的平台生成如上所述的片段,作为从vtable中获取指针的一种方式.
首先,类类型有一个 vtable。该类型的实例有一个指向 vtable 的指针。这意味着如果某个类型的 vtable 的内容发生更改,则该类型的所有实例都会受到影响。但是特定实例可以更改其 vtable 指针。
没有从实例中检索 vtable 指针的标准方法,因为它依赖于编译器的实现。有关更多详细信息,请参阅此帖子。但是,G++ 和 MSVC++ 似乎按照wikipedia 的描述来布局类对象。类可以有指向多个 vtable 的指针。为简单起见,我将讨论只有一个 vtable 指针的类。
要从 vtable 中获取函数的指针,可以像这样简单地完成:
int* cVtablePtr = (int*)((int*)c)[0];
void* doSomethingPtr = (void*)cVtablePtr[1];
Run Code Online (Sandbox Code Playgroud)
其中 c 是此类定义的类 C 的实例:
class A
{
public:
virtual void A1() { cout << "A->A1" << endl; }
virtual void DoSomething() { cout << "DoSomething" << endl; };
};
class C : public A
{
public:
virtual void A1() { cout << "C->A1" << endl; }
virtual void C1() { cout << "C->C1" << endl; }
};
Run Code Online (Sandbox Code Playgroud)
在这种情况下,类 C 只是一个结构体,它的第一个成员是指向 vtable 的指针。
在 JIT 编译器的情况下,可以通过重新生成代码在 vtable 中缓存查找。
起初,JIT 编译器可能会产生这样的结果:
void* func_ptr = obj_instance[vtable_offest][function_offset];
func_ptr(this, param1, param2)
Run Code Online (Sandbox Code Playgroud)
既然 func_ptr 是已知的,JIT 就可以删除旧代码,并简单地将该函数地址硬编码到编译代码中:
hardcoded_func_ptr(this, param1, param2)
Run Code Online (Sandbox Code Playgroud)
我应该注意的一件事是,虽然您可以覆盖实例 vtable 指针,但并不总是可以覆盖 vtable 的内容。例如,在 Windows 上,vtable 被标记为只读内存,但在 OS X 上它是读/写的。因此,在尝试更改 vtable 内容的 Windows 上,除非您使用VirtualProtect更改页面访问权限,否则将导致访问冲突。
为什么你觉得&DerivedFoo::DoSomething不一样?这不正是您所要求的吗?我的想法是,任何调用DerivedFoo::DoSomething()都会调用相同的函数,并传递不同的 this 指针。vtable 仅区分从派生的不同类型Foo,而不区分实例。