栈增长方向: 栈朝着低地址方向增长,程序通过栈传递潜在数据、控制信息和数据,以及分配本地数据。
栈指针 %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