x86-64 内存寻址与栈帧
追踪有效地址、对齐、栈增长方向与函数帧
x86-64 内存寻址与栈帧
本博客文章以教学为目的。 示例采用 Intel 语法;精确的栈布局会随 ABI、编译器、优化与调试信息而变化。
在汇编中,player.health 可能变成 [rcx+12],values[i] 可能变成 [rdi+rsi*4]。局部变量也不是永久固定的“栈格子”,而是相对当前 rsp 或帧指针寻找的存储空间。
有效地址格式
[base + index * scale + displacement]
scale 只能是 1、2、4、8,其他项可省略。
mov eax, [rcx]
mov eax, [rcx + 12]
mov eax, [rcx + rdx*4]
mov eax, [rcx + rdx*4 + 12]
方括号内先计算成地址,再按操作数宽度访问。lea 是例外:它只返回计算出的地址。
lea rax, [rcx + 8] ; 地址
mov rax, [rcx + 8] ; 该地址的值
寄存器位没有“指针类型”标签,调试时要明确记录每个值是地址还是数据。
数组与结构
四字节 int 数组:
mov eax, dword ptr [rcx + rdx*4]
地址是 base + index * sizeof(int)。12 字节结构无法只靠 scale 表达,因此会有额外计算。
lea rax, [rdx + rdx*2]
mov ecx, [rcx + rax*4 + 4]
结构字段 offset 是二进制契约,不是字段名。字段顺序、packing、继承或 ABI 改变都可能让硬编码数字失效。
static_assert(offsetof(Player, health) == 12);
static_assert(sizeof(Player) == 16);
RIP-relative 与小端序
lea rdi, [rip + message]
mov eax, dword ptr [rip + counter]
程序以当前指令位置为基准寻找数据,不把完整绝对地址写进指令。动态链接时可能再经 ELF GOT/PLT 或 PE import table 间接访问。
0x1122334455667788 在 x86-64 内存中的顺序是:
地址 +0 +1 +2 +3 +4 +5 +6 +7
字节 88 77 66 55 44 33 22 11
端序描述多字节值的内存排列,不会反转算术或位运算。
对齐不是单一问题
x86-64 的许多 load/store 容忍未对齐,但不代表所有情况都正确且免费。
- 跨 cache line 或 page 可能增加成本。
- 某些 SIMD 形式要求对齐。
- ABI 要求函数调用边界的栈对齐。
- C/C++ 中解引用未正确对齐的 typed pointer 可能是未定义行为。
- 原子性取决于指令、宽度、对齐与边界。
栈向低地址增长
push rbx ; rsp -= 8; [rsp] = rbx
pop rbx ; rbx = [rsp]; rsp += 8
call 保存返回地址并转移控制;ret 取回地址。
call 前 RSP = S
callee 入口 RSP = S - 8
ret 后 RSP = S
即使现代 ABI 用寄存器传多数参数,返回地址仍由 call 放到栈中。
栈帧
push rbp
mov rbp, rsp
sub rsp, 32
; body
add rsp, 32
pop rbp
ret
这段保存 caller 的 rbp、建立稳定基准、分配 32 字节,再反向恢复。但帧指针并非必需;优化程序可把 rbp 当普通寄存器,甚至完全不分配栈帧。
帧可能包含局部数组、寄存器 spill、保存的 nonvolatile 寄存器、stack 参数、Windows shadow space、对齐 padding、canary 与动态分配。因此不能看到 [rsp+40] 就武断地说是“第五个局部变量”。
leaf 函数与 ABI 差异
add_three:
lea eax, [rdi + rsi]
add eax, edx
ret
不调用其他函数的 leaf 可能完全不用栈。Windows x64 caller 会预留 32 字节 shadow space。System V 没有 shadow space,但符合条件的 user-space leaf 可使用 rsp 下方 128 字节 red zone;Windows 没有这个 red zone。
把缓冲区循环读成契约
xor eax, eax
xor r8d, r8d
loop_begin:
cmp r8, rdx
jae loop_end
movsxd r9, dword ptr [rcx + r8*4]
add rax, r9
inc r8
jmp loop_begin
loop_end:
ret
[rcx+r8*4] 是 values[r8] 的候选地址,movsxd 把 signed 32 位元素扩展到 64 位。若 count > 0,caller 必须提供至少 count*4 字节可读内存。ASM 无法只从指针得知分配大小。
调试工作表
| 时点 | rsp | [rsp] 意义 | 保存状态 |
|---|---|---|---|
| 入口 | 基准 | 返回地址 | 尚无 |
| prologue 后 | 基准 - N | local/save 区 | 列出使用项 |
nested call 前 | 验证 ABI 对齐 | outgoing 区 | 保护 live 值 |
| epilogue 后 | 入口值 | 返回地址 | 全部恢复 |
栈损坏常表现为 ret 跳到无关地址、caller 的 nonvolatile 值改变、只在异常时中断 stack walk,或在对齐 SIMD 指令上 fault。
总结
x86-64 用 [base + index*scale + displacement] 建立有效地址。scale 常显示元素大小,displacement 常显示字段 offset。call 把返回地址放入栈,函数只在需要时分配 local、spill、保存与调用区。不要背一张万能帧图,而要逐指令追踪 rsp。