ImageVerifierCode 换一换
格式:DOCX , 页数:12 ,大小:150.09KB ,
资源ID:18182961      下载积分:3 金币
快捷下载
登录下载
邮箱/手机:
温馨提示:
快捷下载时,用户名和密码都是您填写的邮箱或者手机号,方便查询和重复下载(系统自动生成)。 如填写123,账号就是123,密码也是123。
特别说明:
请自助下载,系统不会自动发送文件的哦; 如果您已付费,想二次下载,请登录后访问:我的下载记录
支付方式: 支付宝    微信支付   
验证码:   换一换

加入VIP,免费下载
 

温馨提示:由于个人手机设置不同,如果发现不能下载,请复制以下地址【https://www.bdocx.com/down/18182961.html】到电脑端继续下载(重复下载不扣费)。

已注册用户请登录:
账号:
密码:
验证码:   换一换
  忘记密码?
三方登录: 微信登录   QQ登录  

下载须知

1: 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。
2: 试题试卷类文档,如果标题没有明确说明有答案则都视为没有答案,请知晓。
3: 文件的所有权益归上传用户所有。
4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
5. 本站仅提供交流平台,并不能对任何下载内容负责。
6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

版权提示 | 免责声明

本文(C++虚函数调用的反汇编解析.docx)为本站会员(b****2)主动上传,冰豆网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对上载内容本身不做任何修改或编辑。 若此文所含内容侵犯了您的版权或隐私,请立即通知冰豆网(发送邮件至service@bdocx.com或直接QQ联系客服),我们立即给予删除!

C++虚函数调用的反汇编解析.docx

1、C+虚函数调用的反汇编解析C+虚函数调用的反汇编解析虚函数的调用如何能实现其“虚”?作为C+多态的表现手段,估计很多人对其实现机制感兴趣。大约一般的教科书就说到这个C+强大机制的时候,就是教大家怎么用,何时用,而不会去探究一下这个虚函数的真正实现细节。(当然,因为不同的编译器厂家,可能对虚函数有自己的实现,呵呵,这就算是虚函数对于编译器的“多态”了:)。 作为编译型语言,C+编译的最后结果就是一堆汇编指令了(这里不同于.NET的CLR)。今天,我就来揭开它的神秘面纱,从汇编的层面来看看虚函数到底怎么实现的。让大家对虚函数的实现不仅知其然,更知其所以然。(本文程序环境为:PC + Windows

2、 XP Pro + Visual C+6.0,文中所得出来的结果和反映的编译器策略也只针对VC6.0的编译器)先看一段简单代码:Line01: #include Line02:Line03: class Base Line04: public:Line05: void _stdcall Output() Line06: printf(Class Basen);Line07: Line08: ;Line09:Line10: class Derive : public Base Line11: public:Line12: void _stdcall Output() Line13: printf

3、(Class Deriven);Line14: Line15: ;Line16:Line17: void Test(Base *p) Line18: p-Output();Line19: Line20:Line21: int _cdecl main(int argc, char* argv) Line22: Derive obj;Line23: Test(&obj);Line24: return 0;Line25: 程序的运行结果将是:Class Base那么将Base类的Output函数声明(Line05)更改为:virtual void _stdcall Output() 那么,很明显地,

4、程序的运行结果将是:Class DeriveTest函数这回算是认清楚了这个指针是一个指向Derive类对象的指针,并且正确的调用了其Output函数。编译器如何做到这一切的呢?我们来看看没有“virtual”关键字和有“virtual”关键字,其最终的汇编代码区别在那里。(在讲解下面的汇编代码前,让我们对汇编来一个简单扫描。当然,如果你对汇编已经很熟练,那么goto到括号外面吧_。先说说上面的Output函数被声明为_stdcall的调用方式:它表示函数调用时,参数从右到左进行压栈,函数调用完后由被调用者恢复堆栈指针esp。其它的调用方式在文中描述。所谓的C+的this指针:也就是一个对象的

5、初始地址。在函数执行时,它的参数以及函数内的变量将拥有如下所示的堆栈结构:(图1)如上图1所示,我们的参数和局部变量在汇编中都将以ebp加或者减多少来表示。你可能会有疑问了:有时候我的参数或者局部变量可能是一个很大的结构体或者只是一个char,为什么这里ebp加减的都是4的倍数呢?恩,是这样的,对于32位机器来说,采用4个字节,也就是每次传输32位,能够取得最佳的总线效率。如果你的参数或者局部变量比4个字节大,就会被拆成每次传4个字节;如果比4个字节小,那还是每次传4个字节。再简单解释一下下面用到的汇编指令,这些指令都是见名知意的哦:mov destination,source将source的

6、值赋给destination。注意,下面经常用到了“xxx”这样的形式,“xxx”对应某个寄存器加减某个数,“xxx”表示是取“xxx”的值对应的内存单元的内容。好比“xxx”是一把钥匙,去打开一个抽屉,然后将抽屉里的东西取出来给别人,或者是把别人给的东西放到这个抽屉里;lea destination,source将source的值赋给destination。注意,这个指令就是把source给destination,而不会去把source对应的内存单元的内容赋给destination。好比是它就把钥匙给别人了;在调试时如果想查看反汇编的话,你应该点击图2下排最右的按钮。(图2)其它指令我估计你

7、从它的名字都能知道它是干什么的了,如果想知道其具体意思,这个应该参考汇编手册。:)一. 没有virtual关键字时:(1) main函数的反汇编内容:Line22: Derive obj;Line23: Test(&obj);/如果你把断点设置在22行,开始调试的时候VC会告诉你这是一个无效行,而把断/点自动移到下一行(Line23),这是因为代码中没有为Derive以及其基类定义构造函/数,而且编译器也没有为它生成一个默认的构造函数的缘故,此行C+代码不会生成/任何可实际调用的汇编指令;004010D8 lea eax,ebp-4/将对象obj的地址放入eax寄存器中;004010DB pu

8、sh eax/将参数入栈;004010DC call ILT+5(Test) (0040100a)/调用Test函数;/这里ILT+5就是跳转到Test函数的的jmp指令的地址,一个模块中所有的/函数调用都会是象这样ILT+5*n,n表示这个模块中的第n个函数,而ILT的意思/是Import Lookup Table,程序调用函数的时候就是通过这个表来跳转到相应函数而执/行代码的。004010E1 add esp,4/调整堆栈指针,刚才调用了Test函数,调用方式_cdecl, 由调用者来恢复堆栈指针;(2) Test函数的反汇编内容:Line18: p-Output();00401048 m

9、ov eax,dword ptr ebp+8/这里的ebp+8其实就是Test函数最左边的参数,就是上面main函数中压栈的eax;/将参数的值(也就是上面的main函数中的obj对象的地址)放入eax寄存器中。/注意:对于C+类的成员函数,默认的调用方式为“_thiscall”,这不是一个由程/序员指定的关键字,它所表示的的函数调用,参数压栈从右向左,而且使用ecx寄存/器来保存this指针。这里我们的Output函数的调用方式为“_stdcall”,ecx寄存器/并不被使用来保存this指针,所以得有额外的指令将this指针压栈,如下句:0040104B push eax/将eax入栈,也

10、就是下面调用Output函数需要的this指针了;0040104C call ILT+0(Base:Output) (00401005)/调用类的成员函数,没有任何悬念,老老实实地调用Base类的Output函数;二. 有virtual关键字时:(1) main函数的反汇编内容:Line22: Derive obj;/在有virtual关键字的时候,把断点设置在22行,调试时就会停在此处了。我们没有/为Derive类或者它的基类声明构造函数,这说明编译器自动为类生成了一个构造函/数,下面我们就可以看看编译器自动生成的这个构造函数干了什么;00401088 lea ecx,ebp-4/将对象ob

11、j的地址放入ecx寄存器中,为什么呢?上面说了哦0040108B call ILT+25(Derive:Derive) (0040101e)/编译器帮忙生成了一个构造函数,它在这里干了什么呢?等会再说吧,作个记号先:/_1;上面要把obj的地址放入ecx中就是为这个函数调用做准备的;Line23: Test(&obj);/这个调用操作跟上面的没有virtual关键字时是一样的:00401090 lea eax,ebp-400401093 push eax00401094 call ILT+5(Test) (0040100a)004010C9 add esp,4(2) Test函数的反汇编内容(

12、跟上面的没有virtual关键字时可是大不一样哦):Line18: p-Output();00401048 mov eax,dword ptr ebp+8/将Test的第一个参数的值放入eax寄存器中,其实你应该已经知道了,这就是obj的/地址了;0040104B mov ecx,dword ptr eax/喔噢,将eax寄存器中存的数对应的地址的内容取出来,你知道这是什么吗?等会再/说,做个记号先: _20040104D mov esi,esp/这个是用来做esp指针检测的0040104F mov edx,dword ptr ebp+8/又把obj的地址存放到edx寄存器中,你该知道,其实就

13、是this指针,而这个就是为 /调用类的成员函数做准备的;00401052 push edx/将对象指针(也就是this指针)入栈,为调用类的成员函数做准备;00401053 call dword ptr ecx/这个调用的就是类的成员函数,你知道调用的哪个函数吗?等会再说,做个记号先:/_300401055 cmp esi,esp/比较esp指针的,要是不相同,下面的_chkesp函数将会让程序进入debug00401057 call _chkesp (00401110)/检测esp指针,处理可能出现的堆栈错误(如果出错,将陷入debug)。对一个C+类,如果它要呈现多态(一般的编译器会将这

14、个类以及它的基类中是否存在virtual关键字作为这个类是否要多态),那么类会有一个virtual table,而每一个实例(对象)都会有一个virtual pointer(以下简称vptr)指向该类的virtual function table,如图3所示:(下面右边表格中的VFuncAddr应该被理解为存放虚函数地址的内存单元的地址才准确。更准确地说,应该是跳转到相应函数的jmp指令的地址。)(图3)先来分析我们的main函数中的Derive类的对象obj,看看它的内存布局,由于没有数据成员,它的大小为4个字节,只有一个vptr,所以obj的地址也就是vptr的地址了。(之所以我这里举例的

15、类没有数据成员,因为不同的编译器将vptr放置的位置在对象内存布局中有可能不一样,当然,一般不是放在对象的头部,比如微软编译器;就是放在对象的尾部。不管哪种情况,对于这个例子,我这里的“obj的地址也就是vptr的地址”都是成立的。)一个对象的vptr并不由程序员指定,而是由编译器在编译中指定好了的。那么现在让我来分别解释上文中标记的_1 - _ 3。_1:也就是要解释这里为什么编译器会为我们生成一个默认的构造函数,它是用来干什么的?还是让我们从反汇编里寻找答案:这是由编译器默认生成的Derive的构造函数中选取出来的核心汇编片段:004010D9 pop ecx/编译器默认生成的Derive

16、的构造函数的调用方式为_thiscall,所以ecx寄存器,如前/所说,保存的就是this指针,也就是obj对象的地址,在这里也是vptr的地址了;/我发现即使你把一个构造函数声明为_stdcall,它跟默认的_thiscall的反汇编也是一/样的,这一点跟成员函数是不一样的;004010DA mov dword ptr ebp-4,ecx/对于_thiscall方式调用的类的成员函数,第一个局部变量总是this指针,ebp-4就是/函数的第一个局部变量的地址004010DD mov ecx,dword ptr ebp-4/因为要调用基类的构造函数,所以又得把this指针赋给ecx寄存器了;0

17、04010E0 call ILT+30(Base:Base) (00401023)/执行基类的构造函数;004010E5 mov eax,dword ptr ebp-4/将this指针放入eax寄存器;004010E8 mov dword ptr eax,offset Derive:vftable (0042201c)/将虚函数表的首地址放入this指针所指向的地址,也就是初始化了vptr了;大家看到了吧,编译器生成一个默认的构造函数,就是用来初始化vptr的;那么你大概也能想到其实Base的构造函数做了什么了,不出你所料,它也是用来做初始化vptr的:0040D769 pop ecx0040

18、D76A mov dword ptr ebp-4,ecx0040D76D mov eax,dword ptr ebp-40040D770 mov dword ptr eax,offset Base:vftable (00422020)不用再解释了,跟Derive的构造函数功能一样,初始化vptr了。如果你自己声明和定义了一个构造函数的话,将先执行这些初始化vptr的代码后,再会来执行你的代码了。(如果你在构造函数中有作为构造函数的初始化列表形式出现的赋值代码,那么将先执行你的初始化列表中的赋值代码,然后再执行本类的vptr的初始化操作,再执行构造函数体内的代码)_2 和 _ 3:0040104

19、8 mov eax,dword ptr ebp+80040104B mov ecx,dword ptr eax这里前一条指令是将obj的地址存放入eax中,那么你该知道obj地址对应的内存单元的前四个字节其实就是vptr地址?而vptr地址所对应的内存单元的内容其实就是vftable表格的起始地址,而vftable表格地址所对应的内存单元的内容就是虚函数地址。用下图更清楚地表示一下吧(如图4,该图表示地址和地址单元中的内容对应表。注意,右边的vftable表中的地址,其实并不是真正的函数地址,而是跳转到函数的jmp指令的地址,如0x0040EF12,并不是真正的Class:XXX函数的地址,而

20、是跳转到Class:XXX函数的jmp指令的地址)。这样ecx其实就是存放Derive:Output函数地址的内存单元的地址,然后调用:0040104F mov edx,dword ptr ebp+800401052 push edx00401053 call dword ptr ecx就跳转到相应函数执行该函数了。(如果有多个虚函数,且调用的是第N个虚函数,那么上句call指令就会被更改为这样的形式:call dword ptr ecx+4*(N-1))上面的汇编是不是象这样:我拿到一把钥匙,打开一个抽屉,取出里面的东西,不过这个东西还是一把钥匙,还得拿着这个钥匙去打开另一个抽屉,取出里面真

21、正的东西。_(图4)知道了来龙去脉,别人这么调用用汇编能做到调用相应的虚函数,那么我如果要用C/C+,该怎么做呢?我想你应该有眉目了吧。看看我是怎么干的(下面用一个C的函数指针调用了一个C+类的成员函数,将一个C+类的成员函数转换到一个C函数,需要做这些:C函数的参数个数比相应的C+类的成员函数多出一个,而且作为第一个参数,而且它必须是类对象的地址):将Base类的Output函数声明为virtual,然后将main函数更改为:int _cdecl main(int argc, char* argv) Derive obj; /对象还是要有一个的 typedef void (_stdcall

22、*PFUNC)(void*); /声明函数指针void *pThis = &obj; /取对象地址,作为this指针用 /对应图4是将0x0012ff24赋给pThisPFUNC pFunc = (PFUNC)*(unsigned int*)pThis; /取这个地址的内容,对应图4就应 /该是取地址0x0012ff24的内容为 /0x00400112了pFunc = (PFUNC)*(unsigned int*)pFunc; /再取这个地址的内容,对应图4就 /应该是取地址0x00400112的内容为 /0x0040EF12,也就是函数地址了 pFunc(pThis); /执行函数,将执行Derive:Output return 0;运行一下,看看结果。我可没有使用对象或者指向类的指针去调用函数哦。J这回你该知道虚函数是怎么回事了吧?这里介绍的都是基于微软VC+ 6.0编译器对虚函数的实现手段。编译器实现C+所使用的方法和策略,都是可以从其反汇编语句中一探究竟的。了解这些底层细节,将会对提高你的C/C+代码大有裨益!希望本文能对你有所帮助。

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1