环境:X86+Vs2013
我们C语言学习过程中是否遇到过如下问题或者疑惑:
1、局部变量是如何创建的?
2、为什么局部变量的值是随机值?
3、函数是怎么传参的?传参的顺序是怎样的?
4、形参和实参是什么关系?
5、函数调用是怎么做的?
6、函数调用完后怎么返回的?
看完这篇关于函数栈帧的博客,我相信你对这些问题会有一些进一步的理解,希望能帮助你解决一些学习中的困惑。
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对栈进行操作,比如获取函数参数,局部变量等。其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部。 |
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:终止函数执行,当前栈帧所开辟的空间收回。
为了能够看清楚全部细节,我们把函数写的尽量细一点。
- #include <stdio.h>
-
- 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
在栈区创建函数栈帧
按下F10,在视图中打开调用堆栈窗口,我们发现
main()
函数被调用了。那么main()函数被谁调用调用了呢?
当我们调试到 return 0 之后;再按F10,我们发现程序跳转到了调用
main()
函数的函数内,
原来
main()
函数是被__tmainCRTStartup
函数调用的,而__tmainCRTStartup
又是被mainCRTStartup
调用的。
函数传参从右向左
形参是实参的一份临时拷贝
过程一:pop edi / esi / ebx
过程二:mov esp, ebp 】
过程三:pop ebp】
过程四:ret】
过程五:mov dword ptr [ebp-20h],eax】
可以看到这里返回到了(3.3调用Add()函数前的准备),最后指令call
的下一条指令。
之后的过程还很复杂,这里就不详细展示了。
对此我们对刚开始的问题是不是有了一点柳暗花明的感觉。
在不同的编译器下,函数调用过程中栈帧的创建是略有差异的,具体细节取决于编译器的实现。
友情提示:
不要使用太高级的编译器,越高级的编译器,越不容易学习和观察。
这篇博客有很多不足的地方,希望大家及时指出来!!!