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

Posted

技术标签:

【中文标题】如何从 C++ vtable 中获取指针?【英文标题】:How to obtain a pointer out of a C++ vtable? 【发布时间】:2011-02-24 03:12:42 【问题描述】:

假设你有一个 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 规范,我可以为每个单独的平台生成一个类似上述的片段,作为从虚表。

【问题讨论】:

这不是一个真正的答案,但您应该阅读codesourcery.com/cxx-abi。忽略提及安腾; GCC 普遍使用该 ABI(带有适当的特定于处理器的调整),现在几乎所有其他用于 *nix 的编译器也是如此。 MSVC 当然是不同的,但可能没有那么不同——只有这么多方法可以做到。 您应该研究如何使用调试符号。如果符号可用,这种事情就很容易了。我回答了这个问题***.com/questions/5740155/access-v-table-at-run-time/…,并提供了有关如何访问 vtable 指针、它们的布局、它们的来源等的参考资料……查看我对***关于 C++ RTT 信息的页面的参考,这可能会对你有所帮助en.wikipedia.org/wiki/Run-time_type_information跨度> 【参考方案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 更改页面访问权限,否则将导致访问冲突。

【讨论】:

我想补充一点来观察MSVC如何设置vtable和vtable指针,可以使用编译器选项/d1reportSingleClassLayoutX,其中X是你想要的类的名称查看。此开关没有正式记录,但在 MSDN 博客和 SO 上的各个位置都提到了它。 [它在/d1reportAllClassLayout 中也有对应物,但我不相信 MSDN 上的任何地方都提到过。]【参考方案2】:

我可以想到另外两个解决方案,而不是深入研究 C++ 对象模型。

第一个(也是显而易见的):通用编程(又名模板)

不要使用基类,重构依赖于基类的方法,以便它们将“策略”作为模板参数。这将完全消除虚拟呼叫。

第二个不太明显的是反转依赖关系。

不是在算法中注入策略,而是在策略中注入算法。这样,您将在开始时进行一次虚拟呼叫,然后它将“正常”进行。模板可以在这里再次提供帮助。

【讨论】:

【参考方案3】:

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

【讨论】:

即使我有 &DerivedFoo::DoSomething,它也没有给我一个可以跳转到的地址,它是 vtable 的偏移量。我想生成显示“jmp ”的汇编语言,其中 是函数的实际地址。问题是如何获取 . 我想这就是我没有仔细阅读的结果。无论如何,您的帖子被标记为跨平台,但由于您似乎不太可能找到真正的跨平台解决方案,您见过这个 GCC 扩展吗? gcc.gnu.org/onlinedocs/gcc/…【参考方案4】:

这不是一个直接的答案,也不一定是最新的,但它确实包含了很多你在尝试做这样的事情时需要注意的细节和警告:http://www.codeproject.com/KB/cpp/FastDelegate.aspx

不,没有标准的 C++ 方法可以做到这一点。以上内容与您要求的内容相似,但不同。

【讨论】:

一篇很棒的文章!如果没有人有更明确的答案(比如指向这样做的图书馆的链接),我肯定会接受这个答案。【参考方案5】:

如果你调用derived-&gt;DoSomething(),而DoSomething()在派生类中不是虚拟的,编译器应该已经生成了直接调用。

如果您调用base-&gt;DoSomething(),编译器必须以一种或另一种方式检查要调用哪个版本的DoSomething(),并且vtable 与任何方法一样有效。如果您可以保证它始终是基类的实例,那么您一开始就不需要将方法设为虚拟。

在某些情况下,在调用基类中的一组虚拟的非虚拟派生方法之前执行 static_cast 可能是有意义的,但由于 vtable 查找很常见,占到并且相对便宜,所以这绝对属于过早优化的范畴。

模板是另一种标准的 C++ 重用代码而不引起 vtable 查找的方法。

【讨论】:

JIT 编译器可以在运行时接收对象的实例,并生成专门用于调用该对象的代码。当你不知道我在做什么时,我不知道你怎么能告诉我我过早地优化了。 我有一个解析器能够以 >500MB/s 的速度解析二进制序列化格式。解析器通过一组回调将解析后的数据传递给客户端应用程序。为每个值(每 5-10 个字节)调用这些回调之一。在 C++ 中为一组回调提供具体实现的一种便捷方法是实现一个虚拟基类。 JITted 版本可以解析 100 兆字节。如果我使用普通的 C++ 虚函数调度,每 5-10 个字节我必须在我的关键路径中追逐两个额外的指针,并且我必须使用一个可能被错误预测的间接分支。 @Josh,听起来你属于我提到的“选择情况”,但仍然可能有比 JIT 更惯用的方式。有多少个不同的客户端类,每个类有多少实例,它们的更改频率如何? @Josh:在你的位置上,我会测量。比较通过基类处理所需的时间与直接使用派生类之一处理所需的时间。这会给你虚拟指针开销。 @Matthieu:我在测试中测量了 25% 的虚拟指针开销。当我调用非虚拟函数时,我得到了 600MB/s。当我调用虚函数时,我得到了 485MB/s。而且这只是一组回调,因此分支是完全可预测的(在我的真实库中,回调可以相互委托,因此间接分支的目标会改变并使分支更难预测)。

以上是关于如何从 C++ vtable 中获取指针?的主要内容,如果未能解决你的问题,请参考以下文章

c++ 怎样从任意类获取CDocument类指针

C++ 字符串 - 如何避免获取无效指针?

C++ 从函数参数中获取指针 指针传递

从vtable到设计模式——我的C++面向对象学习心得

虚表(Vtables)

从 C++ 的成员函数中获取指向成员函数的指针