Linux x64 汇编/栈

栈是内存中的一块特殊区域, 它根据 LIFO(先入后出) 的原理进行工作. 我相信阅读此文的读者应该都了解栈结构, 否则去编写汇编代码是没有意义的. 当 Linux 操作系统载入程序时, 栈通常被安置在内存高位, 栈向下生长; 堆则放置在内存低位, 堆向上生长. 寄存器 rsp 用作栈指针, 它总是指向栈中最顶层的元素.

我们有 16 个通用寄存器用于临时数据存储, 正如你在上文看到的, 我已经可以用它们写出等差数列求和了. 但对于严肃的应用程序来说这未免显的太少了. 为此我们可以将数据存储到栈中. 栈还有另一种用法: 当我们调用函数时, 函数的返回地址会被保存到栈上. 当函数执行结束后, 程序将从被调用函数后的下一条指令继续往下执行.

调用约定

栈的一个最重要作用就发生在函数调用中, 因此, 我会先介绍应该如何去调用函数. 对于 x64 微架构来说, 函数有两种调用约定, 分别是 Windows x64 调用约定和 System V AMD64 调用约定. 我们现在只关注第二种, 即 System V AMD64. 此约定主要在 Solaris, GNU/Linux, FreeBSD 和其他非微软操作系统上使用.

函数的前六个整数或指针参数在寄存器 rdi, rsi, rdx, rcx, r8 和 r9 中传递(但是对于系统调用, 用 r10 来替代 rcx), 然后如果还有多余的参数, 则在栈上传递. 函数的返回值被存储在 rax 中. 如果我们有如下的函数, 则前 6 个参数将在寄存器中传递, 但是第 7 和第 8 个参数将在栈上传递. 同时要注意, 第 8 个参数将先于第 7 个参数被推入堆栈.

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;
}

如果被调用的一方(callee)希望使用寄存器 rbx, rsp, rbp 和 r12-r15, 则在将控制权返回给调用方(caller)之前, 它必须恢复其原始值. 这些寄存器被称为非易失性的. 剩余的所有寄存器都是易失性的, 如果调用者希望保留这些寄存器的值, 则必须自己保存它们.

同时, 必须注意在发起 call 前, 必须确保 rsp 是 16 的倍数(16 byte aligned). 在编写汇编时有一个常用的技巧, 就是在每次 call 前面加上汇编代码 and rsp, -16, 但这并非完美, 我们将在下一篇文章中进行介绍.