x86_64 堆栈的使用与调用栈分析

一、栈的基本性质

在 x86_64 架构(Linux / macOS)下:

  • 栈从 高地址向低地址增长
  • 栈顶由 rsp(stack pointer)指示
  • push → rsp -= 8
  • pop → rsp += 8

原因:

  • 早期内存布局约定(低地址给 code / data)
  • 向下增长可以避免与 heap 冲突(heap 通常向上增长)

二、Sample code

1
2
3
4
5
6
7
8
9
10
11
12
13
int add(int a, int b)
{
int c = a + b;
return c;
}

int main()
{
int x = 3;
int y = 4;
int z = add(x, y);
return z;
}

三、Disassem

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(lldb) disassem
test`main:
0x100003f80 <+0>: pushq %rbp
0x100003f81 <+1>: movq %rsp, %rbp
0x100003f84 <+4>: subq $0x10, %rsp
0x100003f88 <+8>: movl $0x0, -0x4(%rbp)
0x100003f8f <+15>: movl $0x3, -0x8(%rbp)
-> 0x100003f96 <+22>: movl $0x4, -0xc(%rbp)
0x100003f9d <+29>: movl -0x8(%rbp), %edi
0x100003fa0 <+32>: movl -0xc(%rbp), %esi
0x100003fa3 <+35>: callq 0x100003f60 ; add(int, int)
0x100003fa8 <+40>: movl %eax, -0x10(%rbp)
0x100003fab <+43>: movl -0x10(%rbp), %eax
0x100003fae <+46>: addq $0x10, %rsp
0x100003fb2 <+50>: popq %rbp
0x100003fb3 <+51>: retq
1
2
3
4
5
6
7
8
9
10
11
12
13
(lldb) disassemble --name add
test`add:
0x100003f60 <+0>: pushq %rbp
0x100003f61 <+1>: movq %rsp, %rbp
0x100003f64 <+4>: movl %edi, -0x4(%rbp)
0x100003f67 <+7>: movl %esi, -0x8(%rbp)
0x100003f6a <+10>: movl -0x4(%rbp), %eax
0x100003f6d <+13>: addl -0x8(%rbp), %eax
0x100003f70 <+16>: movl %eax, -0xc(%rbp)
0x100003f73 <+19>: movl -0xc(%rbp), %eax
0x100003f76 <+22>: popq %rbp
0x100003f77 <+23>: retq
0x100003f78 <+24>: nopl (%rax,%rax)

四、函数栈帧的建立(Prologue)

前3条指令:

1
2
3
pushq %rbp
movq %rsp, %rbp
subq $0x10, %rsp

作用:

  • 保存上一层函数的 rbp
  • 建立当前函数栈帧基址
  • 为局部变量分配 16 字节空间

注意:
即便只需要 12 字节(3 个 int),编译器仍分配 16 字节。

原因:

  • 栈 16 字节对齐(System V ABI 要求)
  • 调用其他函数前必须满足对齐约束

五、System V ABI 参数传递

在 x86_64 下(System V ABI):

前 6 个整数参数通过寄存器传递:

参数序号 寄存器
1 rdi
2 rsi
3 rdx
4 rcx
5 r8
6 r9

因此:

1
2
movl -0x8(%rbp), %edi
movl -0xc(%rbp), %esi

并不是通过栈传参。

六、call 指令的真实语义

1
callq 0x100003f60

等价于

1
2
3
rsp -= 8
*(rsp) = rip_after_call
rip = target

即:

返回地址被压入栈中。在本例中:

1
0x100003fa8 被压入栈

七、执行 call 后的栈结构

假设进入 add 之前,main 的栈帧如下:

1
2
3
4
5
6
7
8
9
10
11
12
高地址
-------------------------
return address (to caller of main)
-------------------------
saved rbp
------------------------- ← rbp (main)
x (-0x8)
y (-0xc)
z (-0x10)
padding
------------------------- ← rsp
低地址

执行 call 后:

1
2
3
4
5
6
7
8
9
10
11
高地址
-------------------------
return address to main caller
-------------------------
saved rbp (main)
------------------------- ← rbp(main)
local variables
-------------------------
return address (0x100003fa8) ← rsp
-------------------------
低地址

八、add 函数栈帧

add 的 prologue:

1
2
3
pushq  %rbp
movq %rsp, %rbp
movl %edi, -0x4(%rbp)

这里 没有 sub rsp。

该版本编译器没有显式为 add 分配额外栈空间。

本例中 add 为 leaf function。虽然未显式 sub rsp 分配空间,但由于使用了 push %rbp,局部变量仍然位于当前栈帧中,并未实际使用 red zone。
在 macOS / Linux 的 System V ABI 下:

栈顶以下 128 字节称为 red zone,可在 leaf function 中使用。

所以 add 是一个 leaf function:

  • 没有再调用其他函数
  • 不需要保持 16-byte 对齐给下一级
  • 编译器优化掉 sub rsp

栈变为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
高地址
--------------------------------
main 的局部变量
--------------------------------
saved rbp (main)
--------------------------------
return addr → main caller
--------------------------------
return addr → main ← 8(%rbp_add)
saved rbp (main) ← 0(%rbp_add)
-------------------------------- ← rbp(add)
local a copy
local b copy
local c
-------------------------------- ← rsp
低地址

注意:
每次函数调用都会:

  • 保存调用者 rbp
  • 建立新的栈帧

形成一条链。

本例中 add 为 leaf function,未显式执行 sub rsp。
在 System V ABI 下允许使用 128 字节 red zone,因此编译器可省略栈空间分配。

九、rbp 与 rsp 的角色

寄存器 作用
rsp 当前栈顶
rbp 当前函数栈帧基址

局部变量用 rbp - offset 访问。

返回地址在:

1
8(%rbp)

saved rbp 在:

1
0(%rbp)

十、ret 的行为

1
ret

等价于

1
2
rip = *(rsp)
rsp += 8

即:

  • 弹出返回地址
  • 跳转回调用点

其他

函数调用 =

  1. 保存返回地址
  2. 建立栈帧
  3. 执行函数
  4. 恢复栈帧
  5. 跳回

栈本质是:

一种严格的 LIFO 调用上下文保存机制

总结

  • 栈从高地址向低地址增长
  • call 会压入返回地址
  • 每个函数建立自己的栈帧
  • rbp 固定当前帧
  • rsp 始终指向栈顶
  • System V ABI 下参数主要走寄存器