函数的调用过程
在C语言中调用某一函数时,它会跳转过去执行这个函数直到执行完毕后接着执行下一条指令。
在执行调用函数的过程中,通过形成一个栈帧来完成。栈帧是编译器用来实现函数调用过程的一种数据结构。
以Add() 函数为例来分析调用过程:
#include<stdio.h>
#include<stdlib.h>
int Add(int x, int y){
int sum = 0;
sum = x + y;
return sum;
}
int main(){
int a = 10;
int b = 20;
int ret = 0;
ret = Add(a, b);
return 0;
}
- 栈帧的需要ebp和esp两个寄存器。 在函数调用的过程中这两个寄存器存放了维护这个栈的栈底和栈顶指针
注意:ebp指向当前位于系统栈最上边一个栈帧的底部,而不是系统栈的底部。严格说来,“栈帧底部”和“栈底”是不同的概念; ESP所指的栈帧顶部和系统栈的顶部是同一个位置。
esp:栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶; 在32位平台上,ESP每次减少4字节
ebp:基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部
eax 是”累加器”(accumulator), 它是很多加法乘法指令的缺省寄存器
ebx 是”基地址”(base)寄存器, 在内存寻址时存放基地址
ecx 是计数器(counter), 是重复(REP)前缀指令和LOOP指令的内定计数器
edx 则总是被用来放整数除法产生的余数
esi/edi分别叫做”源/目标索引寄存器”(source/destination index),因为在很多字符串操作指令中, DS:ESI指向源串,而ES:EDI指向目标串
汇编指令:
mov :数据传送指令,也是最基本的编程指令,用于将一个数据从源地址传送到目标地址(寄存器间的数据传送本质上也是一样的)
sub:减法指令
lea:取偏移地址
push:实现压入操作的指令是PUSH指令
pop:实现弹出操作的指令
call:用于保存当前指令的下一条指令并跳转到目标函数
内存地址空间的分布:
栈空间是向低地址增长的,主要是用来保存函数栈帧。 栈空间的大小很有限,仅有区区几MB大小 (所以下图高地址在下面)
汇编代码实现:
int main ()
{
011B26E0 push ebp
011B26E1 mov ebp,esp
011B26E3 sub esp,0E4h
011B26E9 push ebx
011B26EA push esi
011B26EB push edi
011B26EC lea edi,[ebp-0E4h]
011B26F2 mov ecx,39h
011B26F7 mov eax,0CCCCCCCCh
011B26FC rep stos dword ptr es:[edi]
int a = 10;
011B26FE mov dword ptr [a],0Ah
int b = 20;
011B2705 mov dword ptr [b],0Ch
int ret = 0;
011B270C mov dword ptr [ret],0
ret = Add(a,b);
011B2713 mov eax,dword ptr [b]
011B2716 push eax
011B2717 mov ecx,dword ptr [a]
011B271A push ecx
011B271B call @ILT+640(_Add) (11B1285h)
011B2720 add esp,8
011B2723 mov dword ptr [ret],eax
return 0;
011B2726 xor eax,eax
}
011B2728 pop edi
011B2729 pop esi
011B272A pop ebx
011B272B add esp,0E4h
011B2731 cmp ebp,esp
011B2733 call @ILT+450(__RTC_CheckEsp) (11B11C7h)
011B2738 mov esp,ebp
011B273A pop ebp
011B273B ret
int Add(int x,int y)
{
011B26A0 push ebp
011B26A1 mov ebp,esp
011B26A3 sub esp,0CCh
011B26A9 push ebx
011B26AA push esi
011B26AB push edi
011B26AC lea edi,[ebp-0CCh]
011B26B2 mov ecx,33h
011B26B7 mov eax,0CCCCCCCCh
011B26BC rep stos dword ptr es:[edi]
int sum = 0;
011B26BE mov dword ptr [sum],0
sum = x + y;
011B26C5 mov eax,dword ptr [x]
011B26C8 add eax,dword ptr [y]
011B26CB mov dword ptr [sum],eax
return sum;
011B26CE mov eax,dword ptr [sum]
}
011B26D1 pop edi
011B26D2 pop esi
011B26D3 pop ebx
011B26D4 mov esp,ebp
011B26D6 pop ebp
011B26D7 ret
1. 调用 main() 函数:
1.push压栈,把ebp放入栈顶,而esp始终指向栈顶
2.mov, 将esp值传给ebp,也就是让esp,ebp移在一起
3.sub(减的意思),即将esp-0E4h赋给esp,且函数调用分配由高地址向低地址增长,因此esp向上移动,即开辟了新空间,也就是为main函数开辟空间
4.接下来三个push分别将ebx,esi,edi按顺序压入栈顶,而esp也会指向栈顶
5.lea指令,加载有效地址;将ebp-0E4h的地址放入edi中,也就是edi指向ebp-0E4h (1. 把39h放到ecx中;2. 把0cccccccch放到eax中;3. 从edi所指向的地址开始向高地址进行拷贝,拷贝的次数为ecx内容,拷贝的内容为eax内)
6.创建变量a与b,并初始化10和20
2. Add() 函数的调用
把b放入eax中,然后对eax压栈(形参a)
把a放入eax中,然后对eax压栈(形参b)
call:将下一条指令地址压栈,然后进入add() 函数里面
注意:call语句push的是下一条指令的地址,为了函数返回时知道从哪儿接着执行
接下来进入add() 函数:
A.先把main函数ebp压栈,保存指向main()函数栈帧底部的ebp的地址,目的是当返回时能找到main函数栈底,此时esp指向新的栈顶位置
将main函数的ebp压栈,也是为了返回时找到main函数栈底
B.将esp的值赋给ebp,产生新的ebp,即Add()函数栈帧的ebp;
C.给esp减去一个16进制数0CCh(为Add()函数预开辟空间);
D.push ebx、esi、edi;
E.lea指令,加载有效地址;
F.初始化预开辟的空间为0xcccccccc;
G.创建变量z并为其赋值
H.把形参a放到eax,即把10,放入eax把形参b加到eax中,即把20加到eax中再把eax放到z的位置,即把两数之和放到z中
I.把z的值放到寄存器eax中返回,因为z为函数临时开辟的变量空间等函数执行完会销毁,因此放寄存器中返回
K.接下来执行pop出栈操作,edi esi ebx依次从上向下出栈,esp 会向下移动,栈的特点:先进后出,后进先出
L.将ebp值赋给esp,也就是esp向下移动指向ebp位置,此时add开辟的栈空间已经销毁
M.pop将栈顶的元素弹出放到ebp中,也就是说将main函数的ebp放入ebp中,即ebp现在指向main函数ebp
N.在执行ret后,会把之前push的地址弹出去,这时就要返回main函数,这也就是为什么之前要push这个地址,这样call指令就完成了
接下来从那个call指令继续执行
O.把esp+8,即esp向下移,把形参销毁
P.最后对mian() 函数栈帧销毁,方法同上
总结:堆栈是C语言程序运行时必须的一个记录调用路径和参数的空间:
函数调用框架;
传递参数;
保存返回地址;
提供局部变量空间;