C语言函数的栈帧详解

2022-10-27 16:07:29 浏览数 (1)

一、栈

简单来说栈的主要特点有:

一个限定表尾进行删除(出栈)和插入(入栈)操作的线性表,其过程类似与压子弹与退子弹(后进先出)。 一个由系统自动分配的内存空间,譬如调用函数、创建临时变量时内存空间的创建与销毁。 用于存储函数内部的局部变量、方法调用、函数传参数值等。 由高地址向低地址生长。

二、常用寄存器及简单汇编指令

寄存器

用途

EAX

累加寄存器:用于乘除法、函数返回值

EBX

用于存放内存数据指针

ECX

计数器

EDX

用于乘除法、IO指针

ESI

源索引寄存器,存放源字符串指针

EDI

目标索引寄存器,存放目标字符串指针

ESP

存放栈顶指针

EBP

存放栈底指针

汇编指令

用途

mov

mov A,B 将数据B移动到A

push

压栈

pop

出栈

call

函数调用

add

加法

sub

减法

rep

重复

lea

加载有效地址

三、理解栈帧

​ 首先,什么是栈帧?引用百度百科:C语言中,每个栈帧对应着一个未运行完的函数。栈帧中保存了该函数的返回地址和局部变量。从这句话中,可以提炼以下几点信息:

  • 栈帧是一块因函数运行而临时开辟的空间。
  • 每调用一次函数便会创建一个独立栈帧。
  • 栈帧中存放的是函数中的必要信息,如局部变量、函数传参、返回值等。
  • 当函数运行完毕栈帧将会销毁。

​ 下面进入主题,图解函数栈帧的创建与销毁过程。

3.1 main函数栈帧创建

​ 根据VS2013编译器调试,调用堆栈,不难发现main函数的调用链条如下:

很显然main函数在被调用时,创建了栈帧。在调试过程中将转到反汇编,便能直观的看到main函数栈帧创建的过程。首先需明确的是,函数栈帧由寄存器esp,ebp维护。

PLAINTEXT

代码语言:javascript复制
008B1410  push        ebp  
008B1411  mov         ebp,esp  
008B1413  sub         esp,0E4h  
008B1419  push        ebx  
008B141A  push        esi  
008B141B  push        edi  
008B141C  lea         edi,[ebp-0E4h]  
008B1422  mov         ecx,39h  
008B1427  mov         eax,0CCCCCCCCh  
008B142C  rep stos    dword ptr es:[edi] //dword 为 4个字节

1.在__tmainCRTStartup()函数顶部压入ebp,如图所示esp指向ebp,ebp成功压入栈中。

2.esp值传递给ebp。

3.esp减去0E4h:由于栈先使用高地址后使用低地址,减去一个值意味着esp指针向低地址移动了0E4h个地址,此处便开辟了main函数的栈帧。

4.压入ebx,esp指向ebx顶部。

5.压入esi,esp指向esi顶部。

6.压入edi,esp指向edi顶部。

7.将edi向下39h个空间全部改为0xCCCCCCCC。

3.1.1 main函数栈帧创建动态演示

3.2 局部变量创建

PLAINTEXT

代码语言:javascript复制
int a = 10;
00AA142E  mov         dword ptr [ebp-8],0Ah  
	int b = 20;
00AA1435  mov         dword ptr [ebp-14h],14h  
	int ret = 0;
00AA143C  mov         dword ptr [ebp-20h],0  

  1. 将十六进制整数:0Ah(DEC 10)放入ebp 向低地址移动8个字节。
  2. 将十六进制整数:14h(DEC 20)放入ebp 向低地址移动20个字节。
  3. 将十六进制整数:0(DEC 0)放入ebp 向低地址移动32个字节。

3.2.1 局部变量创建动态演示

3.3 函数传参与调用

CPP

代码语言:javascript复制
ret = Add(a, b);
00AA1443  mov         eax,dword ptr [ebp-14h]  
00AA1446  push        eax  
00AA1447  mov         ecx,dword ptr [ebp-8]  
00AA144A  push        ecx  
00AA144B  call        00AA10E1  
00AA1450  add         esp,8  
00AA1453  mov         dword ptr [ebp-20h],eax  

3.3.1 函数传参

  1. ebp - 14h 的地址传给eax,即eax中实际存放了20。
  2. eax 压栈。
  3. ebp - 8 的地址传给ecx,即ecx中实际存放了10。
  4. ecx 压栈。

3.3.3 函数调用

可以发现,在执行call指令后,栈中压入call指令的下一条地址。

进入Add()函数,可以看出这与此前main函数开辟栈帧的过程类似,说明Add()函数调用又开辟了一块独立的栈帧。

在函数栈帧、局部变量创建完毕后,进行Add()函数运算过程:

PLAINTEXT

代码语言:javascript复制
c = a   b;
00AA13E5  mov         eax,dword ptr [ebp 8]  
00AA13E8  add         eax,dword ptr [ebp 0Ch]  
00AA13EB  mov         dword ptr [ebp-8],eax  

  1. 将(ebp 8)的值传递给eax,此时的ebp存放Add函数的栈底指针,(ebp 8) 的位置即函数传参时创建的ecx的地址,其内部存放的正是10。
  2. eax寄存器中执行求和指令,加上(ebp 0ch) 中的值,同理可以得知(ebp 0ch)中的值是20。
  3. 将eax的经过求和的结果,传递到(ebp - 8)的位置 。

通过上述过程可以得知函数内部并未给形参开辟空间,而是直接查找了实参传递时的地址,由此解释了形参其实是实参的一份临时拷贝。

3.3.4 函数返回

PLAINTEXT

代码语言:javascript复制
return c;
00AA13EE  mov         eax,dword ptr [ebp-8]  

将返回值传递至寄存器eax中,因此在函数调用结束函数栈帧被销毁时,返回值并不会销毁。在函数拿到返回值后,开始出栈:

PLAINTEXT

代码语言:javascript复制
00AA13F1  pop         edi  
00AA13F2  pop         esi  
00AA13F3  pop         ebx  
00AA13F4  mov         esp,ebp  
00AA13F6  pop         ebp  
00AA13F7  ret  

从低位置到高位置依次弹出edi,esi,ebx,随后将ebp赋给esp并弹出ebp,最后执行ret指令返回到调用Add函数的call指令的下一地址,在执行ret指令时实际已弹出After call,以执行指令 add esp,8,此时esp向高地址移动8字节,esp,ebp重新维护main函数,eax中存放的返回值将被传递给地址(ebp - 20h)即ret的地址。至此,Add函数返回完毕。main函数栈帧销毁过程与前述过程类似。

文章作者: CtrlX

文章链接: http://ctrlx.life/post/8343ef68.html

版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 CtrlCherry

add

0 人点赞