C++虚函数调用的反汇编解析
- 格式:pdf
- 大小:270.07 KB
- 文档页数:7
详解如何实现C++虚函数调⽤汇编代码虚函数(代码段地址)被存放在虚函数表中,调⽤虚函数的流程是这样⼦的:先获取虚函数表的⾸地址,然后根据⽬标虚函数在虚函数表的位置(offset偏移)取出虚函数表中的虚函数地址,最后去call这个虚函数(地址),就完成虚函数的调⽤。
这个虚函数调⽤的流程在汇编代码中可以最直观的反映出来。
在排查软件异常或崩溃时,我们时常要借助汇编代码的上下⽂去辅助分析问题。
读懂C++虚函数调⽤的汇编代码实现,对于搞懂汇编代码的上下⽂时很有好处的。
今天我们就来看看虚函数调⽤的汇编代码实现。
⽐如如下的C++代码:// 1、虚接⼝类中定义了纯虚接⼝class IContactInterface{//...virtual BOOL IsLoadFinish() == 0; // 虚接⼝//...}// 2、⼦类中实现虚接⼝class Contact : public IContactInterface{//...BOOL IsLoadFinish();//...}// 3、new出⼦类的对象,存放到⽗类的指针变量中IContactInterface* g_pContactPtr = new Contact;// 4、获取⼦类对象的接⼝实现IContactInterface* GetContactPtr(){return g_pContactPtr;}// 5、调⽤GetContactPtr获取⼦类的对象去调⽤虚函数IsLoadFinishGetContactPtr()->IsLoadFinish();上述C++代码⽚中,主要包含了以下⼏点:1)定义了虚接⼝类IContactInterface,在类中有个IsLoadFinish虚函数;2)定义了⼀个⼦类Contact,继承于IContactInterface接⼝类,并实现了虚函数IsLoadFinish;3)new出⼀个Contact类的对象,赋值给⽗类IContactInterface的指针变量;4)调⽤GetContactPtr接⼝获取IContactInterface指针变量,调⽤虚函数IsLoadFinish。
C++ 虚函数表解析陈皓前言C++中的虚函数的作用主要是实现了多态的机制。
关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。
这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。
所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。
比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议。
关于虚函数的使用方法,我在这里不做过多的阐述。
大家可以看看相关的C++的书籍。
在这篇文章中,我只想从虚函数的实现机制上面为大家一个清晰的剖析。
当然,相同的文章在网上也出现过一些了,但我总感觉这些文章不是很容易阅读,大段大段的代码,没有图片,没有详细的说明,没有比较,没有举一反三。
不利于学习和阅读,所以这是我想写下这篇文章的原因。
也希望大家多给我提意见。
言归正传,让我们一起进入虚函数的世界。
虚函数表对C++ 了解的人都应该知道虚函数(Virtual Function)是通过一张虚函数表(Vi rtual Table)来实现的。
简称为V-Table。
在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应实际的函数。
这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。
这里我们着重看一下这张虚函数表。
C++的编译器应该是保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下)。
这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。
听我扯了那么多,我可以感觉出来你现在可能比以前更加晕头转向了。
没关系,下面就是实际的例子,相信聪明的你一看就明白了。
假设我们有这样的一个类:class Base {public:virtual void f() { cout << "Base::f"<< endl; }virtual void g() { cout << "Base::g"<< endl; }virtual void h() { cout << "Base::h"<< endl; }};按照上面的说法,我们可以通过Base的实例来得到虚函数表。
C++虚函数及虚函数表解析虚函数的定义: 虚函数必须是类的⾮静态成员函数(且⾮构造函数),其访问权限是public(可以定义为private or proteceted,但是对于多态来说,没有意义。
),在基类的类定义中定义虚函数的⼀般形式: virtual 函数返回值类型虚函数名(形参表) { 函数体 } 虚函数的作⽤是实现动态联编,也就是在程序的运⾏阶段动态地选择合适的成员函数,在定义了虚函数后, 可以在基类的派⽣类中对虚函数重新定义(形式也是:virtual 函数返回值类型虚函数名(形参表){ 函数体 }),在派⽣类中重新定义的函数应与虚函数具有相同的形参个数和形参类型。
以实现统⼀的接⼝,不同定义过程。
如果在派⽣类中没有对虚函数重新定义,则它继承其基类的虚函数。
当程序发现虚函数名前的关键字virtual后,会⾃动将其作为动态联编处理,即在程序运⾏时动态地选择合适的成员函数。
实现动态联编需要三个条件: 1、必须把需要动态联编的⾏为定义为类的公共属性的虚函数。
2、类之间存在⼦类型关系,⼀般表现为⼀个类从另⼀个类公有派⽣⽽来。
3、必须先使⽤基类指针指向⼦类型的对象,然后直接或者间接使⽤基类指针调⽤虚函数。
定义虚函数的限制: (1)⾮类的成员函数不能定义为虚函数,类的成员函数中静态成员函数和构造函数也不能定义为虚函数,但可以将析构函数定义为虚函数。
实际上,优秀的程序员常常把基类的析构函数定义为虚函数。
因为,将基类的析构函数定义为虚函数后,当利⽤delete删除⼀个指向派⽣类定义的对象指针时,系统会调⽤相应的类的析构函数。
⽽不将析构函数定义为虚函数时,只调⽤基类的析构函数。
(2)只需要在声明函数的类体中使⽤关键字“virtual”将函数声明为虚函数,⽽定义函数时不需要使⽤关键字“virtual”。
(3)如果声明了某个成员函数为虚函数,则在该类中不能出现和这个成员函数同名并且返回值、参数个数、参数类型都相同的⾮虚函数。
C++虚函数调用的反汇编解析虚函数的调用如何能实现其“虚”?作为C++多态的表现手段,估计很多人对其实现机制感兴趣。
大约一般的教科书就说到这个C++强大机制的时候,就是教大家怎么用,何时用,而不会去探究一下这个虚函数的真正实现细节。
(当然,因为不同的编译器厂家,可能对虚函数有自己的实现,呵呵,这就算是虚函数对于编译器的“多态”了:)。
作为编译型语言,C++编译的最后结果就是一堆汇编指令了(这里不同于.NET的CLR)。
今天,我就来揭开它的神秘面纱,从汇编的层面来看看虚函数到底怎么实现的。
让大家对虚函数的实现不仅知其然,更知其所以然。
(本文程序环境为:PC + Windows XP Pro + Visual C++6.0,文中所得出来的结果和反映的编译器策略也只针对VC6.0的编译器)先看一段简单代码:Code Segment:Line01: #include <stdio.h>Line02:Line03: class Base {Line04: public:Line05: void __stdcall Output() {Line06: printf("Class Base"n");Line07: }Line08: };Line09:Line10: class Derive : public Base {Line11: public:Line12: void __stdcall Output() {Line13: printf("Class Derive"n");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() {那么,很明显地,程序的运行结果将是:Class DeriveTest函数这回算是认清楚了这个指针是一个指向Derive类对象的指针,并且正确的调用了其Output函数。
目前我们写的最简单的Main函数如下:代码:#include "stdafx.h"int _tmain(int argc, _TCHAR* argv[]){return 0;}利用VS编译器将运行时程序反汇编,结果如下:代码:int _tmain(int argc, _TCHAR* argv[]){010C13A0 push ebp010C13A1 mov ebp,esp010C13A3 sub esp,0C0h010C13A9 push ebx010C13AA push esi010C13AB push edi010C13AC lea edi,[ebp-0C0h]010C13B2 mov ecx,30h010C13B7 mov eax,0CCCCCCCCh010C13BC rep stos dword ptr es:[edi]return 0;010C13BE xor eax,eax}010C13C0 pop edi010C13C1 pop esi010C13C2 pop ebx010C13C3 mov esp,ebp010C13C5 pop ebp010C13C6 ret看起来汇编很头疼吧,那么让我们来茅塞顿开吧。
首先了解一下以前几个未知半懂的汇编指令的含义:代码:push ebpmov ebp,esppush:把一个32位操作数压入栈中,这个操作导致esp被减4。
ebp被用来保存这个函数执行前的esp 的值,执行完毕后用ebp恢复esp。
同时,调用此函数的上层函数也用ebp做同样的事情。
所以先把ebp 压入堆栈,返回之前弹出,避免ebp被我们改动。
代码:xor eax,eaxretxor eax,eax常用来代替mov eax,0。
清零操作。
在windows中,函数返回值都是放在eax中然后返回,外部从eax中得到返回值。
这就代表return 0操作。
代码:lea edi,[ebp-0C0h]lea取得第二个参数代表的地址放入第一个参数代表的寄存器中。
C#的虚函数解析机制前⾔ 这篇⽂章出⾃我个⼈对C#虚函数特性的研究和理解,未参考、查阅第三⽅资料,因此很可能存在谬误之处。
我在这⾥只是为了将我的理解呈现给⼤家,也希望⼤家在看到我犯了错误后告诉我。
⽤词约定“⽅法的签名”包括返回类型、⽅法名、参数列表,这三者共同标识了⼀个⽅法。
“声明⽅法”,即指出该⽅法的签名。
“定义⽅法”,则是指定调⽤⽅法时执⾏的代码。
“同名⽅法”是指⽅法的签名相同的两个⽅法。
“重写”⼀个⽅法,意味着⼦类想继承⽗类对⽅法的声明,却想重新定义该⽅法。
单独使⽤“使⽤”⼀词时,包括“显式”或“隐式”两种使⽤⽅式:前者是指在代码中指明,后者是根据语句的上下⽂推断。
某个类的⽅法,包括了在该类中定义的⽅法,以及由继承得到的直接⽗类的⽅法。
注意这条规则的递归性质。
理论部分 在⽗类与⼦类⾥,除了类之间的继承链,还存在⽅法之间的继承链。
C#⾥,在⼀个类中声明⼀个⽅法时,有四个和⽅法的继承性有关的关键字:new、virtual、sealed、override。
virtual表⽰允许⼦类的同名⽅法与其①建⽴继承链。
override表⽰其①与⽗类的同名⽅法之间建⽴了继承链,并隐式使⽤virtual关键字。
new表⽰其切断了其①与⽗类的同名⽅法之间的继承链。
sealed表⽰将其①与⽗类的同名⽅法建⽴继承链(注意这个就是override关键字的特性),并且不允许⼦类的同名⽅法与其建⽴继承链。
在使⽤sealed关键字时,必须同时显式使⽤override关键字。
以及:在定义⽅法时,若不使⽤以上关键字,⽅法就会具有new关键字的特性。
对于这⼀点,如果⽗类中没有同名⽅法,则没有任何影响;如果⽗类中存在⼀个同名⽅法,编译器会给出⼀个警告,询问你是否是想隐藏⽗类的同名⽅法,并推荐你显式地为其指定new关键字。
①其:指代正在进⾏声明的⽅法。
依照上述的说明,在调⽤类上的某个⽅法时,可以为该⽅法构建出⼀个或多个“⽅法继承链”。
C语言函数调用分析我的测试环境:Fedora14Gcc版本:gcc-4.5.1内核版本:2.6.38.1C语言是一个强大的语言,特别是对于嵌入式开发过程中有时需要反汇编分析代码中存在的问题,函数是C语言中的难点,关于函数的调用也是很多人不能理解的,很多知道的也是一知半解。
对C语言的调用有了一个比较清晰的认识就能够更清晰的分析代码中存在的问题。
我也是看了很多的资料,然后自己写了一一段小代码作为分析的测试代码。
首先记住在X86体系里很多的寄存器都有特殊的用途,其中ESP表示当前函数堆栈的栈顶指针,而EBP则表示当前函数堆栈的基地址。
EBP是栈基址的指针,永远指向栈底(高地址),ESP是栈指针,永远指向栈顶(低地址)。
我的代码如下:#include;int pluss_a_and_b(int a,int b) {int c = -2;return (a + b - c);}int call_plus(int *a,int *b) {int c = *a;int d = *b;*a = d;*b = c;return pluss_a_and_b(c,d); }int main(){int c = 10;int d = 20;int g = call_plus(&c,&d);return 0;}对上面的代码进行编译和反汇编:[gong@Gong-Computer deeplearn]$ gcc -g testcall.c -o testcall[gong@Gong-Computer deeplearn]$ objdump -S -d testcall >; testcall_s然后对反汇编的代码进行分析:...8048393: c3 ret08048394 ;:#include;int pluss_a_and_b(int a,int b){8048394: 55 push %ebp8048395: 89 e5 mov %esp,%ebp8048397: 83 ec 10 sub $0x10,%espint c = -2;804839a: c7 45 fc fe ff ff ff movl $0xfffffffe,-0x4(%ebp)return (a + b - c);80483a1: 8b 45 0c mov 0xc(%ebp),%eax80483a4: 8b 55 08 mov 0x8(%ebp),%edx80483a7: 8d 04 02 lea (%edx,%eax,1),%eax 80483aa: 2b 45 fc sub -0x4(%ebp),%eax}80483ad: c9 leave80483ae: c3 ret080483af ;:int call_plus(int *a,int *b){80483af: 55 push %ebp80483b0: 89 e5 mov %esp,%ebp80483b2: 83 ec 18 sub $0x18,%espint c = *a;80483b5: 8b 45 08 mov 0x8(%ebp),%eax 80483b8: 8b 00 mov (%eax),%eax80483ba: 89 45 fc mov %eax,-0x4(%ebp) int d = *b;80483bd: 8b 45 0c mov 0xc(%ebp),%eax 80483c0: 8b 00 mov (%eax),%eax80483c2: 89 45 f8 mov %eax,-0x8(%ebp)*a = d;80483c5: 8b 45 08 mov 0x8(%ebp),%eax 80483c8: 8b 55 f8 mov -0x8(%ebp),%edx 80483cb: 89 10 mov %edx,(%eax)*b = c;80483cd: 8b 45 0c mov 0xc(%ebp),%eax 80483d0: 8b 55 fc mov -0x4(%ebp),%edx 80483d3: 89 10 mov %edx,(%eax)return pluss_a_and_b(c,d);80483d5: 8b 45 f8 mov -0x8(%ebp),%eax 80483d8: 89 44 24 04 mov %eax,0x4(%esp) 80483dc: 8b 45 fc mov -0x4(%ebp),%eax 80483df: 89 04 24 mov %eax,(%esp)80483e2: e8 ad ff ff ff call 8048394 ; }80483e7: c9 leave80483e8: c3 ret080483e9 ;:int main(){80483e9: 55 push %ebp80483ea: 89 e5 mov %esp,%ebp80483ec: 83 ec 18 sub $0x18,%espint c = 10;80483ef: c7 45 f8 0a 00 00 00 movl $0xa,-0x8(%ebp)int d = 20;80483f6: c7 45 f4 14 00 00 00 movl$0x14,-0xc(%ebp)int g = call_plus(&c,&d);80483fd: 8d 45 f4 lea -0xc(%ebp),%eax 8048400: 89 44 24 04 mov %eax,0x4(%esp) 8048404: 8d 45 f8 lea -0x8(%ebp),%eax 8048407: 89 04 24 mov %eax,(%esp)804840a: e8 a0 ff ff ff call 80483af ; 804840f: 89 45 fc mov %eax,-0x4(%ebp)return 0;8048412: b8 00 00 00 00 mov $0x0,%eax}8048417: c9 leave8048418: c3 ret8048419: 90 nop804841a: 90 nop...首先,C语言的入口都是从main函数开始的,但是从反汇编代码中可以发现并不是只有自己设计的代码,还存在很多关于初始化等操作。
C调用汇编1. 概述C调用汇编是一种将高级语言与底层机器语言相结合的技术。
通过使用汇编语言,我们可以直接访问底层硬件资源,实现高效的代码优化和特殊功能的实现。
C语言作为一种高级语言,具有易读易写的特点,适合用于编写大型程序。
通过C调用汇编,我们可以充分发挥C语言的优势,同时又可以利用汇编语言的强大功能。
在C调用汇编中,我们通常使用汇编嵌入(Inline Assembly)的方式将汇编代码嵌入到C源代码中。
这种方式可以使我们在C代码中直接使用汇编指令,实现对底层硬件的直接控制。
同时,我们也可以将汇编代码编写为独立的汇编文件,并通过链接的方式与C代码进行连接,实现C调用汇编的功能。
2. C调用汇编的优势C调用汇编具有以下几个优势:•直接访问底层硬件资源:通过汇编语言,我们可以直接访问底层硬件资源,实现对硬件的直接控制。
这对于一些特殊功能的实现非常有用,例如驱动程序的编写等。
•高效的代码优化:汇编语言是一种低级语言,可以对代码进行细粒度的优化。
通过使用汇编代码,我们可以充分发挥硬件的性能,提高程序的执行效率。
•实现特殊功能:汇编语言具有强大的功能,可以实现一些高级语言无法实现的特殊功能。
例如,通过汇编语言可以实现对特定硬件的底层控制,或者对特殊算法的加速等。
3. C调用汇编的方法C调用汇编有两种常用的方法:汇编嵌入和汇编文件链接。
3.1 汇编嵌入汇编嵌入是将汇编代码直接嵌入到C源代码中的一种方式。
通过使用汇编嵌入,我们可以在C代码中直接使用汇编指令,实现对硬件的直接控制。
汇编嵌入的语法格式如下:__asm__(汇编指令);在汇编嵌入中,我们可以使用C语言的变量和表达式,并通过汇编指令对其进行操作。
例如,以下是一个简单的示例,演示了如何使用汇编嵌入实现两个整数相加:#include <stdio.h>int main() {int a = 10;int b = 20;int sum;__asm__("movl %1, %%eax;""addl %2, %%eax;""movl %%eax, %0;": "=r" (sum): "r" (a), "r" (b): "%eax");printf("Sum: %d\n", sum);return 0;}在上述示例中,我们使用了汇编嵌入的方式将汇编代码嵌入到C代码中,实现了对两个整数的相加。
函数调用机制、C与汇编相互调用--2012年11月22日22:06:23为了提高代码执行效率,代码中有些地方直接使用汇编语言编制。
这就会涉及到两种语言的程序间相互调用的问题。
本文首先说明C语言函数的相互调用机制,然后使用示例来说明C与汇编之间的调用方法。
【C函数相互调用机制】函数调用操作包括从一块代码到另一块代码之间的双向数据传递和执行控制转移。
数据传递通过函数参数和返回值来进行。
另外,还需要在进入函数时为函数的局部变量分配存储空间,并且在退出函数时收回这部分空间。
Intel80x86CPU为控制传递提供了简单的指令,而数据的传递和局部变量存储空间的分配和回收则通过栈操作来实现。
1、栈帧结构和控制权转移方式大多数CPU上的程序实现使用栈来支持函数调用操作。
栈被用来传递函数参数、存储返回信息、临时保存寄存器原有值以备恢复以及用来存储局部数据。
单个函数调用操作所使用的栈部分被称为栈帧(Stack frame)结构,如下图所示。
栈帧结构的两端由两个指针来指定。
寄存器ebp通常用作帧指针(frame pointer),而esp则用作栈指针(stack pointer)。
在函数执行过程中,栈指针esp会随着数据的入栈和出栈而移动,因而函数中对大部分数据的访问都基于帧指针ebp进行。
对于函数A调用函数B的情况,传递给B的参数包含在A的栈帧中。
当A调用B时,函数A的返回地址(调用返回后继续执行的指令地址)被压入栈中,栈中该位置也明确指明了A栈帧的结束处。
而B的栈帧则从随后的栈部分开始。
再随后则用于存放任何保存的寄存器值以及函数的临时值。
B函数同样也使用栈来保存不能存放在寄存器中的局部变量。
例如由于CPU寄存器数量有限而不能存放函数的所有局部数据,或者有些局部变量是数组或结构,因此必须使用数组或结构引用来访问。
还有就是C语言的地址操作符“&”被应用到一个局部变量上时,就需要为该变量生成一个地址,即为变量的地址指针分配一空间。
Test函数这回算是认清楚了这个指针是一个指向Derive类对象的指针,并且正确的调用了其Output函数。
编译器如何做到这一切的呢?我们来看看没有“virtual”关键字和有“virtual”关键字,其最终的汇编代码区别在那里。
(在讲解下面的汇编代码前,让我们对汇编来一个简单扫描。
当然,如果你对汇编已经很熟练,那么goto到括号外面吧^_^。
先说说上面的Output函数被声明为__stdcall的调用方式:它表示函数调用时,参数从右到左进行压栈,函数调用完后由被调用者恢复堆栈指针esp。
其它的调用方式在文中描述。
所谓的C++的this指针:也就是一个对象的初始地址。
在函数执行时,它的参数以及函数内的变量将拥有如下所示的堆栈结构:
(图1)
如上图1所示,我们的参数和局部变量在汇编中都将以ebp加或者减多少来表示。
你可能会有疑问了:有时候我的参数或者局部变量可能是一个很大的结构体或者只是一个char,为什么这里ebp加减的都是4的倍数呢?恩,是这样的,对于32位机器来说,采用4个字节,也就是每次传输32位,能够取得最佳的总线效率。
如果你的参数或者局部变量比4个字节大,就会被拆成每次传4个字节;如果比4个字节小,那还是每次传4个字节。
再简单解释一下下面用到的汇编指令,这些指令都是见名知意的哦:
①mov destination,source
将source的值赋给destination。
注意,下面经常用到了“[xxx]”这样的形式,“xxx”对应某个寄存器加减某个数,“[xxx]”表示是取“xxx”的值对应的内存单元的内容。
好比“xxx”是一把钥匙,去打开一个抽屉,然后将抽屉里的东西取出来给别人,或者是把别人给的东西放到这个抽屉里;
②lea destination,[source]
将source的值赋给destination。
注意,这个指令就是把source给destination,而不会去把source 对应的内存单元的内容赋给destination。
好比是它就把钥匙给别人了;
在调试时如果想查看反汇编的话,你应该点击图2下排最右的按钮。
(图2)
其它指令我估计你从它的名字都能知道它是干什么的了,如果想知道其具体意思,这个应该参考汇编手册。
:)
一.没有virtual关键字时:
(图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*PFUNC)(void*);//声明函数指针
void*pThis=&obj;//取对象地址,作为this指针用
//对应图4是将0x0012ff24赋给pThis
PFUNC pFunc=(PFUNC)*(unsigned int*)pThis;//取这个地址的内容,对应图4就应
//该是取地址0x0012ff24的内容为
//0x00400112了
pFunc=(PFUNC)*(unsigned int*)pFunc;//再取这个地址的内容,对应图4就
//应该是取地址0x00400112的内容为
//0x0040EF12,也就是函数地址了
pFunc(pThis);//执行函数,将执行Derive::Output
return0;
}
运行一下,看看结果。
我可没有使用对象或者指向类的指针去调用函数哦。
这回你该知道虚函数是怎么回事了吧?这里介绍的都是基于微软VC++6.0编译器对虚函数的实现手段。
编译器实现C++所使用的方法和策略,都是可以从其反汇编语句中一探究竟的。
了解这些底层细节,将会对提高你的C/C++代码大有裨益!。