> 文档中心 > 汇编视角观察函数栈帧的创建和销毁(动图详解)

汇编视角观察函数栈帧的创建和销毁(动图详解)


目录

​1、阅读本文的价值

​2、函数栈帧及栈的概念

 ​3、部分寄存器及汇编指令

​4、main函数的调用

5、main函数的栈帧创建

​6、变量的栈帧创建

​6、函数传参

​7、函数内部运算及销毁

​8、通过函数栈帧引发的思考

​1、局部变量是如何创建的?

​2、 为什么局部变量不初始化是随机的?

​3、函数调用时参数时如何传递的?传参的顺序是怎样的?

4、 函数的形参和实参分别是怎样实例化的?

​5、 函数的返回值是如何带回的?


1、阅读本文的价值

话不多说先上图!

先从某招聘网站随便扒了两张岗位JD。由于博主在宁波,实体经济还是比较强的【外边真的全是嵌入式

那么面试官就会问了,小伙子你了解过汇编么?

我曾从汇编的角度描述过函数栈帧的创建与销毁!(本篇的意义在于了解汇编与深挖函数栈帧的创建与销毁,为后期理解栈区打好基础。)

2、函数栈帧及栈的概念

函数栈帧(stack frame)就是函数调用过程中在程序的调用栈(call stack)所开辟的空间,这些空间 是用来存放:

1、函数参数和函数返回值

2、临时变量(包括函数的非静态的局部变量以及编译器自动生产的其他临时变量)

3、保存上下文信息(包括在函数调用前后需要保持不变的寄存器)。

栈:用户可以将数据压入栈中(入栈,push),也可以将已经压入栈中的数据弹出(出栈,pop),但是栈这个容器必须遵守一条规则:先入后出(First In Last Out, FILO)。在计算机系统中,栈则是一个具有以上属性的动态内存区域。压栈操作使得栈增大,而弹出操作使得栈减小。 在经典的操作系统中,栈总是向下增长(由高地址向低地址)的。

 3、部分寄存器及汇编指令

1、部分寄存器

eax (通用寄存器)

通常用来执行加法,函数调用的返回值一般也放在这里面

ebx (通用寄存器)

保留临时数据

esp (通用寄存器)

栈顶寄存器,指向栈的顶部

ebp (通用寄存器)

栈底寄存器,指向栈的底部,通常用ebp+偏移量的形式来定位函数存放在栈中的局部变量

eip (指令寄存器)

最重要的寄存器,它指向了下一条要执行的指令所存放的地址

 2、部分汇编指令

mov (通用数据传送指令)

数据转移指令

push (通用数据传送指令)

数据入栈,同时esp栈顶寄存器也要发生改变

pop (通用数据传送指令)

数据弹出至指定位置,同时esp栈顶寄存器也要发生改变

sub (算术运算指令)

减法

add (算术运算指令)

加法

call (子程序调用指令)

函数调用1. 压入返回地址 2. 转入目标函数

jump(无条件程序转移指令)

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

ret (子程序或函数返回指令)

恢复返回地址,压入eip,类似pop eip命令

4、main函数的调用

本次以加法函数为例:

#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;}

通过调用堆栈可以发现,main()函数被static int __cdecl invoke_main()进行调用,从__cdecl中能看出C语言函数参数的压栈的顺序是从右向左压入栈中,例如C函数 Fun(a,b,c)函数调用时,参数压栈顺序为 c , b , a。

19b474fcf74a3fde800a0216235bb137.pngmain()函数结束时的return 0,返回至int const main_result中。

5、main函数的栈帧创建

1、main函数汇编代码

int main(){003A18B0  push ebp//在栈中压入ebp的值  003A18B1  mov  ebp,esp//把esp的值给ebp  003A18B3  sub  esp,0E4h//把esp的值减去0E4h  003A18B9  push ebx//在栈中压入ebx的值  003A18BA  push esi//在栈中压入esi的值  003A18BB  push edi//在栈中压入edi的值  003A18BC  lea  edi,[ebp-24h]//把ebp-24h放入edi中  003A18BF  mov  ecx,9//把9的值给ecx  003A18C4  mov  eax,0CCCCCCCCh//把0CCCCCCCCh放入eax  003A18C9  rep stos    dword ptr es:[edi]//从edi开始向下ecx的区域放入eax 

2、动图详解

此处注意0CCCCCCCCh是从地地址向高地址创建的。

6、变量的栈帧创建

1、变量的汇编代码

    int a = 10;003A18D5  mov  dword ptr [ebp-8],0Ah//在ebp-8位置处放入0Ah,即a的值  int b = 20;003A18DC  mov  dword ptr [ebp-14h],14h//在ebp-14h位置处放入14,即b的值    int c = 0;003A18E3  mov  dword ptr [ebp-20h],0//在ebp-20h位置处放入0,即c的值 

2、动图详解

cd0393bf48c36d46be2793c781e80f3f.gif

6、函数传参

1、传参的汇编代码

c = Add(a, b);003A18EA  mov  eax,dword ptr [ebp-14h]//把ebp-14h地址的值放入eax中  003A18ED  push eax//压入eax  003A18EE  mov  ecx,dword ptr [ebp-8]//把ebp-8地址的值放入ecx中    003A18F1  push ecx//压入ecx   003A18F2  call 003A10B4//调用add函数,栈顶保存call指令的下一条指令  003A18F7  add  esp,8//形参销毁  003A18FA  mov  dword ptr [ebp-20h],eax//形参销毁  

2、动图详解

注意传参时,形参是实参的一份临时拷贝,需要创建相同大小的空间用于传参,所以传值调用的效率往往不如传址调用。

7、函数内部运算及销毁

1、函数的汇编代码

int Add(int x, int y){003A1770  push ebp  003A1771  mov  ebp,esp  003A1773  sub  esp,0CCh  003A1779  push ebx  003A177A  push esi  003A177B  push edi  003A177C  lea  edi,[ebp-0Ch]  003A177F  mov  ecx,3  003A1784  mov  eax,0CCCCCCCCh  003A1789  rep stos    dword ptr es:[edi]  003A178B  mov  ecx,3AC008h  003A1790  call 003A131B  int z = 0;003A1795  mov  dword ptr [ebp-8],0  z = x + y;003A179C  mov  eax,dword ptr [ebp+8]  003A179F  add  eax,dword ptr [ebp+0Ch]  003A17A2  mov  dword ptr [ebp-8],eax  return z;003A17A5  mov  eax,dword ptr [ebp-8]//把ebp-8的值放到eax寄存器中,让寄存器把结果带出函数  }003A17A8  pop  edi//弹出edi,同时esp地址增加  003A17A9  pop  esi  003A17AA  pop  ebx  003A17AB  add  esp,0CCh  003A17B1  cmp  ebp,esp  003A17B3  call 003A1244  003A17B8  mov  esp,ebp  003A17BA  pop  ebp//通过ebp找回main的栈底  003A17BB  ret

2、动图详解

通过eax寄存器把函数计算结果带给c。

8、通过函数栈帧引发的思考

1、局部变量是如何创建的?

首先需要为函数分配栈帧空间,在函数的栈帧空间初始化完成后,再为局部变量分配空间。

2、 为什么局部变量不初始化是随机的?

因为在栈帧空间初始化的过程中,通过动图演示,可以看到栈帧空间的部分区域被初始化为0CCCCCCCCh,若局部变量不初始化,将会被赋值为这个值,这也是“烫烫烫”的成因。(全局变量不初始化为0)

3、函数调用时参数时如何传递的?传参的顺序是怎样的?

函数调用时,会现在main函数内压入形参,通过从右向左的压栈方式向函数传递形参。当函数内部需要使用形参时,通过指针偏移量找到传参时生成的形参。

4、 函数的形参和实参分别是怎样实例化的?

形参时函数调用时猜开辟空间,形参与实参的值相同,但所属的空间不同,改变形参不会影响实参。

5、 函数的返回值是如何带回的?

在函数调用前,把call指令下一条指令的地址压入栈中,并且把上一个函数的ebp压入栈中,函数调用完毕,通过弹出ebp找到原始函数的栈底,同时使用压入栈中的地址找到下一条所要执行语句的地址。返回值是通过寄存器带出的。


关注!点赞!评论!收藏!关注!点赞!评论!收藏!关注!点赞!评论!收藏!关注!点赞!评论!收藏!关注!点赞!评论!收藏!