从汇编代码分析c++虚函数表真正原理

本文通过具体的C++代码和汇编指令详细分析了虚函数表的工作原理,解释了多态性的实现机制,并通过实际例子展示了虚函数表如何在继承中被重写。

记得当年刚入行的时候经常看一些别人讲C++虚函数表,当时也不理解,别人怎么说就怎么信了。后来知道多了,发现以前的帖子很有问题,不知道你是否看过这样的图片。现在我用汇编分析真正虚函数是如何实现动态,如何生存虚函数表。如果你觉得有什么不对的地方欢迎交流。这个图片问题很大,我只是指出些这样帖子的人一些错误。

下面来看我写的一对父子类

class MyPreant
{
public:
	MyPreant();
	~MyPreant();

	virtual void DisShow();
	virtual void DisShow2();
private:
	int m_base;
};

class MyClass:public MyPreant
{
public:
	MyClass();
	MyClass(int a);
	~MyClass();

	virtual void DisShow();
	virtual void DisShow2();
private:
	int m_a;
};

下面我们来看调用的过程

MyPreant *p2 = new MyClass;
p2->DisShow();

p2->DisShow2();

	MyPreant *p2 = new MyClass;
00E70FF7 83 C0 01             add         eax,1  
00E70FFA 50                   push        eax  
00E70FFB 68 F0 37 E8 00       push        offset string "c:\\users"... (0E837F0h)  
00E71000 6A 0C                push        0Ch  
00E71002 E8 0F 08 FF FF       call        operator new (0E61816h) //调用new函数 返回地址生成在eax寄存器
00E71007 83 C4 0C             add         esp,0Ch  
00E7100A 89 85 08 FF FF FF    mov         dword ptr [ebp-0F8h],eax  
00E71010 C7 45 FC 00 00 00 00 mov         dword ptr [ebp-4],0  
00E71017 83 BD 08 FF FF FF 00 cmp         dword ptr [ebp-0F8h],0  
00E7101E 74 13                je          CMyRemoteInjectionDlg::OnBnClickedButton1+83h (0E71033h)  
00E71020 8B 8D 08 FF FF FF    mov         ecx,dword ptr [ebp-0F8h]  
00E71026 E8 3A 04 FF FF       call        MyClass::MyClass (0E61465h) //调用子类的构造函数 

这是一个父类的指针指向子类的对象,用来研究多态的真正流程下面我们来看具体代码


这个是new出来的内存地址之后我们进入子类构造函数查看源码

00E71D20 55                   push        ebp  
00E71D21 8B EC                mov         ebp,esp  
00E71D23 81 EC CC 00 00 00    sub         esp,0CCh  
00E71D29 53                   push        ebx  
00E71D2A 56                   push        esi  
00E71D2B 57                   push        edi  
00E71D2C 51                   push        ecx  
00E71D2D 8D BD 34 FF FF FF    lea         edi,[ebp-0CCh]  
00E71D33 B9 33 00 00 00       mov         ecx,33h  
00E71D38 B8 CC CC CC CC       mov         eax,0CCCCCCCCh  
00E71D3D F3 AB                rep stos    dword ptr es:[edi]  
00E71D3F 59                   pop         ecx  
00E71D40 89 4D F8             mov         dword ptr [this],ecx  //赋值给this指针
00E71D43 8B 4D F8             mov         ecx,dword ptr [this]  
00E71D46 E8 E7 FC FE FF       call        MyPreant::MyPreant (0E61A32h) //调用父类的构造函数
00E71D4B 8B 45 F8             mov         eax,dword ptr [this]  
00E71D4E C7 00 DC 35 E8 00    mov         dword ptr [eax],offset MyClass::`vftable' (0E835DCh)  //把虚函数表赋值给地址空间的初始位置
之后我们进入父类的构造函数里
00E71D90 55                   push        ebp  
00E71D91 8B EC                mov         ebp,esp  
00E71D93 81 EC CC 00 00 00    sub         esp,0CCh  
00E71D99 53                   push        ebx  
00E71D9A 56                   push        esi  
00E71D9B 57                   push        edi  
00E71D9C 51                   push        ecx  
00E71D9D 8D BD 34 FF FF FF    lea         edi,[ebp-0CCh]  
00E71DA3 B9 33 00 00 00       mov         ecx,33h  
00E71DA8 B8 CC CC CC CC       mov         eax,0CCCCCCCCh  
00E71DAD F3 AB                rep stos    dword ptr es:[edi]  
00E71DAF 59                   pop         ecx  
00E71DB0 89 4D F8             mov         dword ptr [this],ecx  
00E71DB3 8B 45 F8             mov         eax,dword ptr [this]  
00E71DB6 C7 00 B4 35 E8 00    mov         dword ptr [eax],offset MyPreant::`vftable' (0E835B4h)  //把父类的虚函数表放到地址空间位置

下面来看内存空间 父类的虚函数表位置


之后父类做构造函数的初始化,返回到子类的构造函数。

00E71D4E C7 00 DC 35 E8 00    mov         dword ptr [eax],offset MyClass::`vftable' (0E835DCh)  

这一句是把子类的虚函数表赋值给当前地址,也就是覆盖了父类的虚函数表地址。这就是实现多态的原因。


这块地址的虚函数表已经变为0x00e835dc,也就是子函数的续表。那么我们来看看子函数续表是否包含父类续表地址。也就是最开始那个图是否正确


明显只有2个函数地址说明了子类函数地址只包含自己的虚函数地址。下面我们看看父类虚函数表0x00E835B4


父类的虚函数表也是只有2个函数地址。

那么我们来看看具体是函数地址是否对应看代码

00E614FB E9 70 11 01 00       jmp         MyClass::DisShow2 (0E72670h)  
00E614FB 正好就是我们写的子类的函数地址MyClass::DisShow2
通过查看汇编码和内存我们清楚的知道c++虚函数动态绑定的原理,和续表保存的东西。以后学东西不能看别人怎么写你就怎么信,要学会自己看汇编。知道真正的原理才能了解更深。
<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
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值