函数栈帧的创建和销毁(汇编角度)

奋斗吧
奋斗吧
擅长邻域:未填写

标签: 函数栈帧的创建和销毁(汇编角度) C/C++博客 51CTO博客

2023-07-24 18:24:47 69浏览

函数栈帧的创建和销毁(汇编角度),知识回顾:调用函数就会在栈上形成栈结构,栈整体是向地址减小方向增长的,每次调用一个函数就是形成栈帧的过程,返回函数就是释放栈帧的过程(释放不等于把空间清空,而是设置空间无效,意味着下次再重新调用函数是可以覆盖上次的栈帧结构)局部变量的临时性:局部变量的空间开辟是在对应函数的栈帧结构内开辟的,函数返回时,栈帧结构被释放,变量也会被释放。临时变量是在对应函数栈帧内形成的,临时变量的临时性是因为栈帧是临

知识回顾:

调用函数就会在栈上形成栈结构,栈整体是向地址减小方向增长的,每次调用一个函数就是形成栈帧的过程,返回函数就是释放栈帧的过程(释放不等于把空间清空,而是设置空间无效,意味着下次再重新调用函数是可以覆盖上次的栈帧结构)

局部变量的临时性:局部变量的空间开辟是在对应函数的栈帧结构内开辟的,函数返回时,栈帧结构被释放,变量也会被释放。临时变量是在对应函数栈帧内形成的,临时变量的临时性是因为栈帧是临时的。

学习栈帧前的准备工作

1.汇编相关知识

函数调用和CPU中的寄存器有很大的关系

函数栈帧的创建和销毁(汇编角度)_函数栈帧的形成和释放

注意:

  1. eax/ecx 保存返回值   
  2. 可以通过ebpesp指定一块内存的范围(栈帧就是ebp和esp指定的一块范围)   
  3. eip本质上是用来衡量当前程序执行到什么位置
  4. 学习栈帧需要重点研究ebp esp eip三个寄存器

函数栈帧的创建和销毁(汇编角度)_栈帧结构图分析_02

注意:

  1. mov:集开辟空间和将数据写入空间为一体的函数指令
  2. push:函数入栈   <--->   pop:弹出
  3. jump 跳转 (eip指向执行代码的起始位置,jump代表往哪跳)本质是通过修改eip完成的
  4. 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;
}

函数栈帧的创建和销毁(汇编角度)_汇编角度_03

注意:

  1. 进行调试时,打开内存窗口中地址由上到下依次递增(上面是低地址 下面是高地址),相对应着我们画图就是上面低地址下面高地址
  2. 栈整体是向地址减小方向增长的,即向上增长(箭头向上)

2.1.函数调用关系

F10调试进入main()调用处----> 打开调试 窗口 调用堆栈

函数栈帧的创建和销毁(汇编角度)_汇编角度_04

看到了隐藏的几个调用==>说明了main()也是一个函数,也是要被调用的,main()被_tmainCRTStartup()调用

函数栈帧的创建和销毁(汇编角度)_函数栈帧的形成和释放_05

_tmainCRTStartup()这个函数又被mainCRTStartup()调用

函数栈帧的创建和销毁(汇编角度)_栈帧结构图分析_06

总结:程序最开始的函数入口是从mainCRTStartup()开始,mainCRTStartup()调用_tmainCRTStartup(),而_tmainCRTStartup()又在内部调用main()。

有一个问题:mainCRTStartup()也是函数,它被那个调用?操作系统调用mainCRTStartup(),观察下图可以看到kernel32(32位操作系统)

函数栈帧的创建和销毁(汇编角度)_汇编角度_07

以上说明:mian()被_tmainCRTStartup()调用,_tmainCRTStartup()被mainCRTStartup()调用,mainCRTStartup()被操作系统调用,简单点说就是main()也是被函数调用的

我们知道main()被函数调用有什么用,跟今天说的栈帧有什么联系?

调用一个函数就是形成栈帧的过程,所以main()被调用也会形成栈帧,在栈区可以画出main()的栈帧结构


2.2.样例函数调用图解

main()也是被函数调用的,main()内部调用MyAdd(),以MyAdd()为例研究栈帧的形成和释放,其他函数形成/释放栈帧同理

函数栈帧的创建和销毁(汇编角度)_栈帧结构图分析_08


注意:栈帧结构是向地址减小方向整体增长的

总结:以上是为了让你理解函数调用形成的栈帧在栈区上的体现,栈区的图为什么是那么画的

函数栈帧的创建(形成)和销毁(释放)

1.初始工作

初始工作1

F10调试反汇编:int x = 0xA; 下面那一句汇编是这行代码对应的汇编语言,它的意思是将0Ah(即0xA)放到x变量中  []中显示的是符号名x,没有显示地址

函数栈帧的创建和销毁(汇编角度)_汇编角度_09

显示符号名去除,显示的是  dword ptr [ebp-8],0Ah  //ebp-8:ebp寄存器对应的相对位置

函数栈帧的创建和销毁(汇编角度)_汇编角度_10

总结:在反汇编中去除符号名显示,只显示地址这个初始工作的完成对后续代码的分析很重要,我们主要是通过看汇编中的地址去分析如何画那个栈区栈帧结构图。

初始工作2

栈帧的开始和结束分别用ebp和esp表示,将栈帧结构的起始位置地址和结束位置地址分别存进寄存器ebp(栈底寄存器)和esp(栈顶寄存器),有了地址之后这两个寄存器ebp和esp就指向栈的一段区域,从而就可以衡量这段区域,eip中保存着main()的code,正在执行main()内部的代码

函数栈帧的创建和销毁(汇编角度)_汇编角度_11

注意:

  1. 栈是向上增长的===>ebp(栈底寄存器)esp(栈顶寄存器) 上面的是顶,下面的是底 
  2. 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.给空间赋初始值

栈区图:

函数栈帧的创建和销毁(汇编角度)_函数栈帧的形成和释放_12

ebp-8往上移动(上面是低地址),以ebp-8作为起始地址位置开辟了一个变量空间x存储0xA。

ebp-8是指向变量空间上面还是下面? C语言中定义变量开辟多个字节的空间,取地址取的是最小的地址(取地址取的是起始地址),即所有的变量的起始地址是最小的;而ebp-8是起始地址,所以ebp-8指向上面(上面是低地址,即最小的地址)

函数栈帧的创建和销毁(汇编角度)_函数栈帧的形成和释放_13

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的值

函数栈帧的创建和销毁(汇编角度)_栈帧结构图分析_14

注意:x与y之间留有一点空间,但是y和z是没有间隔的:与编译器有关,栈随机化(留有预留空间,保证安全性)===>x,y,z在对应的栈帧结构中空间排布是随机的,有可能中间是有间隔的或者没有间隔。

函数栈帧的创建和销毁(汇编角度)_函数栈帧的形成和释放_15

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也修改了值

函数栈帧的创建和销毁(汇编角度)_汇编角度_16

2 继续F10往下调试 观察发现:寄存器ESP所存的地址减少了4byte,即栈顶往上移动,寄存器ESP存放的新地址在内存中可观察到,存放的就是EAX中的0xB

函数栈帧的创建和销毁(汇编角度)_栈帧结构图分析_17

3 继续F10往下调试  发现0xA存进了ECX中,EIP中的地址所指向的这条指令还没有被执行,当前程序执行的是0xA存进ECX。

函数栈帧的创建和销毁(汇编角度)_汇编角度_18

4.继续F10往下调试 观察发现:ESP往上移动了4byte,ESP中存放的地址修改了,观察到新的地址在内存中所存放的就是ECX中的0xA

函数栈帧的创建和销毁(汇编角度)_汇编角度_19

函数栈帧的创建和销毁(汇编角度)_栈帧结构图分析_20

00ED191A  mov         eax,dword ptr [ebp-14h]  
00ED191D  push        eax  
00ED191E  mov         ecx,dword ptr [ebp-8]  
00ED1921  push        ecx  
//这四句话形成了临时拷贝 临时拷贝的临时变量通过入栈方式形成的
//ebp栈底不变,esp栈顶一直在递增 动态增长 变量从右向左形成临时变量

总结:

  1. 临时变量的形成是在函数正式被调用之前就形成了的
  2. 形参实列化的顺序是从右向左的
  3. 通过入栈的方式,变量x,y之间是没有间隔的
2.2.2.正式调用函数1:

压入返回值地址到栈中,转入目标函数

00ED1922  call        00ED11FE 
00ED1927  add         esp,8  
//call:函数调用  1.压入返回值 2.转入目标函数
//call后面跟的是地址,本质是修改EIP值  实现跳转
//不能只考虑跳出去,还要考虑返回(call完成了就返回)返回到call命令的下一条指令的地址处
//所以call命令所对应的返回值(地址)(call命令的下一条指令的地址)也要保存
//函数调用完成后就回归到返回值处继续执行

注意:

  1. 所有函数的函数名都可以充当它的地址
  2. 先压入返回值的根本原因是:函数是可能调用完毕的,就需要返回
  3. 返回值保存的内容是当前执行指令的下一条指令的地址,需要将返回的地址进行压栈保存(保存方式:入栈式保存)

F11调试执行了call指令后 esp修改为新的esp,新ESP所指向的内存空间里面存放着call指令下一条指令的地址,EIP中修改为call指令后面跟的地址,即jump指令前的地址 ,call指令完成了两件事:1.完成了返回值(地址)入栈    2.跳转到jump命令的起始地址

函数栈帧的创建和销毁(汇编角度)_函数栈帧的形成和释放_21

jump:通过修改eip,跳转转入目标函数,进行调用

00ED11FE  jmp         00ED17A0   //MyAdd()的地址(入口)

F11调试,发现EIP的值修改为jump后跟的地址00ED17A0,跳转到00ED17A0,正式进入MyAdd()

函数栈帧的创建和销毁(汇编角度)_函数栈帧的形成和释放_22

函数栈帧的创建和销毁(汇编角度)_栈帧结构图分析_23

总结:调用函数时需要将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中的内容入栈

注意:

  1. ebp的内容是main()栈帧的栈底地址
  2. push:压入数据,修改esp栈顶的指向

观察可知esp往上移动,esp所指向的内存空间中存放着ebp的地址,即将ebp地址入栈,压入栈顶

函数栈帧的创建和销毁(汇编角度)_汇编角度_24

00ED17A1  mov         ebp,esp  //将esp的内容放入到ebp里面

注意:

  1. ebp,esp都是cpu中的寄存器,都有存储空间和数据
  2. esp的内容是main()的栈顶(地址),将esp的内容放入到ebp里面,即将main()的栈顶(拷贝)放入到ebp里面
  3. 将esp的内容拷贝到ebp中,ebp原来的内容会被覆盖
  4. 拷贝过程中,没有访问内存,就在cpu内部两个寄存器之间进行数据的拷贝
  5. 这里栈底、栈顶都是指地址

观察可知:esp和ebp内容一样,都指向了栈顶

函数栈帧的创建和销毁(汇编角度)_函数栈帧的形成和释放_25

函数栈帧的创建和销毁(汇编角度)_汇编角度_26

00ED17A3  sub         esp,0CCh 
//sub esp和0CCh两个值相减 将结果放入esp
//sub 减  将esp栈顶对应的地址-0CCh 
//减多大与当前函数规模有关 
//函数内部定义太多变量就减的多,函数空间小就减的小

esp值减小了0CCh ,esp向上移动,不再指向原来的栈顶

函数栈帧的创建和销毁(汇编角度)_栈帧结构图分析_27

====>形成了一段新的区域空间:MyAdd()的栈帧

函数栈帧的创建和销毁(汇编角度)_栈帧结构图分析_28

注意:

  1. 函数的栈帧是自己形成的(前面形成函数栈帧的汇编代码是编译器自己形成的,不是我们写的)
  2. 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()的栈帧中形成的

函数栈帧的创建和销毁(汇编角度)_栈帧结构图分析_29

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寄存器

函数栈帧的创建和销毁(汇编角度)_栈帧结构图分析_30

ebp+12==>0xB  即ebp+12指向了临时变量b,b的值为0xB  ,执行第二条指令,把变量b的值和eax存的值相加的结果再存入eax 即0xA+0xB结果存入eax,即10+11=21===>0x15

函数栈帧的创建和销毁(汇编角度)_栈帧结构图分析_31

将eax结果写入到ebp-8所指向的空间中,即变量c中

函数栈帧的创建和销毁(汇编角度)_栈帧结构图分析_32

函数栈帧的创建和销毁(汇编角度)_函数栈帧的形成和释放_33

2.2.3.4.返回
return c;
00ED17D5  mov         eax,dword ptr [ebp-8]
//保存返回值 将ebp-8指向的变量c的内容0x15存入eax

注意:函数的返回值是通过eax/ecx等通用寄存器返回的

函数栈帧的创建和销毁(汇编角度)_函数栈帧的形成和释放_34

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()的栈底地址

函数栈帧的创建和销毁(汇编角度)_栈帧结构图分析_35

函数栈帧的创建和销毁(汇编角度)_函数栈帧的形成和释放_36

00ED17EA  pop         ebp  

注意:返回的本质:1.返回到main()的栈帧 2.返回到main()的代码处

将esp指向的内容0x0053FD54存放到ebp中,同时esp增加4个字节,即esp向下移动

函数栈帧的创建和销毁(汇编角度)_栈帧结构图分析_37

函数栈帧的创建和销毁(汇编角度)_栈帧结构图分析_38

00ED17EB  ret  
//ret:1.恢复返回值 2.压入eip  即将返回地址压入eip  类似pop eip
//当前栈顶指向空间的内容是main的返回值
//ret将这个返回值恢复到eip,同时将esp向下移动

EIP中存放了返回值,返回到main()调用函数的下一条指令,同时esp继续增大4字节,即esp往下移

2.2.3.6.返回主函数

函数栈帧的创建和销毁(汇编角度)_汇编角度_39

函数栈帧的创建和销毁(汇编角度)_汇编角度_40

00ED1927  add         esp,8  
//esp+8即esp往下移动8

函数栈帧的创建和销毁(汇编角度)_栈帧结构图分析_41

函数栈帧的创建和销毁(汇编角度)_栈帧结构图分析_42

00ED192A  mov         dword ptr [ebp-20h],eax  
//将eax存放的数值存放到ebp-20所指向的变量z的空间

输入&z查看z变量的内容被改成了0x15

函数栈帧的创建和销毁(汇编角度)_栈帧结构图分析_43

函数栈帧的创建和销毁(汇编角度)_栈帧结构图分析_44

注意:

  1. 栈帧的形成和释放本质上是通过若干寄存器去完成的
  2. 实参实例化形成的临时变量在main()栈帧和Myadd()栈帧之间形成的
  3. 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.总结

  1. 调用函数,需要先形成临时拷贝,形成过程是从右向左的
  2. 临时空间的开辟,是在对应函数栈帧内部开辟的
  3. 函数调用完毕,栈帧结构被释放掉
  4. 临时变量具有临时性的本质:栈帧具有临时性
  5. 调用函数是有成本的,成本体现在时间和空间上,本质是形成和释放栈帧有成本
  6. 函数调用,因拷贝所形成的临时变量,变量和变量之间的位置关系是有规律的

好博客就要一起分享哦!分享海报

此处可发布评论

评论(0展开评论

暂无评论,快来写一下吧

展开评论

您可能感兴趣的博客

客服QQ 1913284695