C++ 函数调用过程中栈区的变化——(栈帧、esp、ebp)

1、C++ 函数调用过程中栈区的变化

1.1、程序的内存分布

在这里插入图片描述

  • 1、代码段(.text),也称文本段(TextSegment),存放着程序的机器码只读数据,可执行指令就是从这里取得的。如果可能,系统会安排好相同程序的多个运行实体共享这些实例代码。这个段在内存中一般被标记为只读,任何对该区的写操作都会导致段错误(Segmentation Fault)
  • 2、数据段,包括已初始化的数据段(.data)未初始化的数据段(.bss)
    • data:用来存放保存全局的和静态的已初始化变量,
    • bss:后者用来保存全局的和静态的未初始化变量。数据段在编译时分配。
  • 3、堆栈段分为堆和栈:
    • 堆(Heap):用来存储程序运行时分配的变量。
      • 堆的大小并不固定,可动态扩张或缩减。其分配由malloc()、new()等这类实时内存分配函数来实现。
      • 当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);
      • 当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)
      • 堆的内存释放由应用程序去控制,通常一个new()就要对应一个delete(),如果程序员没有释放掉,那么在程序结束后操作系统会自动回收。
    • 栈(Stack)是一种用来存储函数调用时的临时信息的结构,如函数调用所传递的参数函数的返回地址函数的局部变量等。在程序运行时由编译器在需要的时候分配,在不需要的时候自动清除。
      • 栈的特性: 最后一个放入栈中的物体总是被最先拿出来,这个特性通常称为先进后出(FILO)队列。
      • 栈的基本操作: PUSH操作:向栈中添加数据,称为压栈,数据将放置在栈顶;
      • POP操作:POP操作相反,在栈顶部移去一个元素,并将栈的大小减一,称为弹栈。

堆和栈的区别:
在这里插入图片描述

1.2、函数调用过程中栈的变化解析

函数调用的另一个词语表示叫作 过程。一个过程调用包括将数据和控制从代码的一部分传递到另一部分。另外,它还必须在进入时为过程的局部变量分配空间,并在退出时释放这些空间。

而大多数机器的数据传递、局部变量的分配和释放通过操纵程序栈来实现。为单个过程(函数调用)分配的那部分栈称为栈帧

栈帧(stack frame),机器用栈来传递过程参数,存储返回信息,保存寄存器用于以后恢复,以及本地存储。栈帧其实是两个指针寄存器,寄存器%ebp为帧指针(指向该栈帧的最底部),而寄存器%esp为栈指针(指向该栈帧的最顶部)。
当程序运行时,栈指针可以移动(大多数的信息的访问都是通过帧指针的,换句话说,就是如果该栈存在,%ebp帧指针是不移动的,访问栈里面的元素可以用-4(%ebp)或者8(%ebp)访问%ebp指针下面或者上面的元素)。
总之一句话,栈帧的主要作用是用来控制和保存一个过程的所有信息的。

需要明确的是,栈是从高地址向低地址延伸的。每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息。寄存器ebp指向当前的栈帧的底部(高地址),寄存器esp指向当前的栈帧的顶部(低地址)。

void fun(void)
{
   printf("helloworld")}
void main(void)
{
  fun()
  printf("函数调用结束");
}

当程序进行函数调用的时候,我们经常说的是先将函数压栈,当函数调用结束后,再出栈。这一切的工作都是系统帮我们自动完成的。但在完成的过程中,系统会用到下面三种寄存器:EIP、ESP、EBP。

1、当调用fun函数开始时,三者的作用。

  • 1、EIP寄存器里存储的是CPU下次要执行的指令的地址。也就是调用完fun函数后,让CPU知道应该执行main函数中的printf(“函数调用结束”)语句了。
  • 2、EBP寄存器里存储的是是栈的栈底指针,通常叫栈基址,这个是一开始进行fun()函数调用之前,由ESP传递给EBP的。(在函数调用前你可以这么理解:ESP存储的是栈顶地址,也是栈底地址。)
  • 3、ESP寄存器里存储的是在调用函数fun()之后,栈的栈顶。并且始终指向栈顶。

2、当调用fun函数结束后,三者的作用:

  • 系统根据EIP寄存器里存储的地址,CPU就能够知道函数调用完,下一步应该做什么,也就是应该执行main函数中的printf(“函数调用结束”)。
  • EBP寄存器存储的是栈底地址,而这个地址是由ESP在函数调用前传递给EBP的。等到调用结束,EBP会把其地址再次传回给ESP。所以ESP又一次指向了函数调用结束后,栈顶的地址。

具体过程
在这里插入图片描述
入栈操作:push eax; 等价于 esp=esp-4,eax->[esp];如下图
在这里插入图片描述
出栈操作:pop eax; 等价于 [esp]->eax,esp=esp+4;如下图
在这里插入图片描述

我们来看下面这个C程序在执行过程中,栈的变化情况

void func(int m, int n) {
    int a, b;

    a = m;

    b = n;

}

main() {
...

    func(m, n);

L:  下一条语句

...
} 

 

在main调用func函数前,栈的情况,也就是说main的栈帧:
在这里插入图片描述

从低地址esp到高地址ebp的这块区域,就是当前main函数的栈帧。当main中调用func时,写成汇编大致是:

push m

push n; 两个参数压入栈

call func; 调用func,将返回地址填入栈,并跳转到func

在这里插入图片描述

当跳转到了func,来看看func的汇编大致的样子:

__func: push ebp; 这个很重要,因为现在到了一个新的函数,也就是说要有自己的栈帧了,那么,必须把上面的函数main的栈帧底部保存起 来,

栈顶是不用保存的,因为上一个栈帧的顶部将会是func的栈帧底部。(两栈帧相邻的)

mov ebp, esp; 上一栈帧的顶部,就是这个栈帧的底部;暂时先看现在的栈的情况
在这里插入图片描述
新的栈帧开始了

   sub esp, 8   ;  int a, b 这里声明了两个int,所以esp减小8个字节来为a,b分配空间

   mov dword ptr [esp+4], [ebp+12];   a=m

   mov dword ptr [esp], [ebp+8]; b=n         

这样,栈的情况变为:
在这里插入图片描述

 ret 8 ;  

返回,然后8是什么意思呢,就是参数占用的字节数,当返回后,esp-8,释放参数m,n的空间
  • 由此可见,通过ebp,能够很容易定位到上面的参数。当从func函数返回时,首先esp移动到栈帧底部(即释放局部变量);
  • 然后把上一个函数的栈帧底部指针弹出到ebp,再弹出返回地址到cs:ip上,esp继续移动划过参数,这样,ebp,esp就回到了调用函数前的状态,即现在恢复了原来的main的栈帧。

参考

1、https://blog.csdn.net/m0_38087936/article/details/82938006
2、https://blog.csdn.net/fang92/article/details/46494665?utm_source=blogxgwz3

  • 14
    点赞
  • 64
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
很抱歉,我作为一个语言模型AI,无法进行本地编程和调试操作。但是,我可以提供一个示例程序,供您参考。 下面是一个使用C++和汇编语言混合编程的示例程序,它使用Dev-C++编译器并进行内嵌调用。 ```c++ #include <iostream> using namespace std; extern "C" int add(int a, int b); int main() { int a = 10; int b = 20; int sum = add(a, b); cout << "The sum of " << a << " and " << b << " is " << sum << endl; return 0; } ``` 上面的程序中,我们使用了 `extern "C"` 来标识 `add` 函数,这是因为在汇编语言中,函数名可能会变得不同。在这种情况下,我们需要使用 `extern "C"` 来确保函数名在链接时正确匹配。 下面是汇编代码的实现,它将两个整数相加并返回它们的和。 ```asm section .text global add add: push ebp mov ebp, esp mov eax, [ebp+8] add eax, [ebp+12] pop ebp ret ``` 在这个例子中,我们使用了 `push` 和 `pop` 指令来保存和恢复堆栈帧指针。然后我们使用 `mov` 指令来将参数加载到 `eax` 寄存器中,并使用 `add` 指令将它们相加。最后,我们使用 `ret` 指令返回函数。 为了将这两个文件组合在一起,我们需要将汇编代码保存到一个单独的文件中,并将其编译为目标文件。然后,我们可以使用 C++ 编译器来编译主程序并将其链接到目标文件。 调试过程中,我们需要注意保留堆栈帧指针的正确性。我们还需要注意确保函数名在链接时正确匹配。如果存在问题,我们可以使用调试器和调试工具来排除问题。 希望这个示例程序可以帮助您了解使用C++和汇编语言混合编程的基础知识。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值