C++反汇编 剖析虚函数表的实现原理(下)
上节回顾:
C++多态的实现,以及虚函数表的引入,反汇编剖析
基础反汇编知识:
反汇编分析C/C++常见语句的底层实现(入门)
回顾
我们上一节讲到,定义为virtual的函数就称为虚函数,它为继承自它的子类提供了不同的函数调用的方法。
class Animals { public: void speak() { cout << "Animal::speak()\n"; } void run() { cout << "Animal::ran()\n"; } }; ....省略动物的继承关系 //一个统一的动物行为 void action(Animals* ani) { ani->speak(); ani->run(); } int main() { action(new Dog); action(new Cat); action(new Pig); return 0; }
我们把这个过程称之为实现多态的过程。我们利用父类指针指向子类对象,这就是实现多态的本质。
为什么父类指针可以动态的决定他要调用哪个函数? 明明都是一个Animals的指针,为什么在调用函数后却能调用不同的子类的函数?
答案:它使用了一个叫做虚函数表的东西。
当我们把父类实现为virtual的时候,它实际上就产生了一个虚表,
还记得我们在上一节讲到的加不加virtual的区别吗?
-
如果我们
不加virtual
,则他就不是虚函数: 它产生的汇编是这样的:
已经给你固定死了
,尽管你使用的是多态的方法,即父类指针调用子类对象,但是因为你没有声明为virtual虚函数,所以在call函数地址的时候,已经给你写死了,就是直接调用本对象的函数地址
。 -
如果你
加virtual
,他就是虚函数: 它产生的汇编:(完整汇编代码)
在下面分析的时候也会用到此图:
它为什么加了这么多行东西?
实际上,当你定义了一个virtual虚函数,并且使用父类指针调用子类对象的时候,指针会自动识别你的对象的具体类型
。是一个Animals指针,但是是一个Dog对象,所以他会生成一个虚表,根据虚表来查找到你具体要调用的函数地址
。
反汇编剖析虚函数表
虚函数表
:只要包含虚函数的类就会有一个虚函数表,当这个类是基类时,它的派生类也会有相应的虚函数表。当一个类有多个对象的时候,这些对象共享一个虚函数表。
我们分别赋予父类和子类两个变量,m_age和m_life,我们可以查看他们所处的类所占的字节大小:
class Animals{ public: int m_life = 10; virtual void speak(){ ... } virtual void run(){ .... } }; class Dog :public Animals{ public: int m_age = 20; void speak(){ ... } void run(){ ... } }; int main(){ Animals* dog = new Dog; dog->speak(); dog->run(); cout<<sizeof(Dog)<<endl; //结果为12 return 0; }
我们的Dog类继承了父类,它自己有一个m_age的4字节int,父类有一个m_life也占用四个字节,他不应该一共只占用8个字节吗? 怎么多出来4个字节,其实,多出来的4个字节,存放此类的虚函数表的地址
。
8个字节存储两个int,它又产生了4个字节的地址,我们称此地址为虚表的地址,并且此地址位于总共12个字节的最前面4个字节。
这就是每个指针变量(dog)的内存结构图:
我们来一步步解析汇编指令: (完整汇编代码,请看上面,并且我去除了一些不重要的汇编指令)
关键四句:
//d->speak(); 实现汇编代码: mov eax,dword ptr [dog] //找到指针变量d的地址所处的内存空间,取出四个字节给到eax存储, eax存储了指针d的地址 mov edx,dword ptr [eax] //找到eax所存储的地址的内存空间,取出4个字节给到edx存储,edx存储了首4个字节,即虚表的地址 mov eax,dword ptr [edx] //找到虚表的地址所处的内存空间,取出4个字节给到eax存储,eax存储了虚函数表的前4个字节的内存,即Dog::speak 的函数地址 call eax // 跳转到函数
这是我们的d->speak
的实现原理:
mov eax,dword ptr [dog]
首先:找到指针变量dog(Animals*类型)的地址所在的内存空间,取出四个字节给到eax寄存器,eax寄存器就存储了Dog对象的地址。 此对象包括12个字节,前四个字节为虚表的地址
eax存放的内容:(就是图示的整个结构,即Dog类的对象的内存空间的内容)
mov edx,dword ptr [eax]
在eax所处的内存空间中取出四个字节给到edx寄存器,此处,eax就得到了图示四个字节存储空间里的内容,内容就是虚表的地址,所以edx就获取了此地址。
edx存储的内容:虚表的地址,edx: 0x00B89B64h
mov eax,dword ptr [edx]
在edx所处的内存空间中取出4个字节给到eax寄存器,edx此时已经存储了虚表的地址了,虚表里面前4个字节是第一个函数的地址,后4个字节是第二个函数地址(一共8个,因为有两个虚函数),所以取出的是前四个字节的内容,即第一个虚函数的地址,即eax里存储第一个虚函数的地址。
eax存储的内容:此时就是第一个虚函数speak的地址:eax:0x00B814E7
call eax
eax经过上面的操作,已经存储了第一个虚函数的地址,call eax,代表call到寄存器所存储的内容,即call此地址,跳转到此虚函数。
我们来看一下run函数的执行过程:
dog->run(); mov eax,dword ptr [dog] mov edx,dword ptr [eax] mov eax,dword ptr [edx+4] //此时第二个函数的地址 call eax
也是,首先找到对象的地址,再找到虚函数表的地址,再找到第二个虚函数的地址,即 [edx+4] 就存储了第二个虚函数的地址,把此地址给到eax存储,再由call跳转到此函数。
验证虚函数地址的正确性
如何证明虚表里存储的就是虚函数的地址??
你怎么证明虚表的所对应的存储地址的那8个字节就是两个虚函数的地址呢?
解析:
根据dog的地址找到其内存
dog对象的内存空间
:
它一共占用12个字节(一个虚表4字节,两个int8字节
),并且前四个字节就是虚表的地址。
假设这就是虚表的地址:所以我们得到了虚表的地址:0x00169b6c (小端存储)
根据此虚表地址,我们进入其内存空间:
虚函数表的地址所在的内存空间对应8个字节, 分别表示表示两个虚函数的地址。
前四个:第一个虚函数的地址: 0x001615fa
后四个:第二个虚函数的地址: 0x001615cd
我们已经得到了我们认为的虚函数的准确地址,现在我们进入反汇编来看看call跳转的地址是不是就是我们找到的这两个地址。
我们进入反汇编:
经过上述分析可以得知:
第一个虚函数:speak:
.... .... 0x00166D7D mov eax,dword ptr [edx] //eax存储第一个虚函数地址 0x00166D7F call eax //进入此函数
经过上述分析得到,eax 存储第一个虚函数的地址,此时我们监视eax:
可以得知:eax的值就是 0x001615fa
,即这就是第一个虚函数的地址
继续进入run的反汇编:
eax的值就是0x001615cd,即这就是第二个虚函数的地址。
call地址后,我们就能进入 0x001615cd
,即第二个虚函数的地址:Dog的run函数。
总结:
- 至此,我们在反汇编中看到的eax存储的地址和在dog对象在内存中存储的两个地址是
一摸一样的
,所以,我们可以得知,dog对象的前四个对象的地址就是虚表的地址,同时在虚表的内容中存储的八个字节就是两个虚函数的地址(dword ptr)。
本文来自博客园,作者:hugeYlh,转载请注明原文链接:https://www.cnblogs.com/helloylh/p/17209700.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)