堆栈

堆栈是内存中的一块特殊区域, 它根据 LIFO(先入后出) 的原理进行工作.

我们有 16 个通用寄存器用于临时数据存储, 它们是 rax, rbx, rcx, rdx, rdi, rsi, rbp, rsp 和 r8-r15. 对于严肃的应用程序来说这未免显的太少了. 为此我们可以将数据存储到栈中. 堆栈还有另一种用法: 当我们调用函数时, 函数的返回地址会被保存到栈上. 当函数执行结束后, 程序将从被调用函数后的下一条指令继续往下执行.

global _start

section .text

_start:
        mov rax, 1
        call incRax
        cmp rax, 2
        jne exit
        ;;
        ;; Do something
        ;;

incRax:
        inc rax
        ret

在这里我们可以看到, 在应用程序开始运行时, rax 等于 1. 然后调用函数 incRax, 它将 rax 的值增加 1, 现在 rax 的值必须为 2. 对于 x64 微架构来说, 函数有两种调用约定, 分别是 Windows x64 调用约定和 System V AMD64 调用约定. 我们现在只关注第二种, 即 System V AMD64. 此约定主要在 Solaris, GNU/Linux, FreeBSD 和其他非微软操作系统上使用. 函数的头六个整型参数放在寄存器 rdi, rsi, rdx, rcx, r8 和 r9上; 同时 xmm0 到 xmm7 用来放置浮点参数. 对于系统调用, 用 r10 来替代 rcx. 同 Windows x64 约定一样, 其他额外的参数推入栈. 函数的返回值被保存在 rax 中.

举个例子来说, 如果我们有如下的函数:

int foo(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8)
{
    return a1 + a2 - a3 + a4 - a5 + a6 - a7 + a8;
}

前 6 个参数将在寄存器中传递, 但是 7 和第 8 个参数将在栈上传递. 同时要注意, 第 8 个参数将先于第 7 个参数被推入堆栈.

有两个有趣的寄存器, rsp 和 rbp. rbp 存储当前堆栈的栈底, rsp 则指向当前堆栈的栈顶. 用于操作堆栈的两条指令是 push 和 pop. 程序的运行内存空间, 堆从低地址往高地址方向生长, 栈则是从高地址往低地址生长. 因此, 当 push 一个数据到栈上时, rsp 将会减小.

我们来看一个简单的例子, 你能告诉我程序执行结束后, rax 的值是多少吗(提示: 正确答案是 1)?

global _start

section .text

_start:
        mov rax, 1
        mov rdx, 2
        push rax
        push rdx

        mov rax, [rsp + 8]

        ;;
        ;; Do something
        ;;