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

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

我的问题是:

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

  2. 有没有任何独立于远程编译器的方法,比如有人实现了提供访问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中获取指针的一种方式.

Eva*_*van 5

首先,类类型有一个 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更改页面访问权限,否则将导致访问冲突。


Sea*_*ean 2

为什么你觉得&DerivedFoo::DoSomething不一样?这不正是您所要求的吗?我的想法是,任何调用DerivedFoo::DoSomething()都会调用相同的函数,并传递不同的 this 指针。vtable 仅区分从派生的不同类型Foo,而不区分实例。