反汇编之C++的虚函数

C++虚函数表解析

先来查看一简单例子



 
  1. #include<iostream>
  2. using namespace std;
  3. class Base{
  4. public:
  5. virtual void f() { cout << "base f()被call"<<endl; }
  6. virtual void g() { cout << "父类虚函数G被call" << endl; }
  7. };
  8. class Derive : public Base{
  9. public:
  10. virtual void f() { cout << "子类虚函数f()被call" << endl; }
  11. };
  12. int main()
  13. {
  14. Derive d; //子类对象
  15. Base *pb;//父类指针
  16. pb = &d;//父类指针指向子类对象
  17. int *q =(int*) pb;//取出类对象地址
  18. void(*f)(Derive*) = (void(*)(Derive*))(*(int*)(*q));//取出f()函数地址
  19. void(*g)(Derive*) = (void(*)(Derive*))(*(int*)(*q+4));//取出g()函数地址
  20. f((Derive*)pb);//调用子对象虚函数表的第一个元素
  21. g((Derive*)pb);//调用子对象虚函数表的第二个元素
  22. system("pause");
  23. return 0;
  24. }

用VS2015调试一番.查看pb保存子对象的地址.

 

 以上pb保存了子类对象地址,地址为0x003bfb64.来看看.这个子类对象的首4个字节(32位机子)地址保存了啥内容(其实是虚函数表的地址)

 .

 

子类对象首4个字节保存了00948b34这个数值.(这个其实是虚函数表的地址).再来看看这个数值保存了啥内容.

 

 发现这个地址开始 0094142e跟00941424应该就是虚函数的地址了.不信.咋们到反汇编查看该地址内容.

 

先查看0x0094142e(函数转为反汇编.其实第一个地址就是跳转).发现没.反汇编已经提示了是Derive::f这个函数

再看看0x00941424.是Base::g这个函数

以上可知.一般编译器实现.把虚函数表的地址用放在子对象首4个字节.虚函数表保存的是虚函数的地址.

  1. void(*f)(Derive*) = (void(*)(Derive*))(*(int*)(*q));//取出f()函数地址
  2. void(*g)(Derive*) = (void(*)(Derive*))(*(int*)(*q+4));//取出g()函数地

 

比如以上2行.q是子对象的地址.(*q)就是虚函数表的地址 (*q)+4 相当于虚函数的第二个元素的地址. *(int*)(*q)表示第一个虚函数的地址.*(int*)(*q+4));表示第二个虚函数表的地址..最后强制转换为类成员函数.用(void(*)(Derive*).因为一般类成员函数在编译后.首形参是一个this指针.这里用Derive*.因为this指针的类型就是类的类型

  1. f((Derive*)pb);//调用子对象虚函数表的第一个元素
  2. g((Derive*)pb);//调用子对象虚函数表的第二个元素

 

 最后调用这2个虚函数表中的虚函数.把pb(子对象的地址)当做参数this指针.然后直接调用.即可使得函数被调用了

转自https://www.cnblogs.com/zengyiwen

<think>我们被要求通过汇编分析C++虚函数表的存储结构,并且要参考前面提供的引用内容。根据引用,虚函数表(vtable)的实现原理可以通过汇编代码来观察。 关键点: 1. 每个包含虚函数的类(或从包含虚函数的类派生)都有一个虚函数表。 2. 每个对象在创建时,会在对象的起始位置(即对象内存布局的开头)存储一个指向虚函数表的指针(通常称为vptr)。 3. 虚函数表本身是一个函数指针数组,其中每个元素指向该类的一个虚函数的实际实现。 4. 当通过基类指针或引用调用虚函数时,会通过对象的vptr找到虚函数表,然后根据函数在表中的偏移量来调用正确的函数。 通过汇编分析步骤: 1. 创建对象:观察对象的内存布局,特别是对象开头是否有一个指针被设置。 2. 调用虚函数:观察调用过程中如何通过对象地址找到虚函数表,如何通过偏移量找到函数地址,并进行调用。 根据引用中的内容: - 引用[2]提到:指向虚函数表的指针被存放在虚类的最开头。 - 引用[5]提到:初始化b的vptr,将虚表地址存入对象首地址。 - 引用[4]提到:对象的前四个字节(在32位系统)存储虚表地址,虚表内容就是虚函数的地址。 下面我们通过一个例子,结合汇编代码逐步分析。 示例C++代码: ```cpp class Animal { public: virtual void speak() { /* ... */ } }; class Dog : public Animal { public: void speak() override { /* ... */ } }; int main() { Animal* animal = new Dog(); animal->speak(); delete animal; return 0; } ``` 我们关注`animal->speak();`这一行对应的汇编。假设我们使用x86-64架构,编译器为GCC。 反汇编可能如下(关键部分): ```asm ; 创建Dog对象,假设使用new mov edi, 8 ; 分配8字节(假设指针大小8字节,Animal只有一个虚函数指针) call operator new(unsigned long) mov rbx, rax ; 保存返回的地址到rbx mov rdi, rbx ; 将地址作为this指针传递给构造函数 call Dog::Dog() ; 调用Dog的构造函数 ; 将animal指针指向新创建的对象 mov QWORD PTR [rbp-8], rbx ; 将对象地址存入animal变量 ; 调用animal->speak(); mov rax, QWORD PTR [rbp-8] ; 将animal指针(对象地址)加载到rax mov rax, QWORD PTR [rax] ; 取对象的前8个字节(即虚函数表指针)到rax mov rdi, QWORD PTR [rbp-8] ; 将this指针(即对象地址)作为第一个参数(隐含参数)传递 call QWORD PTR [rax] ; 调用虚函数表中的第一个函数(即speak) ; 后续省略... ``` 解释: 1. 在创建Dog对象时,首先分配内存,然后调用Dog的构造函数。在构造函数内部,会初始化虚函数表指针,使其指向Dog类的虚函数表。 2. 当调用`animal->speak()`时: - 首先从`animal`变量(即对象指针)所指向的内存中取出前8个字节(这就是虚函数表指针)。 - 然后准备调用:将对象指针作为this指针传递给函数(在x86-64上,通常第一个参数是rdi)。 - 接着从虚函数表中取出第一个函数指针(因为speak是第一个虚函数,所以它在虚函数表的第一项)并调用。 虚函数表的存储结构: - 虚函数表是一个静态数组,在编译时由编译器生成,存放在程序的只读数据段(如.rodata)。 - 每个类(而不是每个对象)都有自己的虚函数表。同一个类的所有对象共享同一个虚函数表。 - 虚函数表中的每一项是一个函数指针,指向该类的虚函数的实现。 - 派生类的虚函数表会继承基类的虚函数表,如果派生类覆盖了某个虚函数,则在相应位置替换为派生类的函数地址;如果派生类新增了虚函数,则追加在虚函数表末尾。 通过汇编如何验证虚函数表的存储结构? 1. 查看对象内存:在调试器中,打印对象地址处的内存,应该可以看到一个指针(在64位系统下是8字节)。这个指针指向虚函数表。 2. 查看虚函数表内存:然后打印该指针(虚函数表地址)处的内存,应该是一系列函数指针。每个函数指针对应一个虚函数的入口地址。 例如,假设对象地址为0x123400,那么: - 地址0x123400处存储的值(8字节)就是虚函数表地址,假设为0x567800。 - 然后,地址0x567800处开始的8字节就是第一个虚函数的地址。 - 地址0x567808处的8字节就是第二个虚函数的地址(如果有的话)。 引用[4]中描述:在dog对象的内存中,前四个字节(32位)或八个字节(64位)就是虚表地址,虚表的内容就是虚函数的地址。 总结方法: 1. 通过反汇编观察对象的创建过程,特别是构造函数中对虚函数表指针的初始化。 2. 通过反汇编观察虚函数调用时的寻址过程:先取对象地址,再取虚表指针,再计算偏移量,最后调用。 3. 使用调试器查看对象的内存布局,验证虚函数表指针的位置和虚函数表的内容。 注意:不同编译器、不同平台(x86/ARM)、不同调用约定可能会有差异,但基本原理相同。 相关问题: 1. 如何通过调试器查看C++对象的虚函数表? 2. 虚函数表在多重继承下的结构是怎样的? 3. 虚析构函数在虚函数表中是如何存储的? 4. 虚函数表在程序内存的哪个段? </think>### 通过汇编分析C++虚函数表存储结构的方法 #### 一、核心分析原理 虚函数表(vtable)在汇编层的本质是**连续的函数指针数组**,其存储结构遵循以下原则: 1. **虚表指针位置**:每个对象实例的首地址存储指向虚函数表的指针(vptr)[[2]](#引用2)[[5]](#引用5) 2. **虚表内存布局**:虚函数表本身是编译器生成的静态数据结构,包含按声明顺序排列的虚函数地址[[3]](#引用3)[[4]](#引用4) 3. **动态绑定机制**:虚函数调用通过`vptr → offset → call`三级寻址实现运行时绑定[[1]](#引用1) #### 二、具体分析步骤(x86-64架构示例) ##### 步骤1:定位虚表指针 ```asm ; 对象创建时的虚表指针初始化 lea rax, [vtable_Dog] ; 加载虚表地址到rax [^5] mov QWORD PTR [rbp-0x18], rax ; 存入对象首地址(vptr位置)[^5] ``` - 验证方法:通过调试器查看对象内存首8字节(64位系统)的值即为vptr地址 ##### 步骤2:解析虚表内容 ```asm ; 虚函数调用过程 mov rax, QWORD PTR [obj] ; 获取对象首地址 → vptr [^4] mov rax, QWORD PTR [rax] ; 获取虚表首地址 mov rdx, QWORD PTR [rax+8] ; 获取第二项虚函数地址(偏移8字节) call rdx ; 调用虚函数 ``` - 虚表存储结构示意: ``` 虚表地址+0x0: &Animal::vfunc1 虚表地址+0x8: &Dog::vfunc2 ; 派生类覆盖的函数 虚表地址+0x10: &Dog::vfunc3 ; 派生类新增虚函数 ``` ##### 步骤3:验证多重继承场景 ```asm ; 多重继承的虚表跳转 mov rax, QWORD PTR [obj+16] ; 访问第二个基类的vptr call QWORD PTR [rax+24] ; 调用偏移24字节的虚函数 ``` - 特征:每个基类对应独立的虚表指针,派生类对象包含多个vptr[[4]](#引用4) #### 三、关键验证技术 1. **内存查看器验证**: - 使用GDB的`x/xg obj_addr`查看对象首地址值 → vptr - 用`x/8g vptr_addr`查看虚表内容(每8字节一个函数指针) 2. **编译器布局检查**: ```bash g++ -fdump-class-hierarchy source.cpp # 输出类内存布局 objdump -d -j .rodata a.out # 查看只读段的虚表数据 ``` 3. **运行时调试技巧**: ```gdb (gdb) p *obj # 查看对象 (gdb) p /a *(void**)obj # 获取vptr (gdb) p /a ((void***)obj)[0][0] # 首个虚函数地址 (gdb) disas /m main # 反汇编关联源码 ``` #### 四、存储结构核心特征 1. **层级关系**: ```mermaid graph TD A[对象实例] --> B[vptr] B --> C[虚函数表] C --> D[&Base::func1] C --> E[&Derived::func2] C --> F[...] ``` 2. **跨平台差异**: | 架构 | vptr偏移 | 虚表项大小 | 调用约定 | |------------|----------|------------|---------------| | x86-64 | +0 | 8字节 | rdi传this指针[[2]](#引用2) | | ARM64 | +0 | 8字节 | x0传this指针 | | x86 | +0 | 4字节 | ecx传this指针 | 3. **动态特性证据**: - 同一虚函数在不同派生类中对应不同地址 - RTTI(类型信息)通常存储在虚表前的负偏移位置[[4]](#引用4) #### 五、典型误区的汇编验证 1. **虚表非"表"**: 汇编显示虚表是连续内存块,而非抽象数据结构 ```asm section .rodata vtable_Dog: dq 0 ; 类型信息偏移 dq _ZN3Dog5barkEv ; 函数1地址 dq _ZN3Dog5jumpEv ; 函数2地址 ``` 2. **非单继承优化**: 多重继承时,汇编显示派生类包含多个独立虚表指针: ```asm mov rax, [obj] ; 第一个基类vptr mov rbx, [obj+32] ; 第二个基类vptr ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值