手撕虚拟内存(8)——函数栈桢原理


正如我们在之前的文章中看到的,栈驻留在虚拟内存的高地址端,并向下增长。但它究竟是如何工作的呢?如何将其转换为汇编代码?使用什么寄存器?在这一章中,我们将进一步了解栈的工作方式,以及程序如何自动分配及释放本地变量。

一旦我们理解了这一点,我们就可以对它进行一些操作,从而控制程序的执行流程。

自动分配变量

让我们先来看一个非常简单的程序,它只使用一个本地变量:


#include <stdio.h>

int main(void)
{
    int a;
    a = 972;
    printf("a = %d\n", a);
    return (0);
}


让我们编译这个程序并使用objdump对其进行反汇编:(译者注:原作者为64位系统,译者为32位系统机器,但是为了和原文中图片示意保持一致,这里直接贴上原文的结果,不像之前的文章在本地环境实践过

main函数输出汇编程序如下:


000000000040052d <main>:
  40052d:       55                      push   rbp
  40052e:       48 89 e5                mov    rbp,rsp
  400531:       48 83 ec 10             sub    rsp,0x10
  400535:       c7 45 fc cc 03 00 00    mov    DWORD PTR [rbp-0x4],0x3cc
  40053c:       8b 45 fc                mov    eax,DWORD PTR [rbp-0x4]
  40053f:       89 c6                   mov    esi,eax
  400541:       bf e4 05 40 00          mov    edi,0x4005e4
  400546:       b8 00 00 00 00          mov    eax,0x0
  40054b:       e8 c0 fe ff ff          call   400410 <printf@plt>
  400550:       b8 00 00 00 00          mov    eax,0x0
  400555:       c9                      leave  
  400556:       c3                      ret    
  400557:       66 0f 1f 84 00 00 00    nop    WORD PTR [rax+rax*1+0x0]
  40055e:       00 00


现在我们先来看看前三行:


000000000040052d <main>:
  40052d:       55                      push   rbp
  40052e:       48 89 e5                mov    rbp,rsp
  400531:       48 83 ec 10             sub    rsp,0x10


函数的第一行涉及rbp和rsp;这些是专用寄存器。rbp是指向当前栈桢底部的基指针,rsp是指向当前栈桢顶部的堆栈指针。(译者注:在很多翻译过来的书上,有些地方将Stack翻译为栈桢,有的地方叫堆栈,只要知道这里的堆栈是指Stack,Heap没关系就好)

Step1:

让我们一步一步地分解这里发生的事情。这是我们在进入main函数之前,准备运行其第一条指令时栈的状态:


函数栈桢原理_寄存器

函数栈桢原理_寄存器_02

Step2:

push rbp指令将寄存器rbp的值推入堆栈。因为它“push”到堆栈上,所以现在rsp的值是堆栈新顶部的内存地址。堆栈和寄存器现在看起来是这样的:


函数栈桢原理_cpu_03

函数栈桢原理_cpu_04

Step3:

mov rbp, rsp,将堆栈指针rsp的值复制到基指针rbp -> rbp和rsp现在都指向堆栈的顶部。


函数栈桢原理_寄存器_05

函数栈桢原理_3c_06

Step4:

sub rsp, 0x10创建一个空间来存储本地变量的值。rbp和rsp之间的空间就是这个空间。注意,这个空间足够大,可以存储类型为integer的变量。


函数栈桢原理_cpu_07

函数栈桢原理_堆栈_08

我们刚刚在内存中为本地变量在堆栈上创建了一个空间。这个空间称为栈帧。每个具有局部变量的函数都将使用栈桢来存储这些变量。

使用局部变量(给局部变量赋值)

main函数的第4行汇编代码如下:


400535:       c7 45 fc cc 03 00 00    mov    DWORD PTR [rbp-0x4],0x3cc


0x3cc实际上是十六进制中的值972。这行对应于C代码行:


a = 972;


mov DWORD PTR [rbp-0x4],0x3cc:将内存地址为rbp- 4的内容设置为972。[rbp - 4]是我们的局部变量a。计算机实际上并不知道我们在代码中使用的变量的名称,它只是引用栈上的内存地址。

这是这个操作之后堆栈和寄存器的状态:


函数栈桢原理_3c_09

函数栈桢原理_cpu_10

leave,自动释放

如果我们看一下函数的末尾,我们会发现:


400555:       c9                      leave


指令leave将rsp设置为rbp,然后将堆栈顶部弹出到rbp中。


函数栈桢原理_堆栈_11

函数栈桢原理_寄存器_12

函数栈桢原理_3c_13

函数栈桢原理_堆栈_14

因为我们在进入函数时将rbp的上一个值推入堆栈,所以rbp现在被设置为rbp的上一个值。这就是:

  • 局部变量释放
  • 在我们离开当前函数之前,将恢复上一个函数的栈桢。

堆栈和寄存器rbp和rsp的状态恢复到进入main函数时的状态。