函数栈帧(详细图解)

目录

 一、栈

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

三、理解栈帧

3.1 main函数栈帧创建

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

3.2 局部变量创建

 3.2.1 局部变量创建动态演示 

3.3 函数传参与调用

3.3.1 函数传参

3.3.2 函数传参动态演示 

3.3.3 函数调用

3.3.4 函数返回

四、END


 一、栈

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

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

 函数栈帧(详细图解)_第1张图片

函数栈帧(详细图解)_第2张图片


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

        

寄存器 用途
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维护。

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成功压入栈中。

函数栈帧(详细图解)_第3张图片

     2.esp值传递给ebp。

函数栈帧(详细图解)_第4张图片

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

函数栈帧(详细图解)_第5张图片

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

函数栈帧(详细图解)_第6张图片

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

函数栈帧(详细图解)_第7张图片

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

函数栈帧(详细图解)_第8张图片

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

函数栈帧(详细图解)_第9张图片

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

3.2 局部变量创建

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 局部变量创建动态演示 

函数栈帧(详细图解)_第10张图片

3.3 函数传参与调用

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 压栈。

函数栈帧(详细图解)_第11张图片

3.3.3 函数调用

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

函数栈帧(详细图解)_第12张图片

函数栈帧(详细图解)_第13张图片

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

函数栈帧(详细图解)_第14张图片

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

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 函数返回

return c;
00AA13EE  mov         eax,dword ptr [ebp-8]  

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

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函数栈帧销毁过程与前述过程类似。

 


四、END

      通过对函数栈帧创建、销毁过程的剖析使我们不仅了解计算机做了什么,还了解了它是如何做的。通过函数栈帧尝试解析递归等问题相信也会更加直观。笔者水平有限,不足之处还请各位大佬多指教。

你可能感兴趣的:(数据结构和算法,c语言,栈)