函数调用过程(栈帧的创建和销毁)

在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语言程序运行时必须的一个记录调用路径和参数的空间:
函数调用框架;
传递参数;
保存返回地址;
提供局部变量空间;

猜你喜欢

转载自blog.csdn.net/weixin_44280688/article/details/105468514