目录
一、 基础知识掌握(针对本文)
1.寄存器的种类及功能
2. 汇编指令
二、简单了解内存管理和函数栈帧
三、初步了解main函数的函数栈帧创建
1.创建一个简单的代码
2.main函数的创建过程
3.main函数的调用图解
四、main函数的栈前准备
1.push压栈操作
2.mov指令
3.sub指令
4.push - ebx、esi、edi指令
5.led、mov、mov、rep stos指令
五、正式执行有效代码
1.局部变量的创建以及值的问题
2.函数是如何传参的
3.Add函数的函数栈帧创建
4.形参与实参之间的关系
5.函数是如何返回值的
1.pop出栈指令
2.函数值的返回
六、总结
1.eax:累加寄存器,相对于其他寄存器,在运算方面比较常用。
2.ebx:基地址寄存器,作为内存偏移指针使用。
3.ecx:计数器,用于特定的技术。
4.edx:作为EAX的溢出寄存器,(除法产生的余数)。
5.esp:指针的寄存器,用于堆栈操作。又称为栈顶指针,堆栈的顶部是地址小的区域,压入堆栈的数据越多,esp也就越来越小。
在32位平台上,esp每次减少4字节。
6.ebp:基址指针,指栈的栈底指针。7.esi:在内存操作指令中作为“源地址指针”使用。
8.edi:在内存操作指令中作为“目的地址”使用。
1.push 指令:入栈指令,将源操作数指定的字数据压入堆栈栈顶,在32位平台上,esp每次减少4字节。
2.mov 指令:将源操作数送至目的操作数。
3.pop 指令:出栈指令,将源操作数指定的字数据压入堆栈栈顶,在32位平台上,esp每次增加4字节。
4.jmp 指令:跳转至指定地址执行指令。
5.lea 指令:全称(Load effective address),加载有效地址(偏移地址)至寄存器(如edi)。
6.call 指令:将程序的执行交给其他代码段,执行结束后会返回到call指令的下一行指令。
7.ret 指令:子程序的返回指令。
8.add 指令:加法指令。
9.sub 指令:减法指令。
10.rep 指令:重复前缀指令,不可独立使用
形式:位于stos、lod、ins、outs等传送指令之前,如 rep stos dword ptr es:[edi]
11.stos 指令:传输指令,可独立使用,它是将eax(寄存器)中的值传送到指定单元;
例如:rep stos dword ptr es:[edi] :将eax中的值拷贝到es:[edi]指向的地址
本次我们主要探讨栈区,函数在调用的过程中都是在栈区上开辟一块属于自己的空间的,这块空间是由两个寄存器esp和ebp来维护的,这块空间我们称为函数栈帧;
本篇文章主要探究以下几个问题:
1.局部变量是如何创建的?
2.为什么局部变量是随机值?
3.函数是如何传参的?
4.形参与实参之间的关系?
5.函数调用是如何进行的?
6.函数是如何返回值的?
编译器的选择: 尽量选低版本的编译器,高版本的编译器封装性比较好,难以看到效果;我选用的是VS2013
在VS2013下创建如下代码,来对函数栈帧的创建和销毁进行演示
#define _CRT_SECURE_NO_WARNINGS 1
#include
int Add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 10;
int b = 20;
int c = 0;
c = Add(a, b);
printf("%d\n", c);
return 0;
}
按F10进行代码调试,在菜单栏(调试)中找到调用堆栈,逐步走完mian函数(不需要进入Add函数),就会出现以下代码,虽然大部分我们都看不懂,但是我们对于函数的调用关系还是能够找出来的,看下图的具体分析;
此时此刻栈区已经创建好__tmainCRTStartup函数的函数栈帧,为调用main函数做好准备了,同时有两个指针来维护这块空间(ebp-栈底指针 和 esp-栈顶指针);
接下来main函数开始调用,就需要在此基础上开辟空间,称为压栈;因为栈区的使用特点就是先使用高地址,再使用低地址;就好比盖楼房,先打地基,再一步一步的往上盖;
不要眨眼,请仔细看接下来的操作:
00DE1410 push ebp
重新调试,进入C语言的反汇编代码(在代码区直接右键鼠标就可以找到),此时是还没执行任何操的情况,从内存中我们获取到esp(栈顶指针)和ebp(栈底指针)的地址;
执行push操作:push ebp --- 将ebp压栈(把ebp放到已经创建好的函数栈帧上),此时esp会自动的去维护栈顶,那么相应的其地址也会发变小,上图中esp的地址是0x00cffb34,执行push后地址变为0x00cffb30。由下表可以看出ebp确实被压入栈中了,esp现在所指向的就是ebp;
00DE1411 mov ebp,esp
move指令:是将源操作数复制到目的操作数中
此时ebp就指向了栈顶所指的位置
00DE1413 sub esp,0E4h
sub指令---就是减法(将esp - 0E4h(16进制数))
当esp减去0E4h之后,地址变小了,将会指向上面的空间,此时esp和ebp维护的是紫色的空间,也就是为main函数预开辟的空间,main函数终于等到老弟(__tmainCRTStartup)的消息了,如图所示;
00DE1419 push ebx
00DE141A push esi
00DE141B push edi
这里对于ebx、esi、edi不做详细的解释,在本文最后会加以注释;
压栈压入3个元素后,esp的地址又变小了,也确实从下图可以看出这三个元素的的确确被压进去了,也许从刚才的几张图来看,不是很能理解为什么esp会自动,但是结合下图和前几张图来看,就很容易的理解了,本人的理解就是压一个元素就要esp来维护;
这三个元素在函数返回的时候会自动弹出去,切不要在这三个寄存器上多考虑;
00DE141C lea edi,[ebp-0E4h]
00DE1422 mov ecx,39h
00DE1427 mov eax,0CCCCCCCCh
00DE142C rep stos dword ptr es:[edi]
1.lea edi,[ebp-0E4h] --- 加载有效地址:edi可以指向ebp - 0E4h这个位置
2. mov ecx,39h --- 把39h这个值存放到ecx寄存器中
3.mov eax,0CCCCCCCCh --- 把0CCCCCCCCh这个值存放到eax寄存器中
4.rep stos dword ptr es:[edi] --- 从edi这个位置开始向下39h次个内容初始化为eax的内容(0CCCCCCCCh)
具体如下图所示:
局部变量的创建就是建立在栈帧已经创建好的基础上,去找到一些空间来存放这些局部变量,当局部变量未初始化时,里面默认的是0CCCCCCCCh这些值,所以就有了我们在编写代码时未初始化变量,导致打印出来是随机值;
下图是局部变量在栈区中存放的位置,不是每个编译器都按照这种方式存储,有可能是紧挨着存储,也有可能随机存,但一定在已经开辟好的栈区空间中,这里数据的存储是小端存储,后续会给大家介绍数据的存储;
int a = 10;
00DE142E mov dword ptr [ebp-8],0Ah
int b = 20;
00DE1435 mov dword ptr [ebp-14h],14h
int c = 0;
00DE143C mov dword ptr [ebp-20h],0
下图中这几段代码是函数传参;可以看出,在传参时,先传b再传a;
00DE13C0 push ebp 00DE13C1 mov ebp,esp 00DE13C3 sub esp,0CCh 00DE13C9 push ebx 00DE13CA push esi 00DE13CB push edi 00DE13CC lea edi,[ebp-0CCh] 00DE13D2 mov ecx,33h 00DE13D7 mov eax,0CCCCCCCCh 00DE13DC rep stos dword ptr es:[edi]
这部分的代码和刚刚main函数创建的函数栈帧是一样的,这里就不一步一步的给大家解释了,仿照main函数的函数栈帧创建过程也能够画出下面的图解;
仔细观察下面的图,就可以发现,函数在传参的过程中是先将b的值存到eax这个寄存器里,然后压栈,把a的值存到exc这个寄存器里,然后压栈,在调用这些值进行运算的时候,是通过ebp加减的操作来获取这些值,并保存到eax寄存器里;而且变量x、y也没有在Add函数的栈帧中创建,通过ebp+8找到的值(可以理解为形参x)、ebp+12找到的值(可以理解为形参y),所以形参就是实参的一份临时拷贝,改变形参并不会影响实参;
pop --- 出栈:当执行这个指令时,会将栈顶的元素弹出,并且esp(栈顶指针)会变大,也就是向高地址走
00DE13F1 pop edi
00DE13F2 pop esi
00DE13F3 pop ebx
ret这个代码,是用来返回值的,刚才计算好的z是存放在eax这个寄存器中的,它是由寄存器带回给主函数的,虽然Add函数栈帧销毁了,但是算好的值已经被保存了下来;
当走到这一步,此时的esp和ebp就开始维护main函数的函数栈帧了
00DE13F4 mov esp,ebp
00DE13F6 pop ebp
00DE13F7 ret
上面的操作都是在执行call的指令(就是调用Add),因为对Add的地址进行压栈的,弹出之后也就能找到了Add的地址(存这个地址就是为了函数在返回时能够找到原来的位置)也就能够继续执行下面的程序了,如下图;
下图是Add函数栈帧销毁后,返回值返回的操作
00DE144B call 00DE10E1
00DE1450 add esp,8
00DE1453 mov dword ptr [ebp-20h],eax
以上是main函数创建及Add函数的创建和销毁,main函数的销毁就不往下画了,大致的步骤都是一样的,笔者也是花了一点时间一步一步的操作,注释。希望可以给大家带来帮助,不足之处希望大家指出。
总的来说,细细的去感受函数栈帧,确实是挺快乐的,你会发现很多奇妙的事情。