函数的栈帧的创建与销毁

目录

提出问题

概念

寄存器

汇编指令

main函数的调用

汇编代码:

 调用堆栈:

整体汇编代码解析

main函数

 Add函数

整体过程栈帧分布图

回答开头问题


提出问题

局部变量是怎么创建的?

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

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

形参和实参是什么关系?

函数调用是怎么做的?

函数调用结束后怎么返回的?

如果这些问题你看到之后能像ChatGPT一样迎刃而解,那么这篇文章将对你毫无用处。倘若你看到问题毫无头绪或者回答的不是很全面,那么我建议你看看我的这篇博客。

概念

函数栈帧(Function Stack Frame)是指在函数调用时,为了维护函数执行的现场信息和实现参数传递等功能,在当前函数的栈空间中动态分配的一块连续内存区域。它通常由以下几部分组成:

  1. 返回地址:保存当前函数执行完成后返回到调用者函数的下一条指令地址。

  2. 基址指针(Base Pointer,BP):指向函数栈帧的起始位置,用于定位局部变量和参数的位置。

  3. 本地变量(Local Variables):保存函数内部声明的局部变量。

  4. 参数(Arguments):函数调用时传入的参数值。

函数栈帧的创建过程发生在函数调用时。当一个函数被调用时,首先需要将函数参数压入栈中,然后使用 CALL 指令跳转到函数体开始处,并将当前函数执行完成后返回到调用者函数的下一条指令地址压入栈中。进入函数体后,通过 PUSH 指令将基址指针寄存器 BP 和其他需要保护的寄存器的值压入栈中,此时函数栈帧就被创建并进入了就绪状态。

在函数执行过程中,可以通过基址指针 BP 来访问栈帧中的局部变量和参数。例如,通过将 BP 寄存器的值加上偏移量可以访问栈帧中的具体变量。在函数执行完成后,需要使用 POP 指令将保存在栈中的各种信息恢复并释放相关的内存空间。

函数栈帧是实现函数调用和参数传递等功能的重要机制,对于理解汇编语言和底层系统编程非常重要。

寄存器

  1. EBP(Base Pointer):指向栈底的位置(当前函数栈帧的起始位置),并且在函数内部可以通过它来访问函数参数和局部变量。

  2. ESP(Stack Pointer):指向栈顶的位置,用于管理函数调用时的栈空间。

  3. EAX(Accumulator Register):用于一些特殊的运算,如乘除法,还可以作为函数返回值的寄存器。

  4. ECX(Counter Register):常用于循环操作,也可以用作计数器或指针寄存器。

  5. ESI(Source Index):源地址索引寄存器,常用于字符串处理等操作。

  6. EDI(Destination Index):目标地址索引寄存器,常用于字符串处理等操作。

  7. EBX(Base Register):通用寄存器,可以用作任意数据的基址寄存器。

汇编指令

  1. MOV(Move):将一个数据从一个寄存器或内存位置复制到另一个寄存器或内存位置。

  2. CALL(Call Subroutine):调用一个子程序,并将返回地址压入堆栈中。

  3. ADD(Addition):将两个数相加,并将结果存储在一个寄存器或内存位置中。

  4. SUB(Subtraction):将两个数相减,并将结果存储在一个寄存器或内存位置中。

  5. PUSH(Push):将一个值压入堆栈中,并将堆栈指针减少。

  6. POP(Pop):将一个值弹出堆栈,并将堆栈指针增加。

  7. JUMP(Jump):根据条件或无条件跳转到程序的另一个位置,以继续执行其他指令。

  8. RET(Return):从一个子程序中返回,并恢复调用前的状态。

  9. LEA(Load Effective Address):是一种用于加载有效地址的指令。它不是传统意义上的数据传输指令,而是将内存地址计算后存储到一个寄存器中。

这些指令是汇编语言中最基本和最常用的指令之一,它们可以实现很多计算机程序的核心功能。例如,MOV 指令可以用于数据传输、CALL 指令可以用于函数调用、ADD 和 SUB 指令可以用于数学运算、PUSH 和 POP 指令可以用于堆栈管理、JUMP 指令可以用于控制程序流程等等。

main函数的调用

函数用例:

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

当我们调试该代码的时候,通过函数栈帧可以看到,其实main函数也要被调用,具体调用情况如图。

汇编代码:

 调用堆栈:

 如图我们能看出mainCRTStarup函数调用了__tmainCRTStartup函数,然后__tmainCRTStartup函数又调用了我们的main函数。

整体汇编代码解析

main函数

汇编代码及其解析


//main
//int main()
//{
//      //为main函数开辟空间
//	    00051410  push        ebp//将epb压入栈中(这一步能记录上一个函数即(__tmainCRTStartup函数的栈底位置)使销毁到该位置的时候可以找到__tmainCRTStartup函数的栈底位置)
//		00051411  mov         ebp, esp //让epb指向现在esp指向的位置(将esp的地址给ebp)
//		00051413  sub         esp, 0E4h//将esp向下偏移0E4h
//		00051419  push        ebx//将ebx压栈
//		0005141A  push        esi//将esi压栈
//		0005141B  push        edi//将edi压栈
// 
//		//看这四条汇编指令的时候需要将显示符号名给勾选上
//		//下边这四条汇编指令的意思是在ebp的上边0E4h大小的区域里
//		//为这块区域赋初值cccccccc
//		0005141C  lea         edi, [ebp-0E4h]
//		00051422  mov         ecx, 39h
//		00051427  mov         eax, 0CCCCCCCCh
//		0005142C  rep stos    dword ptr es : [edi]
// 
//		int a = 10;
//	    0005142E  mov         dword ptr[ebp - 8], 0Ah//将0Ah(转化为十进制就是10)放进[ebp-8]的位置
//		int b = 20;
//	    00051435  mov         dword ptr[ebp - 14h], 14h//将14h(转化为十进制就是20)放进[ebp-14h]的位置
//		int c = 0;
//	    0005143C  mov         dword ptr[ebp - 20h], 0//将14h(转化为十进制就是0)放进[ebp - 20h]的位置
//		c = Add(a, b);
//	    00051443  mov         eax, dword ptr[ebp - 14h]//将[ebp - 14h]中的值放入寄存器eax
//		00051446  push        eax//将eax压栈--这就是形参
//		00051447  mov         ecx, dword ptr[ebp - 8]//将[ebp - 8]中的值放入寄存器ecx
//		0005144A  push        ecx//将ecx压栈--这就是形参
//		0005144B  call        000510E1//进入Add函数并且将call下一条指令的地址存入000510E1
//		00051450  add         esp, 8//将esp向下偏移8个字节
//		00051453  mov         dword ptr[ebp - 20h], eax//将eax寄存器的值赋给[ebp - 20h]
//		printf("%d\n", c);
//		
//	    00051456  mov         esi, esp
//		00051458  mov         eax, dword ptr[ebp - 20h]
//		0005145B  push        eax
//		0005145C  push        55858h
//		00051461  call        dword ptr ds : [00059114h]
//		00051467  add         esp, 8
//		0005146A  cmp         esi, esp
//		0005146C  call        0005113B
//		return 0;
//	    00051471 xor eax, eax
//}
// 
//		//销毁函数栈帧
//      00051473  pop         edi//将edi出栈
//      00051474  pop         esi//将esi出栈
//      00051475  pop         ebx//将ebx出栈
//      00051476  add         esp, 0E4h使esp向下偏移0E4h
//      0005147C  cmp         ebp, esp
//      //0005147E  call        0005113B
//      00051483  mov         esp, ebp//将ebp赋给esp
//      00051485  pop         ebp//将ebp出栈
//      00051486  ret//将main函数返回

call指令使call下一条指令的地址压栈 

 Add函数

汇编代码及其解析


//Add
//
//int  Add(int x, int y)
//{
//		//为Add函数开辟空间
//	    000513C0  push        ebp //将epb压入栈中(这一步能记录上一个函数即(main函数的栈底位置)使销毁到该位置的时候可以找到main函数的栈底位置)
//		000513C1  mov         ebp, esp//将esp的地址给ebp
//		000513C3  sub         esp, 0CCh//esp的地址减去0CCh(相当于让esp向上偏移0CCh)
//		000513C9  push        ebx//将ebx压栈
//		000513CA  push        esi//将esi压栈
//		000513CB  push        edi//将edi压栈
// 
// 
//		//看这四条汇编指令的时候需要将显示符号名给勾选上
//		//下边这四条汇编指令的意思是在ebp的上边0E4h大小的区域里
//		//为这块区域赋初值cccccccc
//		000513CC  lea         edi, [ebp-0CCh]
//		000513D2  mov         ecx, 33h
//		000513D7  mov         eax, 0CCCCCCCCh
//		000513DC  rep stos    dword ptr es : [edi]
// 
//		int z = 0;
//	    000513DE  mov         dword ptr[ebp - 8], 0将0(转化为十进制就是0)放进[ebp-8]的位置
//
//		z = x + y;
//	    000513E5  mov         eax, dword ptr[ebp + 8]//将[ebp + 8](就是a形参中的值)中的值放入eax寄存器
//		000513E8  add         eax, dword ptr[ebp + 0Ch]//让[ebp + 0Ch](就是b形参中的值)中的值和eax中的值相加然后赋给eax
//		000513EB  mov         dword ptr[ebp - 8], eax//将eax的值赋给[ebp - 8]的值
//
//		return z;
//	    000513EE  mov         eax, dword ptr[ebp - 8]//将[ebp - 8]的值赋给eax的值
//}
//		//销毁函数栈帧
//      000513F1  pop         edi//将edi出栈
//      000513F2  pop         esi//将esi出栈
//      000513F3  pop         ebx//将ebx出栈
//      000513F4  mov         esp, ebp//将ebp的地址赋给esp
//      000513F6  pop         ebp//将ebp出栈
//      000513F7  ret//将该函数返回
//

整体过程栈帧分布图

回答开头问题


局部变量是怎么创建的?
当函数被调用时,操作系统首先为函数分配好一段内存作为函数栈帧,并在函数栈帧中开辟一块空间用于存储局部变量。

为什么局部变量的值是随机值?
因为我们为局部变量分配空间之后,会对局部变量进行初始化,编译器将随机值写入已经分配的空间中。

函数是怎么传参的?传参的顺序是怎样的?
主函数将要传递给子函数的参数存储在栈中,然后通过CALL指令调用了子函数。在子函数中,通过MOV指令获取栈顶指针并存入寄存器中,然后通过偏移量从栈中获取前两个参数值,进行计算并返回结果。最后,在主函数中恢复栈顶指针并获取子函数的返回值。
一般是从右向左。某些编程语言或者编译器可能采用其他的参数传递顺序,如从左向右或者混合顺序,因此在使用函数时应该根据具体语言和编译器的要求来传递参数。    

形参和实参是什么关系?
形参和实参的值是相同的,但是空间是独立的,形参是实参的一份临时拷贝。所以改变形参不会影响实参。

函数调用是怎么做的?
1、将函数参数压入堆栈中。在函数调用前,需要将函数的参数值按照从右到左的顺序压入堆栈中,使得函数可以在执行时访问它们。

2、使用 CALL 指令跳转到函数体开始处。在 CALL 指令中指定要跳转到的函数名称或地址。当 CPU 执行该指令时,会自动将当前代码执行的下一条指令地址(即返回地址)压入堆栈中,并跳转到函数体的开始处继续执行。

3、在函数体内部执行操作。在函数体内部,可以使用传递进来的参数进行计算和操作,并可能调用其他函数实现更复杂的功能。

4、使用 RET 指令返回。在函数执行结束后,使用 RET 指令跳转回到调用者函数的下一条指令地址,也就是之前压入堆栈中的返回地址。同时,需要将函数执行过程中所保存的寄存器状态恢复并释放相应的资源。

函数调用结束后怎么返回的?
将返回值存入寄存器,通过寄存器将值返回。因为函数调用完之后所开辟的空间会销毁。但是寄存器并没有存在内存当中,它是位于 CPU 内部的高速数据存储器件。因此不会销毁。
 

  • 5
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

袁百万

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值