探究栈帧的奥妙

目录

探究栈帧的奥妙

引言

浅浅说一下栈

问问自己几个问题

什么是栈帧

栈帧的维护

汇编预备知识

小例子

访问栈帧里的数据

例子

栈帧是如何切换的

栈帧是如何处理参数和返回值的


探究栈帧的奥妙

作者申明:

文中有些名词可能不太官方,大部分是作者自己的理解,只是为了方便理解,所以用了一些不太专业的词汇,如果有错误,欢迎各位佬指正!

引言

本文我们要讲解的是栈帧,为了较好的引入它,本文使用的C语言函数作为载体,默认看文章的大家C语言函数的基本使用都没有问题啊,如何检测自己的C语言有没有问题呢?可以看看下面的代码,如何还行的话,那表示这篇文章对你是大有帮助的(只要不是太小白都可以的)。注意:需要读者知道内存分配栈区的概念!这很重要

#include 
​
int Add(int x, int y)
{
    int sum = x + y;
    return sum;
}
​
int main()
{
    int a = 10;
    int b = 20;
    int sum = Add(a, b);
    printf("sum=%d\n", sum);
    
    return 0;
}

十分简单的一段程序对吧,OK,那么现在就以这一小段函数为例来讲解一下栈帧

浅浅说一下栈

使用的内存是有分区的:

  • OS区

  • 栈区

  • 堆区

  • 全局区

  • 共享区

  • 代码区

  • 数据区

针对本文,只需了解栈区即可,我们分配的空间是有地址的,地址是有大小的,而栈区的特点是从高地址向低地址开始分配内存,即对于栈区来说,高地址是栈底,低地址是栈顶

堆栈相对而生,箭头是资源申请的方向

如图

探究栈帧的奥妙_第1张图片

问问自己几个问题

  • 函数是如何调用的

  • 参数是如何传入的

  • 函数是如何返回的

  • 函数返回后是怎么找到之前调用的地方的

如何你能回答上来,那么你的高度可能在这篇文之上,回答不上来,相信看了这篇文章就可以回答上来了,哈哈哈

什么是栈帧

在我们进行函数调用的时候,编译器都会在栈区为这个函数维护一个栈帧,即每一个函数对应着一个栈帧,这是概念,那么我们通俗的来讲讲什么事栈帧。首先明确一点,我们在进行变量创建的时候,编译器是给这个变量分配了对应的虚拟内存的,即创建变量时,需要一定的开销,而这个开销一部分就是体现在内存占用上,那么回到函数调用这里来,和创建变量类似,函数也是一个类型,在进行调用的时候同样也需要一定的内存开销,这个函数占用的内存空间就是这个函数的栈帧,即栈帧=占用内存。

探究栈帧的奥妙_第2张图片

栈帧的维护

介绍了什么是栈帧,系统是如何来得知这个函数的栈帧大小和所在栈区的位置呢,系统使用了两个寄存器来维护栈帧的范围:

  • EBP(extended base pointer) 基址指针寄存器,存放当前栈帧的底部地址

  • ESP(extended stack pointer) 栈指针寄存器,存放当前栈帧的顶部地址

注意:这两个寄存器的内容是动态变化的,同一时刻只会调用一个函数,即同一时刻只会一一个栈帧,用上面的例子来看,最开始是调用的main函数,此时ebp和esp里面存放的就是main函数的栈帧,之后调用了Add函数,这时ebp和esp里面存放的是Add函数的栈帧,Add返回main后,esp和esp又重新开始维护main函数的栈帧。

这里有几个细节,栈帧是如何创建的,ebp和esp在main和Add之间切换栈帧的时候,如何切换的,ebp和esp又是如何再一次的维护main栈帧的,这些都是值得探讨的问题。

汇编预备知识

  • mov:eax, ebx 将ebx的值赋值给eax

  • push:xxx 将xxx压入栈,esp-4

  • pop:xxx 将栈顶元素出栈,写入xxx,再让esp+4

  • call:函数名 将IP旧值压栈保存(栈顶低地址处),设置IP新值,新函数的指令执行地址

  • ret:从函数的栈帧顶部找到IP旧值,将其出栈并恢复IP寄存器

补充:IP寄存器存放的是程序下一次要执行的指令的地址

小例子

push eax:#将寄存器eax的值压栈
push 985:#将立即数985压栈
push [ebp+8]:#将主存地址[ebp+8]里的数据压栈
    
pop eax:#栈顶元素出栈,写入寄存器eax
pop [ebp+8]:#栈顶元素出栈,写入主存地址[ebp+8]

访问栈帧里的数据

我们知道esp&ebp分别指向栈顶和栈底,那么我们可以直接通过mov指令来访问栈帧里面的数据,只需有esp和ebp分别+/-就可以获得栈帧中的地址

例子

sub esp,12  #栈顶指针-12
mov [esp+8],eax #将eax的值复制到主存[esp+8]

栈帧是如何切换的

这里要探讨的问题是,栈帧是如何切换的,也就是如何从main函数突然就执行Add函数的

main:
push ebp
mov ebp,esp
...... #省略不重要的部分
call Add #将当前IP值压栈,重新设置IP值
......
​
Add:
push ebp     #把ebp的值压栈到栈顶
mov ebp,esp  #将esp的值复制给ebp  这两个指令等价于enter指令
......
leave        #等价于 mov esp, ebp \ pop ebp
ret
int add(int x, int y)
{
    return x + y;
}

int main()
{
    int x = 10;
    int y = 20;

    add(x, y);

    return 0;
}

当执行add(x, y)时,底层汇编翻译的指令是

call add

当add即将返回时,底层汇编翻译的指令是

leave

ret

上面main和add的栈帧切换,当执行call add的时候,会先将当前的IP寄存器的值压入栈帧,然后再设置IP的值,使其下一次执行的指令跳到add函数栈帧中,再将main函数栈帧的ebp寄存器值压入栈帧,再将esp的值复制给ebp,再依次执行add函数体中指令,就完成了栈帧切换。

add函数执行完后,会执行leave指令将ebp的值复制给esp,然后在Pop ebp,即将栈顶元素出栈,再将其出栈的内容复制给ebp,而这里的内容刚好是main函数栈帧的基地址,然后再执行ret指令将IP的值恢复到原来的值,这样就完成了栈帧的切换。

探究栈帧的奥妙_第3张图片

栈帧是如何处理参数和返回值的

返回值处理,一般返回值只有一个,所以处理的时候,一般是用一个通用寄存器(eax)来临时记录一下值,然后再复值给需要的变量。

将栈帧划分为几个区域,分别负责存放函数中的局部变量,函数体的指令,函数的参数

探究栈帧的奥妙_第4张图片

你可能感兴趣的:(C语言,c语言)