1. 函数栈

程序要想运行,首先需要由操作系统负责为其创建进程,并且在进程的虚拟地址空间中为其代码段和数据段建立映射。光有代码段和数据段还不够,进程在运行过程中,还有其动态环境,其中最重要的就是堆和栈。下图是 Linux 中进程的地址空间布局:

linux-address-space.gif

代码段存储程序文本,所以也叫文本段,指令指针中的指令就从这里取得,对应图中的 text。该段是可被共享,比如在 Linux 中打开两个 Vim 编辑文本,那么一般来说这两个 Vim 共享代码段,但是数据段不同(这有点类似 C++ 中类的不同对象共享相同的成员函数)。代码段的特点是可读可执行不可写

数据段用于存储数据,包括初始化的数据和未初始化的数据(BSS 段)两部分,对应图中的 databssdata 一般存放静态非零数据和全局非零数据;bss(Block Started by Symbol)段主要存放未初始化的静态数据和全局数据。数据段的特点是可读可写不可执行

堆用于动态分配内存,malloc 等函数分配的内存就在这个区域里。堆的特点是可读可写可执行

execve(2) 负责为进程的代码段和数据段建立映射,而真正地将代码段和数据段的内容读进内存由系统的缺页异常处理程序按需完成。另外,execve(2) 还会将 bss 段清零,这就是未赋初值的全局变量和静态变量的初值为零的原因。

进程的用户空间的最高位置用于存放程序运行时的命令行参数和环境变量,在这段地址的下方和 bss 段的上方还有一个很大的“空洞”,作为进程动态运行环境的栈(stack)和堆(heap)就栖身其中,其中栈向下生长,堆向上生长。

栈中存放的是与每次函数调用对应的桢(Frame,也叫活动记录)。当函数调用发生时,新的桢被压入栈;当函数返回时,相应的桢从栈中弹出。典型的桢结构如下图所示:

frame.gif

桢的顶部为函数的实参,下面是函数的返回地址以及前一个桢的基址指针,最下面是分配给函数的局部变量使用的空间。桢通常有两个指针,其中一个为基址指针,另外一个为栈顶指针,前者所指向的位置是固定的,而后者所指向的位置在函数运行过程中可变。因此,在函数中访问实参和局部变量时,都以基址指针为基址,再加上偏移。从上图可以看出,实参的偏移为正,局部变量的偏移为负。


2. x86-64 的桢实现

下面的示例是简单的 C 程序,以及编译它生成的汇编程序:

example.c:

使用如下命令生成汇编代码:

example.s:

在解释汇编代码之前,先简单介绍这段代码用到的 x86-64 寄存器:

下面开始对这段汇编代码进行解释。

表示源代码文件为 "example.c"。

表示接下来的指令属于代码段。

定义函数 function,将其标记为全局可见,并且指定其类型为函数。

函数 function 的起始标签。

开始一个过程的调试信息。.cfi 开头的指令是调试指令,用于生成调试信息,下面不再介绍。

%rbp 寄存器的值推入栈中,保存旧的基址指针。

使用 pushq 指令时,栈指针将自动减小,以便为新数据腾出空间。可以通过 popq 指令弹出被压入栈中的数据。

将当前的栈指针 %rsp 的值复制给 %rbp,建立新的帧。

%edi%esi%edx 寄存器中的值(也就是函数的参数)分别保存在相对于 %rbp 的位置上,即在帧中分配内存。

至此,形成的帧如下所示:

stack-based-runtime-1.jpg

将保存在帧中的参数相加,最终将结果保存在 %eax 寄存器中。

popq %rbp 从栈中弹出 8 个字节(64 位),将这些字节的值加载到 %rbp 寄存器中,并且将栈指针 %rsp 的位置相应地增加 8。

此时 %rbp 保存调用方的帧的基址指针。

ret 指令从栈中弹出 call 指令压入栈中的返回地址。存储到指令指针寄存器(Instruction Pointer)中,通常是 %rip(x86-64 架构)或 %eip(x86 架构)寄存器。在 ret 指令执行后,栈指针 %rsp 被移动到返回地址下面的位置,以恢复调用函数时的栈状态。

至此,调用 function 的帧被完全销毁,函数调用的结果被保存在 %eax 寄存器中。

.size 指令的语法是:

其中 <symbol> 是函数或符号的名称,<expression> 是计算大小的表达式。.-function 表示当前位置与函数 function 起始位置之间的距离。

函数 main 与函数 function 类似,下面只介绍差异的部分。

在栈上为局部变量和临时数据分配空间,这里为它们分配 16 字节的空间。subq 指令的执行过程如下:

将立即数分别赋值给寄存器 %edx%esi%edi,用于参数传递。

call 指令的执行过程如下:

至此,形成的帧如下图所示:

stack-based-runtime-2.jpg

将函数的返回值从寄存器 %eax 移动到栈上的位置 -4(%rbp),用于存储返回值。

leave 指令的等效指令序列为:

movq %rbp, %rsp 完成局部变量的清理。popq %rbpret 完成函数 main 的退出和帧的恢复。


3. 参考文档