目录
- 一、相关寄存器和汇编命令的理解
- 1、认识相关寄存器的理解
- 2、认识相关汇编命令的理解
- 二、代码实例
- 三、函数栈帧的深度理解
- 1、main函数栈帧的形成
- 2、局部变量的开辟及相关寄存器的变化
- 3、函数调用前形成临时拷贝
- 4、函数的调用(形成函数栈帧)
- 5、释放栈帧
- 6、释放临时拷贝,彻底释放空间
一、相关寄存器和汇编命令的理解
1、认识相关寄存器的理解
EAX:通用寄存器,保存临时数据,常用于返回值;
EBX:通用寄存器,保存临时数据;
EBP:栈底寄存器,存放指向函数栈底的指针;
ESP:栈顶寄存器,存放指向函数栈顶的指针;
EIP:指令寄存器,保存当前指令的下一条指令的地址。
2、认识相关汇编命令的理解
mov:数据转移指令。
push:数据入栈,同时ESP栈顶寄存器也要发生改变(永远指向栈顶)。
push命令做两件事:
1、将push指定的的内容压入栈中(push进去,影响ESP的指向);
2、ESP寄存器的指向往低地址偏移,指向压栈后那块空间的栈顶。
pop:数据弹出至指定位置,同时ESP栈顶寄存器也要发生改变。
pop命令做两件事:
1、将EBP寄存器所指向的内存空间的内容拷贝进EBP寄存器中,(pop出来,影响ESP的指向);
2、ESP栈顶寄存器的指向往高地址偏移,指向弹栈后对应内存空间的栈顶。
sub:减法命令。
add:加法命令。
call:函数调用,1. 压入返回地址; 2. 转入目标函数。
call命令做三件事:
1.将当前要执行的call命令的下一条指令对应的地址入栈(push到栈中,同时影响ESP的指向);
2、ESP寄存器的指向往低地址偏移,指向压栈后的那块空间的栈顶;
3、跳转到目标函数起始地址运行(修改EIP,到达目标函数)。
jump:通过修改EIP,转入目标函数,进行调用。
ret:恢复返回地址,压入eip,类似pop EIP命令。
恢复返回地址,内存中的数据存放到EIP指令寄存器,然后ESP寄存器实现弹栈,即释放内存。
二、代码实例
本代码在main函数中创建三个局部变量并初始化,通过调用函数实现两数相加, 并返回相加后的结果,最终打印输出。
接下来从栈帧角度深度理解函数(栈帧)的形成,函数传参的本质,函数被调用过程,函数调用完释放函数(栈帧)过程,以及返回值如何存放等一系列问题。
三、函数栈帧的深度理解
注意:
讲解思路:由于使用的VS2017有栈随机化的处理,所以每次重新编译生成看到的相关数据可能不太一样,不过我们重点关注变化原理,弱化数据。
对main函数的反汇编语言进行分析前,我们先补充一些内容的讲解:
1、在函数的栈帧中,EBP和ESP这两个寄存器是存放地址的,通过这两个地址可以用来维护函数的栈帧;
2、ESP寄存器存放的是函数的栈顶地址,也就是栈顶指针;EBP寄存器存放的是函数的栈底地址,也就是栈底指针;
3、EIP寄存器存放的书执行当前指令的下一条指令的地址。
4、EAX寄存器存放的是临时数据,通过存放可以返回的数据。
>注意:上述讲解设计到的地址可被称之为指针,而指针可以理解为是“具有指向的数字”,即指针具有指向的作用;所以说,寄存器里面如果存放的是指针,那么这个寄存器也就具有指向的作用。
1、main函数栈帧的形成
(1)起步,main函数也要被调用的;
(2)main函数也是要形成栈帧结构的。
红色粗方框里的三条汇编代码就实现了在内存中形成main函数栈帧。其中:
1、让栈底寄存器中的内容(存放的是指针)进行push(压栈),同时进行一个操作,就是让栈顶寄存器的指向移动,重新指向新的栈顶,即栈顶寄存器也要同时发生改变;
2、把栈顶寄存器的内容(存放的是具有指向的数字(指针))拷贝一份到栈底寄存器(EBP)的内容中,此时栈底寄存器(EBP)原先的内容被覆盖,栈顶寄存器(ESP)和栈底寄存器(EBP)保存的内容(指针)相同,并且指向同一栈顶;
3、让栈顶寄存器(ESP)减去数值0E4h(十六进制)后的值拷贝给栈顶寄存器,最终就在内存中形成了main函数栈帧。
注意:减多少是由编译器决定的。因为定义变量都有确定的类型,编译器编译期间根据定义的变量类型的大小,结合自身需求来开辟空间数。
2、局部变量的开辟及相关寄存器的变化
接下来就是在main函数栈帧中,开辟并初始化局部变量及此过程中相关寄存器的变化:
1、将栈底寄存器的内容减8个字节,即栈底寄存器的指向往低地址处偏移8个字节后,由该低地址开始往高地址方向开辟4个字节(int类型)并将0Ah数据按小端形式放入内存中;实现局部变量a的开辟并初始化;
2、从main函数栈底指向开始,将栈底寄存器的内容减14个字节,即栈底寄存器的指向往低地址处偏移14个字节后,由该低地址开始往高地址方向开辟4个字节(int类型)并将0Bh数据按小端形式放入内存中;实现局部变量b的开辟并初始化;
3、从main函数栈底指向开始,将栈底寄存器的内容减20个字节,即栈底寄存器的指向往低地址处偏移20个字节后,由该低地址开始往高地址方向开辟4个字节(int类型)并将0数据按小端形式放入内存中;实现局部变量c的开辟并初始化;
注意:1、此过程虽然让栈低寄存器往低地址偏移对应字节数,但此运算操作是在CPU内部进行的,仅仅是计算而已,没有把计算最终值赋值给栈底寄存器,故此时栈底寄存器保存的内容不变,即栈底寄存器的指向没有发生变化,还是指向main函数栈底。
2、在调试过程中,按键盘的F11每进行一次指令操作,EIP指令寄存器的内容会发生改变,存放的内容为当前执行的指令的下一条指令的地址;然后main函数栈帧最终会根据压栈的顺序,从高地址–>低地址依次创建a、b、c三个局部变量;
3、函数调用前形成临时拷贝
调用函数时前先对形参实例化。
实例化形参,必定形成临时拷贝。
通过栈底寄存器指向变量b,将获取到的变量b的内容保存到通用寄存器eax里面,然后通过push汇编命令将通用寄存器里的内容进行压栈,同时栈顶寄存器的指向移动,指向新的栈顶;
通过栈底寄存器指向变量a,将获取到的变量a的内容保存到通用寄存器ecx里面,然后通过push汇编命令将通用寄存器里的内容进行压栈,同时栈顶寄存器的指向移动,指向新的栈顶;
>注意:
1、此过程在函数调用,需要传参时,对数据的临时拷贝;是在函数开始调用前完成的;由压栈的顺序可知,形参实例化是从右向左的。
2、压栈时不需要预留内存空间,每压栈一次,都是紧挨着的。
4、函数的调用(形成函数栈帧)
通过汇编命令call对函数进行调用,其过程如下:
开始调用,call做两件事:
1、将当前指令的下一条指令入栈,此时栈顶寄存器存放main函数中函数调用结束后执行下一条指令的地址,即00EF46FA;
2、通过修改EIP寄存器的内容,此时修改后EIP寄存器存放00EF11C7地址,让其执行当前指令时跳转到EIP寄存器存放指针对应地址处;
接着通过汇编命令jmp,将地址00EF16F0赋值到EIP寄存器中,执行当前执行时转入目标函数,进行调用;
函数调用完毕,接下来就是函数栈帧的形成,如下图所示:
Add函数栈帧的形成同main函数栈帧形成一致;形成函数栈帧后,进入函数执行语句代码块;在Add函数栈帧中开辟变量c并初始化为0,然后将变量a拷贝到通用寄存器EAX,通过加法指令add将EAX里的内容与变量b相加,再将EAX的值转移到c变量中,函数执行到retuen 值,是将此值先存放于CPU通用寄存器EAX中;
5、释放栈帧
函数调用执行结束后,返回到main函数,那么这个过程是怎样返回的,返回到mian函数的哪里?
可知,函数调用执行结束后,接下来就是对函数栈帧的释放,通过汇编代码可知,是将EBP寄存器拷贝到ESP寄存器,这一条汇编指令就实现了对函数栈帧的释放,然后执行pop指令,是将此时指向的内存里面的内容拷贝进EBP寄存器中,此内容为保存main函数栈底的地址,此时EBP寄存器重新指向main函数的栈底,同时ESP寄存器的指向跳过4个字节,即跳过保存此地址的空间,重新指向新的栈顶,此地址空间就被释放,实现弹栈,。
接下来执行ret汇编指令,是将此时ESP寄存器指向的内存里面的内容拷贝进EIP寄存器中,内容为main函数执行函数调用后执行下一条指令的地址,故通过此命令的执行,可从Add函数返回到main函数中,同时ESP寄存器的指向也要跳过保存此地址的空间,重新指向栈顶,此地址空间就被释放,实现弹栈。
6、释放临时拷贝,彻底释放空间
返回到mian函数执行函数调用后执行下一条指令的地址处,此时同时汇编命令add将ESP寄存器保存的地址加上8个字节,ESP寄存器的指向就往高地址方向偏移8个字节,实现将临时拷贝数据释放,这下就彻底释放空间;而保存在CPU通用寄存器EAX中的值赋值给main栈帧开辟的c变量中,最终打印输出。
>注意:空间的释放,是让ESP寄存器的不在指向那块空间的栈顶,但是那块空间依然存在,只是我们无法访问到而已,在形成新的栈帧结构时,把之前内存中占用的那块空间给覆盖掉。
以上就是我对函数栈帧的理解,如有不恰当的的讲解,还请指定出来。