如何从C ++ vtable中获取指针?

时间:2011-02-24 03:12:42

标签: c++ cross-platform

假设您有一个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()函数的地址,因此我可以生成直接跳转到它的代码而不是执行表查找和间接分支。

我的问题是:

  1. 是否有任何标准的C ++方法(我几乎可以肯定答案是否定的,但是为了完整起见,我想问一下)。

  2. 有没有任何与远程编译器无关的方法,比如有人实现了提供访问vtable的API的库?

  3. 如果他们能够工作,我会完全打开黑客。例如,如果我创建了自己的派生类并且可以确定其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规范,我可以为每个单独的平台生成如上所述的片段,作为一种获取指针的方法虚表。

5 个答案:

答案 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查找的方法。