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语言的地址操作符“&”被应用到一个局部变量上时,就需要为该变量生成一个地址,即为变量的地址指针分配一空间。
C__函数调用原理理解函数调用是计算机程序中常见的操作之一,它是通过调用函数并传递一些参数来执行特定任务的过程。
下面我将详细解释函数调用的原理。
在C语言中,函数调用的原理可以通过以下几个步骤来理解:1.当程序执行到函数调用语句时,首先会将函数调用所在的指令地址压入程序栈中,然后跳转到要调用的函数的入口地址。
2.调用函数时,会创建一个新的栈帧,该栈帧用于存储函数所需的局部变量、参数值和临时变量。
3.在调用函数时,会将函数的参数以特定的方式传递给函数。
在C语言中,通常使用栈来传递参数。
参数的值被压入栈中,并且按照逆序的顺序存储。
4. 当函数执行完毕后,返回值会被存放在指定的寄存器中,通常是eax寄存器。
同时,还会将函数的返回地址从栈中弹出,并跳转回调用函数的位置。
5.在函数执行完毕后,局部变量、参数值和临时变量所占用的栈帧空间会被释放,同时栈指针会回到上一个栈帧的位置。
6.在函数调用过程中,程序栈在调用栈帧之间进行切换,以便正确地管理函数调用和返回的过程。
总结起来,函数调用的原理可以归纳为以下三个关键步骤:压栈、跳转和弹栈。
首先,将返回地址和参数值压入栈中;然后,跳转到被调用函数的入口地址;最后,在函数执行完毕后,返回地址被从栈中弹出,并跳转回调用函数的位置。
函数调用还涉及到参数传递的原理。
在C语言中,参数可以通过寄存器传递,也可以通过栈传递。
当函数的参数较少时,常见的做法是将参数值存放在寄存器中,这样可以提高程序的执行效率。
当参数过多时,或者参数的值较大时,会使用栈来传递参数。
栈可以动态调整大小,因此更适合于存储大量的参数值。
函数调用的原理也与函数的返回类型有关。
当函数的返回类型是void时,函数执行完毕后不需要返回值。
当函数的返回类型是其他类型时,返回值需要存放在特定的寄存器中,以便调用函数可以获取到正确的返回值。
总之,函数调用是程序执行过程中的重要操作,它通过在栈上创建新的栈帧,传递参数值和返回地址,以及释放栈帧空间来实现函数调用和返回的过程。
c++虚函数的调用过程
虚函数的调用过程如下:
1、在编译时,编译器将非虚函数的调用准备好,并且不绑定到具体的函数实现。
2、如果有一个虚函数被执行,编译器会构建一个调用函数,该调用函数将检查对象的虚函数表,以查找相应的函数实现。
这个调用函数会被加载到对象的调用图表中,在调用的时候,执行的是这个调用表的函数。
3、如果在调用的时候,基类的虚函数被实现成派生类的函数,那么编译器会生成一个指向派生类函数的指针,并将这个指针放入到虚函数表里面。
4、一旦这个调用函数被构建好,它会在执行期间调用具体的函数实现。
如果对象是一个基类,那么调用的函数就是基类的实现;如果对象是一个派生类,那么调用的函数就是派生类的实现。
1.3函数调⽤反汇编解析以及调⽤惯例案例分析⾸先来段代码来瞧瞧:#include <stdio.h>int add(int x,int y){int z;z=x+y;return z;}int main(){int r=add(3,4);printf("%d\n",r);return0;}⼀个简单的函数调⽤,我们把main函数⾥的r=add(3,4)反汇编:可以看到,(这⾥采⽤c默认的函数调⽤惯例,)⾸先进⾏参数压栈,看清楚了,是把参数从右往左压栈,然后call这个函数。
跟踪,call跟进去后,发现call指令执⾏后,ESP寄存器减4,也就是说,有往栈⾥压了个参数--函数返回地址。
看内存变化:压进去的是0x00401081,从第⼀图可以看到,这就是call指令后的下⼀句,也就是函数的返回地址,函数调⽤完后得知道往哪⾥返回啊。
其实,call指令等价于两步操作:push 返回地址jmp 函数⼊⼝地址我们继续跟进,看被调函数的反汇编代码: ⾸先,再提醒⼀下,前⾯知道了,已经压栈了三次,前两次是压栈形参,第三次是压栈返回地址。
看这⾥的前两句,⼜把EBP压栈了,然后把ESP赋值给了EBP。
有没有觉得奇怪呢?通常情况下,EBP都存储基址,这⾥也不例外,由于后⾯可能出现多次压栈出栈操作,ESP是变动的,需要⼀个基址寄存器来加减偏移量去栈上的值,毕竟刚才也看到了,栈⾥可是有不少重要的东东哦,通过基址加减偏移量就可以访问了。
于是,EBP就暂时担待了这个重任。
后⾯,ESP做了个减法,为函数内部局部变量等留下⼀定的栈空间,⼜压栈了⼏个寄存器,以备使⽤他们⽽不⾄于毁坏原有数据(后⾯再出栈就恢复了)。
看核⼼代码,z=x+y后⾯,把ebp+8地址的值赋给eax,思考⼀下,ebp+8是哪块内存?回忆下前⾯栈⾥都压了什么进去?ebp+0存的是ebp原有值,ebp+4存的是返回地址,ebp+8存的是最后⼀个被压的参数,ebp+0c存的是......所以这⾥就是作加法,然后赋值给了ebp-4.这⼜是什么呢?这是z的地址。
C++反汇编-虚函数学⽆⽌尽,积⼟成⼭,积⽔成渊-《C++反汇编与逆向分析技术揭秘》读书笔记在C++中,使⽤关键字virtual声明为虚函数。
虚函数地址表(虚表)定义:当类中定义有虚函数时,编译器会把该类中所有虚函数的⾸地址保存在⼀张地址表中,即虚函数地址表。
虚表信息在编译后被链接到执⾏⽂件中,因此所获得的虚表地址是⼀个固定的地址。
虚表中虚函数的地址排列顺序依据虚函数在类中的声明顺序⽽定。
虚表指针同时编译器还会在类的每个对象添加⼀个隐藏数据成员,称为虚表指针,保存着虚表的⾸地址,⽤于记录和查找虚函数。
虚表指针的初始化是通过编译器在构造函数中插⼊代码实现的。
由于必须初始化虚表指针,编译器会提供默认的构造函数。
虚函数调⽤过程虚表间接寻址访问:使⽤对象的指针或引⽤调⽤虚函数。
根据对象的⾸地址,取出相应的虚表指针,在虚表查找对应的虚函数的⾸地址,并调⽤执⾏。
直接调⽤访问:使⽤对象调⽤虚函数,和调⽤普通成员函数⼀样。
虚函数的识别:类中隐式定义⼀个数据成员数据成员在⾸地址处,占4字节构造函数初始化该数据成员为某个数组的⾸地址地址属于数据区,相对固定的地址数组的成员是函数指针函数被调⽤⽅式是thiscall构造函数与析构函数都会将虚表指针设置为当前对象所属类中的虚表地址。
构造函数中是完成虚表指针的初始化,此时虚表指针并没有指向虚表函数。
执⾏析构函数时,其对象的虚表指针已经指向某个虚表⾸地址。
虚函数是在还原虚表指针,让其指向⾃⾝的虚表⾸地址,防⽌在析构函数中调⽤虚函数时取到⾮⾃⾝虚表。
⽰例C++源码1 #include <iostream>2using namespace std;34class CVirtual {5public:6virtual int GetNumber() { return m_nNumber; }7virtual void SetNumber(int nNumber) { m_nNumber = nNumber;}8 ~CVirtual(){ printf("~CVirtual!"); }9private:10int m_nNumber;111213 };1415int main()16 {17 CVirtual myVirtual ,*pVirtual;18 pVirtual = &myVirtual;19 pVirtual->SetNumber(10);20 printf("%d\r\n", pVirtual->GetNumber());21return0;汇编代码1.构造函数mov [ebp-8], ecx ;=>保存this指针mov eax, [ebp-8] ;=>eax获得this指针mov dword ptr [eax], offset ??_7CVirtual@@6B@ ; const CVirtual::`vftable' ;=>虚表指针初始化 2.析构函数mov [ebp-4], ecx ;=>保存this指针mov eax, [ebp-4] ;=>eax获得this指针mov dword ptr [eax], offset ??_7CVirtual@@6B@ ; const CVirtual::`vftable' =>虚表指针重置push offset aCvirtual ; "~CVirtual!"3.虚函数调⽤pVirtual->SetNumber(10);push 0Ah ;=>参数10压栈mov eax, [ebp-18h] ;=>eax为this指针mov edx, [eax] ;=>edx为虚表指针mov ecx, [ebp-18h] ;=>ecx传递this指针mov eax, [edx+4] ;=>虚函数SetNumber的地址=虚表+offset 4call eax。
c语⾔中函数调⽤的本质从汇编⾓度分析今天下午写篇博客吧,分析分析c语⾔中函数调⽤的本质,⾸先我们知道c语⾔中函数的本质就是⼀段代码,但是给这段代码起了⼀个名字,这个名字就是他的的这段代码的开始地址这也是函数名的本质,其实也就是汇编中的标号。
下⾯我们会接触到⼀些东西⽐如 eip 就是我们常常说的程序计数器,还有ebp和esp (这⾥是俩个指针,记得我们以前学8086也就⼀个sp堆栈指针)分别为EBP是指向栈底的指针,在过程调⽤中不变,⼜称为帧指针。
ESP指向栈顶,程序执⾏时移动,ESP减⼩分配空间,ESP增⼤释放空间,ESP⼜称为栈指针。
当然现在不理解没关系(在堆栈中变量分布是从⾼地址到低地址分布)。
好了我们开始正式话题吧:1.先看图,下⾯我先贴出⼀个调⽤代码。
# include <stdio.h>int fun(int a, int b){int c = 0;c= a + b;return c;}int main(void){int a = 1;int b = 3;fun(a,b);return 0;} 反汇编后的代码--- 汇编代码-----------------------------------------------------------------------__CxxUnhandledExceptionFilter:00A51113 jmp __CxxUnhandledExceptionFilter (0A525E0h)___CxxSetUnhandledExceptionFilter:00A51118 jmp __CxxSetUnhandledExceptionFilter (0A52660h)_QueryPerformanceCounter@4:00A5111D jmp _QueryPerformanceCounter@4 (0A53BB0h) 、_fun: ;注意了 fun在这⾥00A51122 jmp fun (0A513C0h) ;可以看出fun还要跳转这次跳到了0A513C0h__unlock:00A51127 jmp __unlock (0A536F4h)_GetCurrentProcessId@0:00A5112C jmp _GetCurrentProcessId@0 (0A53BB6h)@_RTC_CheckStackVars2@12:00A51131 jmp _RTC_CheckStackVars2 (0A51490h)___set_app_type:00A51136 jmp ___set_app_type (0A5269Eh)--- 被调函数的真正地址 -----------------------------------------1: # include <stdio.h>2:3: int fun(int a, int b)4: {00A513C0 push ebp ;压栈 ebp 保护ebp00A513C1 mov ebp,esp ;将现在的esp地址给ebp换句话说ebp现在指向了这⾥;其实也就是栈帧的最下⾯00A513C3 sub esp,0CCh00A513C9 push ebx00A513CA push esi00A513CB push edi00A513CC lea edi,[ebp-0CCh]00A513D2 mov ecx,33h00A513D7 mov eax,0CCCCCCCCh00A513DC rep stos dword ptr es:[edi]5: int c = 0;00A513DE mov dword ptr [c],06: c= a + b;00A513E5 mov eax,dword ptr [a]00A513E8 add eax,dword ptr [b]00A513EB mov dword ptr [c],eax7: return c;00A513EE mov eax,dword ptr [c]8:9: }00A513F1 pop edi00A513F2 pop esi00A513F3 pop ebx00A513F4 mov esp,ebp00A513F6 pop ebp00A513F7 ret--- 主调函数 -----------------------------------------------------------------------10: int main(void)11: {00A51A11 mov ebp,esp00A51A13 sub esp,0D8h00A51A19 push ebx00A51A1A push esi00A51A1B push edi00A51A1C lea edi,[ebp-0D8h]00A51A22 mov ecx,36h00A51A27 mov eax,0CCCCCCCCh00A51A2C rep stos dword ptr es:[edi]12: int a = 1;00A51A2E mov dword ptr [a],1 ;定义变量a13: int b = 3;00A51A35 mov dword ptr [b],3 ;定义变量b14: fun(a,b);00A51A3C mov eax,dword ptr [b] ;把变量b给eax00A51A3F push eax ;eax压栈也就b压栈00A51A40 mov ecx,dword ptr [a] ;同上00A51A43 push ecx00A51A44 call _fun (0A51122h) ;汇编开始调⽤,在汇编中函数名前⾯加下划线当标号处理;地址是0A51122h,现在我们去哪⾥00A51A49 add esp,815:16:17: return 0;00A51A4C xor eax,eax18: }00A51A4E pop edi00A51A4F pop esi00A51A50 pop ebx00A51A51 add esp,0D8h00A51A57 cmp ebp,esp00A51A59 call __RTC_CheckEsp (0A5113Bh)00A51A5E mov esp,ebp00A51A60 pop ebp00A51A61 ret--- ⽆源⽂件 ----------------------------------------------------------------------- 2.是不是看上⾯的已经懵逼了,没关系了,我来介绍⼀下上⾯我是在vs中进⾏了反汇编,原本准备gcc下搞,后来懒得折腾了。
C++函数调⽤的反汇编过程及Thunk应⽤x86汇编基础知识1. 汇编常⽤寄存器1. esp,(Extended stack pointer)栈顶指针。
因为x86的栈内存是向下扩展的,因此当push⼊栈时,esp–。
pop出栈时,esp++。
esp主要维护当前栈。
2. ebp,(Extended Base Pointer)栈基地址。
⼀般都是在函数⼊⼝时,保存前函数的ebp,并将esp赋值给ebp,然后通过ebp来操作形参和临时参数。
3. eax,(Extended Accumulator)累加器寄存器,加法乘法指令的缺省寄存器。
函数的返回值⼀般也会存在eax。
4. ebx,(Extended Base)基址寄存器,在内存寻址时存放基地址。
5. ecx,(Extended Counter)计数器寄存器,配合rep/loop指令,主要⽤来表⽰循环次数。
C++中,this指针会存在ecx中。
6. edx,存⼊除法的余数。
7. esi/edi,(source/destination index)源/⽬标索引寄存器,因为在很多字符串操作指令中, DS:ESI指向源串,⽽ES:EDI指向⽬标串。
8. eip,CPU每次执⾏指令都要先读取EIP寄存器的值,然后定位EIP指向的内存地址(偏移地址),并且读取汇编指令,最后执⾏。
其实就是当前的汇编内存地址。
2. 汇编常⽤指令基础1. push指令,压栈。
2. pop指令,出栈。
3. mov指令,将源数(可以是⽴即数,也可以是寄存器或内存单元),拷贝到⽬标指定位置。
4. lea指令,(Load Effective Address)将源数(可以是表达式)的地址拷贝到指定寄存器。
lea edi,[ebp];因为取值符[],这样lea edi,[ebp]和mov edi,ebp是等价的。
不同点是,lea的源数可以是表达式完成加减法,相较于mov不能,所以在源数是表达式时,lea更有效率些。
详解C++虚函数的⼯作原理静态绑定与动态绑定讨论静态绑定与动态绑定,⾸先需要理解的是绑定,何为绑定?函数调⽤与函数本⾝的关联,以及成员访问与变量内存地址间的关系,称为绑定。
理解了绑定后再理解静态与动态。
静态绑定:指在程序编译过程中,把函数调⽤与响应调⽤所需的代码结合的过程,称为静态绑定。
发⽣在编译期。
动态绑定:指在执⾏期间判断所引⽤对象的实际类型,根据实际的类型调⽤其相应的⽅法。
程序运⾏过程中,把函数调⽤与响应调⽤所需的代码相结合的过程称为动态绑定。
发⽣于运⾏期。
C++中动态绑定在C++中动态绑定是通过虚函数实现的,是多态实现的具体形式。
⽽虚函数是通过虚函数表实现的。
这个表中记录了虚函数的地址,解决继承、覆盖的问题,保证动态绑定时能够根据对象的实际类型调⽤正确的函数。
这个虚函数表在什么地⽅呢?C++标准规格说明书中说到,编译器必须要保证虚函数表的指针存在于对象实例中最前⾯的位置(这是为了保证正确取到虚函数的偏移量)。
也就是说,我们可以通过对象实例的地址得到这张虚函数表,然后可以遍历其中的函数指针,并调⽤相应的函数。
虚函数的⼯作原理要想弄明⽩动态绑定,就必须弄懂虚函数的⼯作原理。
C++中虚函数的实现⼀般是通过虚函数表实现的(C++规范中没有规定具体⽤哪种⽅法,但⼤部分的编译器⼚商都选择此⽅法)。
类的虚函数表是⼀块连续的内存,每个内存单元中记录⼀个JMP指令的地址。
编译器会为每个有虚函数的类创建⼀个虚函数表,该虚函数表将被该类的所有对象共享。
类的每个虚成员占据虚函数表中的⼀⾏。
如果类中有N个虚函数,那么其虚函数表将有N*4字节的⼤⼩。
虚函数(virtual)是通过虚函数表来实现的,在这个表中,主要是⼀个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其真实反映实际的函数。
这样,在有虚函数的类的实例中分配了指向这个表的指针的内存(位于对象实例的最前⾯),所以,当⽤⽗类的指针来操作⼀个⼦类的时候,这张虚函数表就显得尤为重要,指明了实际所应调⽤的函数。
#include <iostream>using namespace std;int func(int a,int b);void main(){int x = 1;int y = 2;int z = func(x,y);}int func(int a,int b){return a + b;}汇编解析:void main(){//这个函数头产生的汇编代码是:main:013E13A0 push ebp013E13A1 mov ebp,esp013E13A3 sub esp,0E4h013E13A9 push ebx013E13AA push esi013E13AB push edi013E13AC lea edi,[ebp-0E4h]013E13B2 mov ecx,39h013E13B7 mov eax,0CCCCCCCCh013E13BC rep stos dword ptr es:[edi]******************************************************* int x = 1;int y = 2;//这个的汇编代码是:013E13BE mov dword ptr [x],1013E13C5 mov dword ptr [y],2******************************************************* //接下来就是函数的调用了,先是把参数压栈013E13CC mov eax,dword ptr [y]013E13CF push eax013E13D0 mov ecx,dword ptr [x]013E13D3 push ecx//参数压栈后就是调用函数013E13D4 call func (13E11D1h)// 在这个地方用F11调试步进,可以进入子函数func的汇编代码,如下:013E11D1 jmp func (13E35B0h)func:013E35B0 push ebp013E35B1 mov ebp,esp013E35B3 sub esp,0C0h013E35B9 push ebx013E35BA push esi013E35BB push edi013E35BC lea edi,[ebp-0C0h]013E35C2 mov ecx,30h013E35C7 mov eax,0CCCCCCCCh013E35CC rep stos dword ptr es:[edi]013E35CE mov eax,dword ptr [a]013E35D1 add eax,dword ptr [b]013E35D4 pop edi013E35D5 pop esi013E35D6 pop ebx013E35D7 mov esp,ebp013E35D9 pop ebp013E35DA ret//func函数执行完毕后返回主函数中013E13D9 add esp,8013E13DC mov dword ptr [z],eax013E13DF xor eax,eax013E13E1 pop edi013E13E2 pop esi013E13E3 pop ebx013E13E4 add esp,0E4h013E13EA cmp ebp,esp013E13EC call @ILT+315(__RTC_CheckEsp) (13E1140h)013E13F1 mov esp,ebp013E13F3 pop ebp013E13F4 ret接下来再看看各个寄存器都是怎么变的,里面存的什么东西1 首先进入main函数最开始:再看看此时的寄存器的内容:EAX = 00851D88 EBX = 7E0BC000 ECX = 00854A30 EDX = 00000001 ESI = 00000000EDI = 00000000 EIP = 013E13A0 ESP = 009BF76C EBP = 009BF7B8 EFL = 000002022 再进行单步调试:再观察一下寄存器的内容:EAX = 00851D88 EBX = 7E0BC000 ECX = 00854A30 EDX = 00000001 ESI = 00000000EDI = 00000000 EIP = 013E13A1 ESP = 009BF768 EBP = 009BF7B8 EFL = 00000202我们发现esp的值少了4,这就说明了函数压栈时是从高字节到低字节递减,越是后压进来的越是字节低。
//target:从内存角度熟悉VC++面向对象机制作为MFC编程的基础//author:by Reduta//descritption:IA32 + win sp3 + vc6.0 /OD 1.10一:构造函数的之争1.1:构造函数是有返回值的——返回当前对象的this指针基本打开每一本C++的教程,都会对构造函数有如此类似的描述“构造函数无返回值,可进行函数重载”,但是事实上又如何呢?答案是构造函数具备返回值,返回值为当前对象的this指针。
编写如下代码://示例#include <iostream>using namespace std;class test{int a;public:test();void show();};test::test(){a=1;}void test::show(){cout<<a<<endl;}int main(){test hacker;//调用构造函数eax为构造函数返回值__asm{//借助返回值修改对象数据成员mov dword ptr ss:[eax],2}//查看修改是否成功hacker.show();return 0;}执行上面的程序,对象hacker的数据成员a的值将变为2,将程序载后OD,看关键代码:00401588 8D4D FC lea ecx,dword ptr ss:[ebp-4] ;hacker的this指针0040158B E8 AFFCFFFF call api.0040123F ;构造函数00401590 36:C700 02000000 mov dword ptr ss:[eax],2 ;修改对象的数据成员00401597 8D4D FC lea ecx,dword ptr ss:[ebp-4] ;hacker的this指针0040159A E8 0FFCFFFF call api.004011AE ;hacker的show函数执行到401590时,注意观察eax与ecx的值,如图1所示,eax和ecx的值是一样的,说明构造函数的确是有返回值,且返回值为当前对象的this指针,重新载入OD,在构造函数处,F7跟进,如图2所示,用红框标识的为关键代码,即构造函数,最后都会执行一句指令,将当前对象的this指针保存到eax中,而eax是函数的返回值。
c++虚函数实现原理
C++的虚函数实现原理是通过虚函数表(virtual table)来实现的。
在C++中,当一个类中声明了虚函数时,编译器会为该类生成一个虚函数表。
虚函数表是一个存储了虚函数地址的数组,每个虚函数在虚函数表中占据一个位置。
当一个类的对象被创建时,对象内部会包含一个指向该类的虚函数表的指针(通常被称为虚指针)。
这个指针会被初始化为指向该类的虚函数表。
当通过一个基类的指针或引用调用虚函数时,编译器会根据该指针或引用所指对象的虚指针找到对应的虚函数表,并通过偏移量找到该虚函数在虚函数表中的位置。
然后,通过该位置获取到实际需要调用的虚函数地址,最后通过函数指针调用该虚函数。
虚函数表的存在使得C++支持了运行时的多态性。
当通过一个基类的指针或引用调用虚函数时,实际调用的是派生类中的虚函数,这样就实现了多态的特性。
同时,虚函数表的使用也使得C++实现了动态绑定,即在运行时决定调用哪个虚函数的能力。
需要注意的是,虚函数表是针对每个类的,每个类都有自己的虚函数表,而不是针对每个对象的。
这意味着所有属于同一个类的对象共享同一个虚函数表。
总结起来,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++代码大有裨益!。