函数栈帧(详解)

一、前言:

环境:X86+Vs2013

我们C语言学习过程中是否遇到过如下问题或者疑惑:

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

2、为什么局部变量的值是随机值?

3、函数是怎么传参的?传参的顺序是怎样的?

4、形参和实参是什么关系?

5、函数调用是怎么做的?

6、函数调用完后怎么返回的?

看完这篇关于函数栈帧的博客,我相信你对这些问题会有一些进一步的理解,希望能帮助你解决一些学习中的困惑。

二、预备知识了解

2.1、寄存器的种类和作用

eax 累加寄存器,相对于其他寄存器,在运算方面比较常用。
ebx 基地址寄存器,在内存寻址时存放基地址。
ecx 计数寄存器,用于循环操作,比如重复的字符存储操作,或者数字统计。
edx 作为EAX的溢出寄存器,总是被用来放整数除法产生的余数。
esi 变址寄存器,主要用于存放存储单元在段内的偏移量。通常在内存操作指令中作为“源地址指针”使用
edi 目的变址寄存器,主要用于存放存储单元在段内的偏移量。
eip 控制寄存器,存储CPU下次所执行的指令地址(存放指令偏移地址)。
esp 栈顶指针,堆栈的顶部是地址小的区域,压入堆栈的数据越多,esp也就越来越小。在32位平台上,esp每次减少4字节。栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶。是CPU机制决定的,push、pop指令会自动调整esp的值。
ebp 基址指针,指栈的栈底指针。基址指针寄存器(extended base pointer),一般与esp配合使用,可以存取某时刻的esp,这个时刻就是进入一个函数内后,CPU会将esp的值赋给ebp,此时就可以通过ebp对栈进行操作,比如获取函数参数,局部变量等。其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部。

2.2、汇编指令

1、push:在栈的顶端开辟地址并存放变量,然后减少esp的值,32位机器上esp每次减少4个字节,64位机器上esp每次减少8字节。

2、pop:在栈顶端去掉一个地址,然后增加esp的值,2位机器上esp每次增加4个字节,64位机器上esp每次增加8字节。

3、mov:用于将一个数据的源地址传送到目标地址,原操作地址不变。将esp值赋给ebp。

4、sub:从寄存器上减去表示的数值,并将结果保存到寄存器上。

5、lea(load effective address):将一个内存地址直接付给目标的操作数。

6、rep(repeat):引发字符串指令被重复使用。

7、stos(store string):将exc的值拷贝到es:[edi]指向的地址。

8、call:将程序下一条指令的位置的IP压入堆栈,并调用的子程序。

9、jmp:跳转指令。

10、add:将两个数相加,结果写入第一个数中。

11、ret:终止函数执行,当前栈帧所开辟的空间收回。

2.3、例子

为了能够看清楚全部细节,我们把函数写的尽量细一点。

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

汇编码

int main() {
002718A0  push        ebp  
002718A1  mov         ebp,esp  
002718A3  sub         esp,0E4h  
002718A9  push        ebx  
002718AA  push        esi  
002718AB  push        edi  
002718AC  lea         edi,[ebp-24h]  
002718AF  mov         ecx,9  
002718B4  mov         eax,0CCCCCCCCh  
002718B9  rep stos    dword ptr es:[edi]  
002718BB  mov         ecx,27C003h  
002718C0  call        0027131B  
	int a = 10;
002718C5  mov         dword ptr [ebp-8],0Ah  
	int b = 20;
002718CC  mov         dword ptr [ebp-14h],14h  
	int c = 0;
002718D3  mov         dword ptr [ebp-20h],0  
	c = Add(a, b);
002718DA  mov         eax,dword ptr [ebp-14h]  
002718DD  push        eax  
002718DE  mov         ecx,dword ptr [ebp-8]  
002718E1  push        ecx  
002718E2  call        002710B4  
002718E7  add         esp,8  
002718EA  mov         dword ptr [ebp-20h],eax  
	printf("%d", c);
002718ED  mov         eax,dword ptr [ebp-20h]  
002718F0  push        eax  
002718F1  push        277B30h  
002718F6  call        002710D2  
002718FB  add         esp,8  
	return 0;
002718FE  xor         eax,eax  
}
00271900  pop         edi  
00271901  pop         esi  
00271902  pop         ebx  
00271903  add         esp,0E4h  
00271909  cmp         ebp,esp  
0027190B  call        00271244  
00271910  mov         esp,ebp  
00271912  pop         ebp  
00271913  ret  

2.4、内存模型

在栈区创建函数栈帧

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

三、栈帧的创建

按下F10,在视图中打开调用堆栈窗口,我们发现main()函数被调用了。

那么main()函数被谁调用调用了呢?

当我们调试到 return 0 之后;再按F10,我们发现程序跳转到了调用main()函数的函数内,

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

原来main()函数是被__tmainCRTStartup函数调用的,而 __tmainCRTStartup又是被mainCRTStartup调用的。

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

3.1、为main函数开辟栈帧

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

 3.2、在main函数中创建变量

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

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

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

3.3、调用add函数前做准备

函数传参从右向左

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

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

 3.4、为add函数开辟栈帧

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

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

 3.5、Add()函数中创建变量并运算

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

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

形参是实参的一份临时拷贝

四、函数栈帧的销毁

4.1、Add()栈帧的销毁

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

过程一:pop    edi / esi / ebx

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

过程二:mov    esp, ebp 】

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

过程三:pop ebp】

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

过程四:ret】

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

过程五:mov     dword ptr [ebp-20h],eax】

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

4.2、返回main()函数栈帧

可以看到这里返回到了(3.3调用Add()函数前的准备),最后指令call的下一条指令。

之后的过程还很复杂,这里就不详细展示了。

五、总结:

对此我们对刚开始的问题是不是有了一点柳暗花明的感觉。

在不同的编译器下,函数调用过程中栈帧的创建是略有差异的,具体细节取决于编译器的实现。

友情提示:

不要使用太高级的编译器,越高级的编译器,越不容易学习和观察。

这篇博客有很多不足的地方,希望大家及时指出来!!!

你可能感兴趣的:(c语言)