假设您有一个C ++类:
class Foo {
public:
virtual ~Foo() {}
virtual DoSomething() = 0;
};
C ++编译器将调用转换为vtable查找:
Foo* foo;
// Translated by C++ to:
// foo->vtable->DoSomething(foo);
foo->DoSomething();
假设我正在编写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)();
}
在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
我不完全理解这里发生了什么,但是如果我可以对此进行逆向工程或使用ABI规范,我可以为每个单独的平台生成如上所述的片段,作为一种获取指针的方法虚表。
答案 0 :(得分:3)
我可以想到另外两个解决方案,而不是挖掘C ++对象模型。
第一个(也是显而易见的):通用编程(又名模板)
不要使用基类,重构依赖于基类的方法,以便它们将“策略”作为模板参数。这将完全消除虚拟呼叫。
第二个不太明显的是反转依赖关系。
不是在算法中注入策略,而是在策略中注入算法。这样,您将在开始时进行单个虚拟呼叫,然后它将“正常”进行。模板可以在这里再次提供帮助。
答案 1 :(得分:2)
为什么你认为&DerivedFoo::DoSomething
有所不同?这不是你要求的吗?我想到它的方式,对DerivedFoo::DoSomething()
的任何调用都将调用相同的函数,传递一个不同的this指针。 vtable仅区分从Foo
派生的不同类型,而非实例。
答案 2 :(得分:2)
这不是一个直接的答案,也不一定是最新的,但它确实涉及到在尝试做这样的事情时需要注意的许多细节和警告:http://www.codeproject.com/KB/cpp/FastDelegate.aspx < / p>
没有标准的C ++方法可以做到这一点。以上内容与您要求的内容类似,但不一样。
答案 3 :(得分:1)
首先,类类型有一个vtable。该类型的实例具有指向vtable的指针。 这意味着如果vtable的内容更改了该类型的所有实例的类型 影响。但是特定的实例可以改变它们的vtable指针。
没有标准方法从实例检索vtable指针,因为它取决于编译器的实现。有关详细信息,请参阅此post。 但是,G ++和MSVC ++似乎按wikipedia所述布局类对象。 类可以有指向多个vtable的指针。为了简单起见,我将谈谈 只有一个vtable指针的类。
要从vtable中获取函数的指针,可以像这样简单地完成:
int* cVtablePtr = (int*)((int*)c)[0];
void* doSomethingPtr = (void*)cVtablePtr[1];
其中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; }
};
C类只是一个结构体,其第一个成员是在这种情况下指向vtable的指针。
对于JIT编译器,可以缓存 通过重新生成代码在vtable中查找。
首先,JIT编译器可能会生成:
void* func_ptr = obj_instance[vtable_offest][function_offset];
func_ptr(this, param1, param2)
现在知道了func_ptr,JIT可以简单地杀掉旧代码 函数地址到编译代码中的硬代码:
hardcoded_func_ptr(this, param1, param2)
我应该注意的一件事是,当你可以覆盖实例vtable指针时,并不总是可以覆盖vtable的内容。例如,在Windows上,vtable被标记为只读存储器,但在OS X上,它是可读/写的。因此,在尝试更改vtable内容的Windows上,除非您使用VirtualProtect更改页面访问权限,否则将导致访问冲突。
答案 4 :(得分:-2)
如果您调用derived->DoSomething()
,DoSomething()
在派生类中不是虚拟的,编译器应该已经生成一个直接调用。
如果你调用base->DoSomething()
,编译器必须以某种方式检查要调用的DoSomething()
版本,而vtable与任何版本一样有效。如果您可以保证它始终是基类的实例,则您不需要首先将该方法设为虚拟。
在某些情况下,在调用一组在基类中是虚拟的非虚拟派生方法之前执行static_cast
可能是有意义的,但由于vtable查找是常见的,占用且相对便宜,这绝对属于过早优化的范畴。
模板是另一种标准C ++重用代码而不会导致vtable查找的方法。