手撕虚拟内存(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函数之前,准备运行其第一条指令时栈的状态:
Step2:
push rbp指令将寄存器rbp的值推入堆栈。因为它“push”到堆栈上,所以现在rsp的值是堆栈新顶部的内存地址。堆栈和寄存器现在看起来是这样的:
Step3:
mov rbp, rsp,将堆栈指针rsp的值复制到基指针rbp -> rbp和rsp现在都指向堆栈的顶部。
Step4:
sub rsp, 0x10创建一个空间来存储本地变量的值。rbp和rsp之间的空间就是这个空间。注意,这个空间足够大,可以存储类型为integer的变量。
我们刚刚在内存中为本地变量在堆栈上创建了一个空间。这个空间称为栈帧。每个具有局部变量的函数都将使用栈桢来存储这些变量。
使用局部变量(给局部变量赋值)
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。计算机实际上并不知道我们在代码中使用的变量的名称,它只是引用栈上的内存地址。
这是这个操作之后堆栈和寄存器的状态:
leave,自动释放
如果我们看一下函数的末尾,我们会发现:
400555: c9 leave
指令leave将rsp设置为rbp,然后将堆栈顶部弹出到rbp中。
因为我们在进入函数时将rbp的上一个值推入堆栈,所以rbp现在被设置为rbp的上一个值。这就是:
- 局部变量释放
- 在我们离开当前函数之前,将恢复上一个函数的栈桢。
堆栈和寄存器rbp和rsp的状态恢复到进入main函数时的状态。