*函数的调用——栈帧*

1.函数调用

首先函数的存在是为了使代码模块化,逻辑清晰且便于查错、进行二次开发。那每一次的函数调用都是一个过程,这个过程我们称为:函数的调用过程。这个过程要为函数开辟栈空间,用于该函数调用中的临时变量的保存、现场保护,这块栈空间即为函数栈帧。

2.栈帧

编写如下代码:

#define _CRT_SECURE_NO_WARNINGS 1
#include <Windows.h>
#include <stdio.h>

int add(int x, int y)
{
	int z = x + y;
	return z;
}

int main()
{
	int a = 15;
	int b = 20;
	int c = add(a, b);
	printf("%d", c);
	system("pause");
	return 0;
}

调试程序并进入反汇编有:

00931410  push        ebp  
00931411  mov         ebp,esp  
00931413  sub         esp,0E4h  
00931419  push        ebx  
0093141A  push        esi  
0093141B  push        edi  
0093141C  lea         edi,[ebp-0E4h]  
00931422  mov         ecx,39h  
00931427  mov         eax,0CCCCCCCCh  
0093142C  rep stos    dword ptr es:[edi]

     在程序执行时,main函数是程序的入口,但是_main函数不是第一个被调用的函数,第一个被调用的函数是_mainStartup函数,是它调用了main函数。ebp、esp是两个基址寄存器,分别用来存放函数栈帧栈底的地址、函数栈帧栈顶的地址。

        int a = 15;
0093142E  mov         dword ptr [ebp-4],0Fh  
	int b = 20;
00931435  mov         dword ptr [ebp-8],14h

     此时,_main函数被调用,同时为main函数创建了栈帧。将15即a放入ebp-4的地址,将20即b放入ebp-8的地址。此时的栈帧图如下:

int c = add(a, b);
0093143C  mov         eax,dword ptr [ebp-8]  
0093143F  push        eax  
00931440  mov         ecx,dword ptr [ebp-4]  
00931443  push        ecx  
00931444  call        @ILT+0(_add) (09310EBh)  
00931449  add         esp,8

     将b放入eax寄存器中,且将eax压入栈中;将a放入ecx寄存器中,且将ecx压入栈中
     call指令的功能:将当前正在执行指令的下一条指令地址压入栈中;随机跳(jmp)至指定函数
     所以将地址00931449压入栈中(压栈操作同时伴随栈顶指针的改变),同时跳转至add函数,此时栈帧图如下:

int add(int x, int y)
{
009313D0  push        ebp  
009313D1  mov         ebp,esp  
009313D3  sub         esp,0CCh  
009313D9  push        ebx  
009313DA  push        esi  
009313DB  push        edi  
009313DC  lea         edi,[ebp-0CCh]  
009313E2  mov         ecx,33h  
009313E7  mov         eax,0CCCCCCCCh  
009313EC  rep stos    dword ptr es:[edi]  
	int z = x + y;
009313EE  mov         eax,dword ptr [ebp+8]  
009313F1  add         eax,dword ptr [ebp+0Ch]  
009313F4  mov         dword ptr [ebp-4],eax 

     此时函数add的栈帧也被创建,首先将寄存器ebp中的内容压入栈中,即将main函数的ebp中的内容压入栈中;再将寄存器esp中的内容放入寄存器ebp中,即栈底指针下移至栈顶指针处;再将esp减去0CCh,即栈顶指针下移,此时ebp、esp分别指向add函数的栈底和栈顶;再将ebp+8即a的内容放入eax中;再将a加上 ebp+0Ch处的b放入eax中;再将 eax的内容放入ebp-4中,此时栈帧图如下:

 
	return z;
009313F7  mov         eax,dword ptr [ebp-4]  
}
009313FA  pop         edi  
009313FB  pop         esi  
009313FC  pop         ebx  
009313FD  mov         esp,ebp  
009313FF  pop         ebp  
00931400  ret  

     再将 ebp-4的内容放入eax中;将ebp的内容给esp,即将栈顶指针上移至栈底指针处(此时函数add的栈帧被销毁);pop将栈顶弹出且放入ebp中,即将ebp改变;ret的功能:将栈顶的值弹出,且将弹出的值放入eip,此时返回到main函数里。此时栈帧图如下:

00931449  add         esp,8  
0093144C  mov         dword ptr [c],eax

      此时给esp加8,即将栈顶上移;将eax的内容放入 ebp-0Ch位置,即将z=a+b放入到了main函数的栈帧中,此时整个函数调用过程完成。此时栈帧图如下:


最终运行结果为:




                
### 函数调用时创建函数的工作原理 在计算机编程中,尤其是像 C 或 C++ 这样的低级语言中,函数调用是一个复杂的过程,涉及到许多细节操作。其中,函数的创建和管理是这一过程中非常重要的部分。 #### 的概念 (Stack Frame),也称为过程活动记录,是一种用于支持函数调用的数据结构[^2]。它存储了每次函数调用所涉及的信息,包括局部变量、参数、返回地址以及其他必要的上下文信息。这些信息被组织成一个连续的内存区域,位于进程的运行时堆上。 #### 创建函数的具体流程 当一个函数调用时,会经历以下几个关键阶段来完成的创建: 1. **传递参数** 调用方(即父函数)负责将要传给目标函数的实参按照特定顺序压入中。通常是从右至左依次压,这样可以让第一个参数始终处于最低位置以便访问[^3]。 2. **保存返回地址** 接下来,CPU 将指令指针寄存器 EIP/RIP 中的内容复制到中作为返回地址。这一步确保一旦子函数结束执行后能够正确回到原来的位置继续未完成的任务。 3. **切换控制权并初始化新环境** 随后发生的是实际跳转动作——通过 CALL 汇编命令转移到指定的目标地址处开始新的逻辑处理;与此同时,在这里还完成了如下几项重要任务: - 把当前基址指针 `%rbp` 的值推送到里头以保留旧有的框架边界; - 更新 `%rbp` 自身使其指向最新分配出来的空间顶部从而定义好本次调用专属的新范围起点。 4. **分配额外的空间供内部使用** 子例程可能会进一步请求更多临时资源比如声明一些自动类型的对象或者数组等等,则需相应调整顶指针 `%rsp` 来预留足够的容量满足需求。 以下是上述描述的一个简化版伪代码表示形式: ```c void callee_function() { int local_variable; // 假设此处存在某些计算... } // 对应汇编层面可能呈现的样子 callee_function: push rbp ; 保护前一层的 base pointer mov rbp, rsp ; 设置自己的 base pointer sub rsp, N ; 为本地变量预留空间 (N bytes) ... ; 执行具体业务逻辑... leave ; 清理现场恢复先前状态 ret ; 返回至上层调用者那里去 ``` 以上每一段都紧密关联着之前提及过的理论知识点,并且共同构成了整个机制运作的核心链条。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值