函数栈帧的创建和销毁(汇编角度)
标签: 函数栈帧的创建和销毁(汇编角度) C/C++博客 51CTO博客
2023-07-24 18:24:47 146浏览
知识回顾:
调用函数就会在栈上形成栈结构,栈整体是向地址减小方向增长的,每次调用一个函数就是形成栈帧的过程,返回函数就是释放栈帧的过程(释放不等于把空间清空,而是设置空间无效,意味着下次再重新调用函数是可以覆盖上次的栈帧结构)
局部变量的临时性:局部变量的空间开辟是在对应函数的栈帧结构内开辟的,函数返回时,栈帧结构被释放,变量也会被释放。临时变量是在对应函数栈帧内形成的,临时变量的临时性是因为栈帧是临时的。
学习栈帧前的准备工作
1.汇编相关知识
函数调用和CPU中的寄存器有很大的关系
注意:
- eax/ecx 保存返回值
- 可以通过ebp和esp指定一块内存的范围(栈帧就是ebp和esp指定的一块范围)
- eip本质上是用来衡量当前程序执行到什么位置
- 学习栈帧需要重点研究ebp esp eip三个寄存器
注意:
- mov:集开辟空间和将数据写入空间为一体的函数指令
- push:函数入栈 <---> pop:弹出
- jump 跳转 (eip指向执行代码的起始位置,jump代表往哪跳)本质是通过修改eip完成的
- ret 返回值 ret通过eax将返回值返回
2.样例代码演示
#include <stdio.h>
#include <windows.h>
int MyAdd(int a, int b)
{
int c = 0;
c = a + b;
return c;
}
int main()
{
int x = 0xA;
int y = 0xB;
int z = 0;
z = MyAdd(x, y);
printf("z=%x\n", z);
system("pause");//暂停一下才能看到输出
return 0;
}
注意:
- 进行调试时,打开内存窗口中地址由上到下依次递增(上面是低地址 下面是高地址),相对应着我们画图就是上面低地址下面高地址
- 栈整体是向地址减小方向增长的,即向上增长(箭头向上)
2.1.函数调用关系
F10调试进入main()调用处----> 打开调试 窗口 调用堆栈
看到了隐藏的几个调用==>说明了main()也是一个函数,也是要被调用的,main()被_tmainCRTStartup()调用
_tmainCRTStartup()这个函数又被mainCRTStartup()调用
总结:程序最开始的函数入口是从mainCRTStartup()开始,mainCRTStartup()调用_tmainCRTStartup(),而_tmainCRTStartup()又在内部调用main()。
有一个问题:mainCRTStartup()也是函数,它被那个调用?操作系统调用mainCRTStartup(),观察下图可以看到kernel32(32位操作系统)
以上说明:mian()被_tmainCRTStartup()调用,_tmainCRTStartup()被mainCRTStartup()调用,mainCRTStartup()被操作系统调用,简单点说就是main()也是被函数调用的
我们知道main()被函数调用有什么用,跟今天说的栈帧有什么联系?
调用一个函数就是形成栈帧的过程,所以main()被调用也会形成栈帧,在栈区可以画出main()的栈帧结构
2.2.样例函数调用图解
main()也是被函数调用的,main()内部调用MyAdd(),以MyAdd()为例研究栈帧的形成和释放,其他函数形成/释放栈帧同理
注意:栈帧结构是向地址减小方向整体增长的
总结:以上是为了让你理解函数调用形成的栈帧在栈区上的体现,栈区的图为什么是那么画的
函数栈帧的创建(形成)和销毁(释放)
1.初始工作
初始工作1:
F10调试反汇编:int x = 0xA; 下面那一句汇编是这行代码对应的汇编语言,它的意思是将0Ah(即0xA)放到x变量中 []中显示的是符号名x,没有显示地址
将显示符号名去除,显示的是 dword ptr [ebp-8],0Ah //ebp-8:ebp寄存器对应的相对位置
总结:在反汇编中去除符号名显示,只显示地址这个初始工作的完成对后续代码的分析很重要,我们主要是通过看汇编中的地址去分析如何画那个栈区栈帧结构图。
初始工作2
栈帧的开始和结束分别用ebp和esp表示,将栈帧结构的起始位置地址和结束位置地址分别存进寄存器ebp(栈底寄存器)和esp(栈顶寄存器),有了地址之后这两个寄存器ebp和esp就指向栈的一段区域,从而就可以衡量这段区域,eip中保存着main()的code,正在执行main()内部的代码
注意:
- 栈是向上增长的===>ebp(栈底寄存器)esp(栈顶寄存器) 上面的是顶,下面的是底
- esp和ebp中存放了地址之后,一般情况下都是不同的地址,比如说ESP=0x0053FB88 EBP = 0x0053FC54,中间隔开了一段距离,也就标识了一段区域
总结:这里专门将栈区放大来研究函数栈帧,用向上的箭头表示栈区整体往地址减小方向增长,画出了三个重要的寄存器,简单介绍了这3个寄存器中存放的内容,为后续详细说明函数的栈帧做准备,同时完成了栈帧结构图的初步绘制,后续分析过程中,不断增添内容完善这个图
2.代码分析
2.1.定义变量:
利用move指令完成xyz变量的开辟内存空间以及内容初始化
int x = 0xA;//定义变量并初始化
00ED1905 mov dword ptr [ebp-8],0Ah
//在main()中定义的变量对应的汇编语句是将0xA move到对应的
//由ebp寄存器所标定的地址-8byte定位的位置,然后再将0xA放到对应的栈帧当中
//一条汇编语句完成了两件事情:1.开辟空间 2.给空间赋初始值
栈区图:
ebp-8往上移动(上面是低地址),以ebp-8作为起始地址位置开辟了一个变量空间x存储0xA。
ebp-8是指向变量空间上面还是下面? C语言中定义变量开辟多个字节的空间,取地址取的是最小的地址(取地址取的是起始地址),即所有的变量的起始地址是最小的;而ebp-8是起始地址,所以ebp-8指向上面(上面是低地址,即最小的地址)
EIP:保存当前正在执行指令的下一条指令的地址,指向了00ED190C这个地址,00ED190C这个地址对应int y=0xB的汇编指令语句,此时该地址处的语句还没有执行,程序当前执行的是该地址前的上一条汇编指令语句int x = 0xA。
int y = 0xB;
00ED190C mov dword ptr [ebp-14h],0Bh
//ebp-14继续往上移动 再以ebp-14为起始地址创建一个y变量空间存储0xB
//ebp-14是起始地址(最小的) 所以esp-14指向变量空间的上面
注意:发现变量x和变量y,那个先定义(先入栈),那个地址高,创建变量的整体空间向地址减小的方向增长;变量x和变量y地址不一定是直接连续
int z = 0;
00ED1913 mov dword ptr [ebp-20h],0
//EIP=00ED1913
//寄存器EIP不断进行递增,线性依次进行代码的访问
//跳转函数 修改的是EIP的值
注意:x与y之间留有一点空间,但是y和z是没有间隔的:与编译器有关,栈随机化(留有预留空间,保证安全性)===>x,y,z在对应的栈帧结构中空间排布是随机的,有可能中间是有间隔的或者没有间隔。
2.2.调用函数
z = MyAdd(x, y);
00ED191A mov eax,dword ptr [ebp-14h]
//将[ebp-14h]的内容(即y的值:0xB)放到eax寄存器
00ED191D push eax
//把eax入栈 入栈===>考虑内存布局情况===>打开调试窗口的内存
//入栈===> 观察esp(栈顶)
00ED191E mov ecx,dword ptr [ebp-8]
//将[ebp-8]的内容(即x的值:0xA)放到ecx寄存器
00ED1921 push ecx
00ED1922 call 00ED11FE
00ED1927 add esp,8
00ED192A mov dword ptr [ebp-20h],eax
//一条C语言代码对应多条汇编语句
2.2.1.调用函数前的动作:
形成临时拷贝
00ED191A mov eax,dword ptr [ebp-14h]
00ED191D push eax
00ED191E mov ecx,dword ptr [ebp-8]
00ED1921 push ecx
1.先输入esp回车,地址栏显示esp地址,观察到寄存器EAX中已经放入了0xB(上一条指令执行完毕),EIP也修改了值
2 继续F10往下调试 观察发现:寄存器ESP所存的地址减少了4byte,即栈顶往上移动,寄存器ESP存放的新地址在内存中可观察到,存放的就是EAX中的0xB
3 继续F10往下调试 发现0xA存进了ECX中,EIP中的地址所指向的这条指令还没有被执行,当前程序执行的是0xA存进ECX。
4.继续F10往下调试 观察发现:ESP往上移动了4byte,ESP中存放的地址修改了,观察到新的地址在内存中所存放的就是ECX中的0xA
00ED191A mov eax,dword ptr [ebp-14h]
00ED191D push eax
00ED191E mov ecx,dword ptr [ebp-8]
00ED1921 push ecx
//这四句话形成了临时拷贝 临时拷贝的临时变量通过入栈方式形成的
//ebp栈底不变,esp栈顶一直在递增 动态增长 变量从右向左形成临时变量
总结:
- 临时变量的形成是在函数正式被调用之前就形成了的
- 形参实列化的顺序是从右向左的
- 通过入栈的方式,变量x,y之间是没有间隔的
2.2.2.正式调用函数1:
压入返回值地址到栈中,转入目标函数
00ED1922 call 00ED11FE
00ED1927 add esp,8
//call:函数调用 1.压入返回值 2.转入目标函数
//call后面跟的是地址,本质是修改EIP值 实现跳转
//不能只考虑跳出去,还要考虑返回(call完成了就返回)返回到call命令的下一条指令的地址处
//所以call命令所对应的返回值(地址)(call命令的下一条指令的地址)也要保存
//函数调用完成后就回归到返回值处继续执行
注意:
- 所有函数的函数名都可以充当它的地址
- 先压入返回值的根本原因是:函数是可能调用完毕的,就需要返回
- 返回值保存的内容是当前执行指令的下一条指令的地址,需要将返回的地址进行压栈保存(保存方式:入栈式保存)
F11调试执行了call指令后 esp修改为新的esp,新ESP所指向的内存空间里面存放着call指令下一条指令的地址,EIP中修改为call指令后面跟的地址,即jump指令前的地址 ,call指令完成了两件事:1.完成了返回值(地址)入栈 2.跳转到jump命令的起始地址
jump:通过修改eip,跳转转入目标函数,进行调用
00ED11FE jmp 00ED17A0 //MyAdd()的地址(入口)
F11调试,发现EIP的值修改为jump后跟的地址00ED17A0,跳转到00ED17A0,正式进入MyAdd()
总结:调用函数时需要将call指令的下一条指令的地址作为返回值入栈,通过修改EIP指向目标函数实现函数的跳转
以上:正式调用函数前的准备:形成形参拷贝;正式调用函数1:1.返回值入栈 2.通过修改eip,跳转转入目标函数,这里还没有正式使用函数可以理解为完成了调用函数的第一个字调。
2.2.3.正式调用函数2:MyAdd()
2.2.3.1.MyAdd()栈帧的形成
00ED17A0 push ebp
00ED17A1 mov ebp,esp
00ED17A3 sub esp,0CCh //前三行是重点 函数栈帧的核心内容
//临时变量的初始化 区域清空
00ED17A9 push ebx
00ED17AA push esi
00ED17AB push edi
00ED17AC lea edi,[ebp-0Ch]
00ED17AF mov ecx,3
00ED17B4 mov eax,0CCCCCCCCh //内存空间初始化(与编译器有关)
00ED17B9 rep stos dword ptr es:[edi]
00ED17BB mov ecx,0EDC0A2h
00ED17C0 call 00ED1334
前三行分析
00ED17A0 push ebp //把ebp中的内容入栈
注意:
- ebp的内容是main()栈帧的栈底地址
- push:压入数据,修改esp栈顶的指向
观察可知esp往上移动,esp所指向的内存空间中存放着ebp的地址,即将ebp地址入栈,压入栈顶
00ED17A1 mov ebp,esp //将esp的内容放入到ebp里面
注意:
- ebp,esp都是cpu中的寄存器,都有存储空间和数据
- esp的内容是main()的栈顶(地址),将esp的内容放入到ebp里面,即将main()的栈顶(拷贝)放入到ebp里面
- 将esp的内容拷贝到ebp中,ebp原来的内容会被覆盖
- 拷贝过程中,没有访问内存,就在cpu内部两个寄存器之间进行数据的拷贝
- 这里栈底、栈顶都是指地址
观察可知:esp和ebp内容一样,都指向了栈顶
00ED17A3 sub esp,0CCh
//sub esp和0CCh两个值相减 将结果放入esp
//sub 减 将esp栈顶对应的地址-0CCh
//减多大与当前函数规模有关
//函数内部定义太多变量就减的多,函数空间小就减的小
esp值减小了0CCh ,esp向上移动,不再指向原来的栈顶
====>形成了一段新的区域空间:MyAdd()的栈帧
注意:
- 函数的栈帧是自己形成的(前面形成函数栈帧的汇编代码是编译器自己形成的,不是我们写的)
- sub esp,0CCh,这里让esp减多少由编译器决定的,编译器根据函数内部决定形成栈帧的大小(C语言定义变量都是要有类型的;sizeof求类型的大小,在编译时确定空间大小===>编译器有能力知道所有类型对应定义变量的大小===>编译器确定开辟栈帧大小:确定在{}内的定义变量的个数去计算出空间大小+自身需求====>确定一个合适栈帧大小)
总结:MyAdd()栈帧的创建:1.ebp值(地址)压栈(压栈是压入内存中的栈区) 2.将ebp原来值(地址)赋为esp值(地址),即ebp和esp都指向原来栈顶位置 3.将esp的地址减小,形成新栈顶,ebp所指向的位置和esp所指向的位置之间形成了一段新的区域空间,即MyAdd()的栈帧。
2.2.3.2.变量创建
int c = 0;
00ED17C5 mov dword ptr [ebp-8],0 //将0 move到ebp-8的位置
注意:临时变量c的形成是在MyAdd()的栈帧中形成的
2.2.3.3.完成加法功能
c = a + b;
00ED17CC mov eax,dword ptr [ebp+8]
//将ebp+8指向的内容放到eax中
00ED17CF add eax,dword ptr [ebp+0Ch]
//将eax的内容和ebp+0Ch指向的内容相加,结果放入eax
//即eax存放了0xA+0xB的结果
00ED17D2 mov dword ptr [ebp-8],eax
//将eax的内容放入ebp-8指向的临时变量c
注意:访问是从低地址(起始地址)向高地址访问
ebp+8==>0xA 即ebp+8指向了临时变量a,a的值为0xA,执行第一条指令,将a变量的值存入eax寄存器
ebp+12==>0xB 即ebp+12指向了临时变量b,b的值为0xB ,执行第二条指令,把变量b的值和eax存的值相加的结果再存入eax 即0xA+0xB结果存入eax,即10+11=21===>0x15
将eax结果写入到ebp-8所指向的空间中,即变量c中
2.2.3.4.返回
return c;
00ED17D5 mov eax,dword ptr [ebp-8]
//保存返回值 将ebp-8指向的变量c的内容0x15存入eax
注意:函数的返回值是通过eax/ecx等通用寄存器返回的
2.2.3.5.MyAdd函数栈帧及其他空间的释放
00ED17D8 pop edi
00ED17D9 pop esi
00ED17DA pop ebx
//pop<--->push是相对应的
//pop弹栈至指定的位置 esp栈顶寄存器要发生变化
//pop出栈<--->push压栈
00ED17DB add esp,0CCh
00ED17E1 cmp ebp,esp
00ED17E3 call 00ED1253
00ED17E8 mov esp,ebp
//把ebp的内容(地址)放到esp中 让ebp和esp指向同一位置(MyAdd()的栈底)
//这一条汇编基本完成了函数栈帧的释放的动作(栈结构空间被释放,数据没有被清空)
00ED17EA pop ebp
//弹栈pop:1.栈顶指向的内容:main()的栈底地址pop到ebp中2.栈顶esp要增大,即向下移动
00ED17EB ret
后三行分析
00ED17E8 mov esp,ebp
当ebp和esp指向同一位置,栈顶指向的内容是什么?栈顶指向的内容是main()的栈底地址
00ED17EA pop ebp
注意:返回的本质:1.返回到main()的栈帧 2.返回到main()的代码处
将esp指向的内容0x0053FD54存放到ebp中,同时esp增加4个字节,即esp向下移动
00ED17EB ret
//ret:1.恢复返回值 2.压入eip 即将返回地址压入eip 类似pop eip
//当前栈顶指向空间的内容是main的返回值
//ret将这个返回值恢复到eip,同时将esp向下移动
EIP中存放了返回值,返回到main()调用函数的下一条指令,同时esp继续增大4字节,即esp往下移
2.2.3.6.返回主函数
00ED1927 add esp,8
//esp+8即esp往下移动8
00ED192A mov dword ptr [ebp-20h],eax
//将eax存放的数值存放到ebp-20所指向的变量z的空间
输入&z查看z变量的内容被改成了0x15
注意:
- 栈帧的形成和释放本质上是通过若干寄存器去完成的
- 实参实例化形成的临时变量在main()栈帧和Myadd()栈帧之间形成的
- push进入的在空间上是连续的
int MyAdd(int a, int b)
{
printf("Before:%d",b);//11
*(&a+1)=100;
printf("After:%d",b);//100
return 0;
}
//a,b都是push进入栈空间的,所有a,b在空间是连续的,通过a的地址可以找到b并修改
//a,b之间的相对位置是确定的
利用相对位置修改返回值
void bug()
{
printf("you can see me!\n");
Sleep(10000);//加上这个延迟10s报错
}
int MyAdd(int a, int b)
{
printf("MyAdd can be called!\n");
*(&a-1)=(int)bug;//所有的函数名都是地址
return 0;
}
//输出
//MyAdd can be called!
//you can see me!
//为什么会报错?:修改了返回值,函数回不来了,bug函数通过非正常渠道调用的
//当代码返回时,找不到main()返回值了
//改进:在bug函数中加上main()返回值(现在的vs安全性高,做不到篡改地址)
2.3.总结
- 调用函数,需要先形成临时拷贝,形成过程是从右向左的
- 临时空间的开辟,是在对应函数栈帧内部开辟的
- 函数调用完毕,栈帧结构被释放掉
- 临时变量具有临时性的本质:栈帧具有临时性
- 调用函数是有成本的,成本体现在时间和空间上,本质是形成和释放栈帧有成本
- 函数调用,因拷贝所形成的临时变量,变量和变量之间的位置关系是有规律的
好博客就要一起分享哦!分享海报
此处可发布评论
评论(0)展开评论
展开评论