栈增长方向: 栈朝着低地址方向增长,程序通过栈传递潜在数据、控制信息和数据,以及分配本地数据。
栈指针 %rsp: 栈指针始终指向栈顶(当前最低地址),是操作栈的核心。
关键作用:
在绘制栈的结构时,我们将低地址放在上面称为“bottom”,高地址放在底部称为“top”(不知道为什么,反正大家都这样画),初始时栈顶指针位于最低端,数据先进后出。
pushq Src(将四字压入栈)。
操作数Src可以是一个寄存器,立即数或一个内存地址中的值。
之后%rsp会减少8字节,而操作数压入了原%rsp的地址。
popq Dest(从栈中取出四字)
从%rsp当前位置取出四字存储到Dest,然后%rsp增加8字节。
在 x86-64 架构中,过程调用(function call) 和 返回(return) 使用栈来管理调用的顺序和返回的地址。我们需要用到call和ret语句:
| 指令 | 描述 | 
|---|---|
| call Label | 过程调用 | 
| call *Operand | 过程调用 | 
| ret | 从过程调用中返回 | 
在反汇编过程得到的代码中的后缀-q只是用来强调这是x86-64版本的调用和返回。
0000000000400540 <multstore>:
  // x in %rdi, y in %rsi, dest in %rdx
    ...
  400541: mov    %rdx,%rbx
  400544: callq  400550 <mult2>
  // t in %rax
  400549: mov    %rax,(%rbx)
    ...
0000000000400550 <mult2>:
  // a in rdi, b in %rsi
  400550: mov    %rdi,%rax
  400553: imul   %rsi,%rax
  // s in %rax
    ...
  400557: retq
我们此时假设栈顶%rsp位于0x120,%rip=0x400544(%rip是程序计数器,与rest in peace无关)表示当前指令地址位于0x544,即call指令;
之后,call指令将下一个指令地址0x400549写入栈顶,同时栈顶%rsp减少8为变为0x118,而%rip记录实际调用的指令地址:0x400550;
当到达ret指令时,会假定栈顶有一个可以跳转的地址,弹出该地址后使程序继续从该地址运行。
对于程序中的数据流(Procedure Data Flow),前六个参数(Arg 1...Arg 6)会被存放在%rdi,%rsi,%rdx,%rcx,%r8,%r9六个寄存器中,返回值位于寄存器%rax,而之后的参数会被存储在栈中。
注意:仅在需要时分配栈空间 (Only allocate stack space when needed)。
前述汇编代码的源代码如下:
void multstone(long x, long y, long *dest){
    long t = mult2(x, y);
    *dest = t;
}
long mult2(long, a, long b){
    long s = a * b;
    return s;
}
如C、Pascal、Java等支持递归的语言,其代码必须是 可重入(Reentrant) 的,,即允许对同一过程的多次同时实例化。于是需要某些地方存储每个实例化的状态,包括参数、局部变量以及返回指针。
栈的规则是后进先出(LIFO),不论是数据还是指令(地址)。
栈帧 (Stack Frame):
%rbp其指向当前栈帧的基地址(当前栈帧的“底”部)栈帧使用时,进入函数时会分配一部分空间,通过call指令进行压栈操作;返回时则会释放空间,通过ret代码进行出栈操作。

在返回地址处,会有一个可选的帧指针%rbp。
long incr(long *p, long val){
    long x = *p;
    long y = x + val;
    *p = y;
    return x;
}
incr:
    movq    (%rdi),%rax   # %rdi : p
    addq    %rax,%rsi     # %rsi : val, y
    movq    %rsi,(%rdi)   # %rax : x
    ret
如上是函数incr的代码和指令。现在我们来看call_incr函数:
long call_incr() {
    long v1 = 15213;
    long v2 = incr(&v1, 3000);
    return v1 + v2;
}
call_incr:
    subq $16, %rsp
    movq $15213, 8(%rsp)
    movl $3000, %esi
    leaq 8(%rsp), %rdi
    call incr
    addq 8(%rsp), %rax
    addq $16, %rsp
    ret
首先,第一行long v1 = 15213;生成了两条指令:
subq $16, %rsp:对%rsp进行减法操作,在栈中分配了16字节空间;
movq $15213, 8(%rsp):将栈的前8字节分配给参数v1,剩余8字节(当前%rsp)未被使用。
leaq 8(%rsp), %rdi将v1的地址存放在%rdi中,作为指针(即*p)传入incr函数。
之后,通过call incr调用incr函数,并把v1也加到%rax上返回。
最后,addq $16, %rsp释放内存。
对于函数的调用关系,调用其他函数的函数称为调用者(caller)、被调用的函数称为被调用者(callee)。
管理寄存器的方法包括:Caller Saved和Callee Saved.
因此按照约定:
%rax:caller-saved,存储返回值。
%rdi,... , %r9:caller-saved,参数
%r10, %r11:caller-saved,可以被任意函数修改的临时值
%rbx,%r12,%r13,%r14:callee-saved,使用前必须存储其原值,并且在使用后恢复其值。
%rbp:callee-saved,同上,也可作为帧指针。
%rsp:callee-saved,%rsp的值在函数调用过程中可以改变(e.g. 分配栈空间或者回收栈空间),但在函数返回时,%rsp的值必须恢复到原始值。
我们以pcount函数的递归版本为例:
long pcount_r(unsigned long x){
    if (x == 0){
        return 0;
    }
    else{
        return (x & 1) + pcount_r(x >> 1);
    }
}
pcount_r:
    movl    $0, %eax
    testq   %rdi, %rdi
    je      .L6
    pushq   %rbx
    movq    %rdi, %rbx
    andl    $1, %ebx
    shrq    %rdi
    call    pcount_r
    addq    %rbx, %rax
    popq    %rbx
.L6:
    rep;    ret