目录

一、什么是函数栈帧?

1.1 - 栈

1.2 - 栈内存

1.3 - 栈帧

1.4 - 相关寄存器

1.5 - 相关汇编指令

二、函数栈帧的创建和销毁

2.1 - main 函数栈帧的创建

2.2 - main 函数局部变量的创建

2.3 - 函数传参和调用

2.3.1 -  进入 add 函数

2.3.2 - add 函数栈帧的销毁

2.4 - 接受函数的返回值

 ​编辑

三、总结



一、什么是函数栈帧?

1.1 - 栈

栈(stack)是只允许在栈顶进行插入(入栈,push)删除(出栈,pop)的操作受限制的线性表。栈的特点是后进先出(Last In First Out,简称 LIFO)

1.2 - 栈内存

栈内存(stack memory)是一个由系统自动分配和回收的内存空间。关于栈内存的增长方向有两种:一种是向上增长,即低地址向高地址增长;另一种是向下增长,即高地址向低地址增长。在目前常见的体系结构和编译系统中,栈内存大多是向下增长的

1.3 - 栈帧

在 C 语言中,每个函数的每次调用,都有它自己独立的一个栈帧(stack frame),栈帧中保存了该函数的返回地址和局部变量。寄存器 ebp 指向当前的栈帧的底部,寄存器 esp 指向当前的栈帧的顶部。  

1.4 - 相关寄存器

寄存器作用
ebpextended base pointer,用于存放栈帧底部的指针
espextended stack ponter,用于存放栈帧顶部的指针
eax累加器(acculator),它是很多加法乘法指令的缺省寄存器
ebx基地址(base)寄存器,在内存寻址时存放基地址
ecx计数器(counter),是重复(REP)前缀指令和 LOOP 指令的内定计数器
edx总是被用来放整数除法产生的余数
esi源索引(source index)寄存器
edi目标索引(destination index)寄存器

1.5 - 相关汇编指令

汇编指令作用
push数据入栈,同时 esp 栈顶寄存器也要发生改变
pop数据出栈,同时 esp 栈帧寄存器也要发生改变
movemove A, B,即将数据 B 移到 A 中
call函数调用:1. 压入返回地址 2. 转入目标函数
add加法
sub减法
rep重复
lea加载有效地址(load effective address)

二、函数栈帧的创建和销毁

演示代码

#include <stdio.h>

int add(int a, int b)
{
	int c = 0;
	c = a + b;
	return c;
}

int main()
{
	int a = 10;
	int b = 20;
	int ret = 0;
	ret = add(a, b);
	printf("ret = %d\n", ret);
	return 0;
}

VS2019 x86 平台上进行调试,首先点击"调试" --> "窗口" --> "调用堆栈" ,然后右击勾选"显示外部代码",我们可以观察到:

这说明在 main 函数调用之前,是由 invoke_main 函数来调用 main 函数的。当然在 invoke_main 函数之前还有函数调用。

调试到 main 函数开始执行的第一行,右击鼠标转到反汇编

int main()
{
// main 函数栈帧的创建
00691820  push        ebp  
00691821  mov         ebp,esp  
00691823  sub         esp,0E4h  
00691829  push        ebx  
0069182A  push        esi  
0069182B  push        edi  
0069182C  lea         edi,[ebp-24h]  
0069182F  mov         ecx,9  
00691834  mov         eax,0CCCCCCCCh  
00691839  rep stos    dword ptr es:[edi]  
// main 函数局部变量的创建
	int a = 10;
0069183B  mov         dword ptr [ebp-8],0Ah  
	int b = 20;
00691842  mov         dword ptr [ebp-14h],14h  
	int ret = 0;
00691849  mov         dword ptr [ebp-20h],0  
// 函数传参和调用
	ret = add(a, b);
00691850  mov         eax,dword ptr [ebp-14h]  
00691853  push        eax  
00691854  mov         ecx,dword ptr [ebp-8]  
00691857  push        ecx  
00691858  call        00691023  
// 接受函数的返回值
0069185D  add         esp,8  
00691860  mov         dword ptr [ebp-20h],eax  
// 函数传参和调用
	printf("ret = %d\n", ret);
00691863  mov         eax,dword ptr [ebp-20h]  
00691866  push        eax  
00691867  push        697B30h  
0069186C  call        006910D2  
00691871  add         esp,8  
	return 0;
00691874  xor         eax,eax  
}

2.1 - main 函数栈帧的创建

00431820  push        ebp  
00431821  mov         ebp,esp  
00431823  sub         esp,0E4h  
00431829  push        ebx  
0043182A  push        esi  
0043182B  push        edi  
0043182C  lea         edi,[ebp-24h]  
0043182F  mov         ecx,9  
00431834  mov         eax,0CCCCCCCCh  
00431839  rep stos    dword ptr es:[edi]  

1. ebp 和 esp 的初始值:

 2.push ebp,即把 ebp 的值压入栈中,同时让 esp - 4:

 

3. mov ebp, esp,即把 esp 赋值给 ebp,这相当于产生了 main 函数栈帧的 ebp

 

 4.sub esp, 0E4h,即让 esp - 0xe4,此时的 esp 就是 main 函数栈帧的 esp:

 

5.push ebx,即把 ebx 的值压入栈中,同时让 esp - 4。

6.push esi,即把 esi 的值压入栈中,同时让 esp - 4。

7.push edi,即把 edi 的值压入栈中,同时让 esp - 4

 

 上面 3 条指令把 3 个寄存器的值都保存在栈区,是因为这 3 个寄存器的值在函数执行过程中可能会被修改,因此先保存寄存器中原来的值,以便在退出函数时恢复

 8.lea edi, [ebp-24h],即将 ebp - 0x24 赋值给 edi:

 

 9.mov ecx, 9,即将 9 赋值给 ecx

10.mov eax, 0CCCCCCCCh,即将 0xCCCCCCCC 赋值给 eax

11. rep stos dword ptr es:[edi],这句汇编代码再加上上面 3 句汇编代码等价于下面的伪代码

edi = ebp - 0x24;
ecx = 9;
eax = 0xCCCCCCCC;
for(; ecx == 0; --ecx, edi += 4)
{
    *(int*)edi = eax;
}

 

2.2 - main 函数局部变量的创建

	int a = 10;
0043183B  mov         dword ptr [ebp-8],0Ah  
	int b = 20;
00431842  mov         dword ptr [ebp-14h],14h  
	int ret = 0;
00431849  mov         dword ptr [ebp-20h],0 

1.mov dword ptr [ebp-8],0Ah,即将 0xa 存储到 ebp - 8 的地址处。

2.mov dword ptr [ebp-14h],14h,即将 0x14 存储到 ebp - 0x14 的地址处。

3.mov dword ptr [ebp-20h],0,即将 0 存储到 exp - 0x20 的地址处。

 这表明局部变量是在其所在的函数的栈帧空间中创建的

2.3 - 函数传参和调用

	ret = add(a, b);
00691850  mov         eax,dword ptr [ebp-14h]  
00691853  push        eax  
00691854  mov         ecx,dword ptr [ebp-8]  
00691857  push        ecx  
00691858  call        00691023  

1.mov eax,dword ptr [ebp-14h],即传递 b,将 ebp - 0x14 地址处存放的 20 放到 eax 寄存器中。

2.push eax,即将 eax 的值压入栈中,同时让 esp - 4。

3.mov ecx,dword ptr [ebp-8],即传递 a,将 ebp - 8 地址处存放的 10 放到 ecx 寄存器中。

4.push ecx,即将 ecx 的值压入栈中,同时让 esp - 4

5.call 00691023,即执行调用 add 函数的操作,但是在调用 add 函数之前,会把 call 指令的下一条指令的地址,即 0069185D,压入栈中,同时让 esp - 4

 按 f11,再按一次 f11 就跳转到 add 函数

2.3.1 -  进入 add 函数

int add(int a, int b)
{
// add 函数栈帧的创建
006917F0  push        ebp  
006917F1  mov         ebp,esp  
006917F3  sub         esp,0CCh  
006917F9  push        ebx  
006917FA  push        esi  
006917FB  push        edi  
// 局部变量的创建
	int c = 0;
006917FC  mov         dword ptr [ebp-8],0  
// 计算 a + b,并把结果保存到 c 中
	c = a + b;
00691803  mov         eax,dword ptr [ebp+8]
00691806  add         eax,dword ptr [ebp+0Ch] 
00691809  mov         dword ptr [ebp-8],eax  
	return c;
0069180C  mov         eax,dword ptr [ebp-8]
}

1.add 函数栈帧的创建和局部变量的创建与 main 函数相似,只是在栈帧空间的大小上有些差异

2.mov eax,dword ptr [ebp+8],即把 ebp + 8 地址处的值存储到 eax 寄存器中

3.add eax,dword ptr [ebp+0Ch],即把 ebp + 0xc 地址处的值加到 eax 寄存器中

4.mov dword ptr [ebp-8],eax,即把 eax 中的结果保存到 ebp - 8 的地址处,即放到变量 c 中

5.mov eax,dword ptr [ebp-8],即把 ebp - 8 地址处的值,即变量 c 中的值,放到 eax 中,通过 eax 带回计算的结果,做函数的返回值

2.3.2 - add 函数栈帧的销毁

0069180F  pop         edi
00691810  pop         esi
00691811  pop         ebx
00691812  mov         esp,ebp
00691814  pop         ebp
00691815  ret

1.pop edi,即在栈顶弹出一个值,存放到 edi 中,同时让 esp + 4

2.pop esi,即在栈顶弹出一个值,存放到 esi 中,同时让 esp + 4

3.pop ebx,即在栈顶弹出一个值,存放到 ebx 中,同时让 esp + 4

4.mov esp,ebp,即将 ebp 赋值给 esp,相当于回收了 add 函数的栈帧空间

5.pop ebp,即将弹出栈顶的值存放到 ebp 中,这个值恰好就是main函数的ebp,同时让 eps + 4,此时就恢复了 main 函数的栈帧维护

6.ret,首先让 esp + 4,然后跳转到 call 指令下一条指令的地址处

2.4 - 接受函数的返回值

0069185D  add         esp,8  // 让 esp + 8
00691860  mov         dword ptr [ebp-20h],eax  // 将 eax 中的值存储到 ebp - 0x20 的地址处,即存储到 main 函数中的 ret 变量中

 

三、总结

当我们理解了函数栈帧的创建和销毁,以下的问题就能够很好地理解了:

  • 局部变量是如何创建的?

  • 局部变量不初始化时,内容为什么是随机的?

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

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

  • 函数的返回值是如何带回的?

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐