跳转至

寄存器、内存与栈

运行中的程序可以粗略看成“寄存器状态 + 虚拟内存 + 操作系统资源”。寄存器速度快、数量少;内存容量大,通过地址访问;栈则是由 ABI 和指令共同维护的一段内存。

通用寄存器

x86-64 有 16 个主要的 64 位通用寄存器:

rax rbx rcx rdx rsi rdi rbp rsp
r8  r9  r10 r11 r12 r13 r14 r15
“通用”不代表没有约定。某些指令隐式使用特定寄存器,ABI 也规定参数、返回值和保存责任。 | 寄存器 | 常见角色(System V) | |---|---| | rax | 整数返回值;可变参数调用前的向量参数计数 | | rbx | callee-saved 通用寄存器 | | rcx | 第四个整数参数;移位计数使用 cl | | rdx | 第三个整数参数;乘除法高半部分 | | rsi | 第二个整数参数 | | rdi | 第一个整数参数 | | rbp | 可选帧指针;callee-saved | | rsp | 栈顶指针 | | r8/r9 | 第五、六个整数参数 | | r10/r11 | caller-saved 临时寄存器 | | r12-r15 | callee-saved 通用寄存器 | 这些是调用约定,不是永久类型。函数内部只要履行保存责任,就可以临时改变寄存器用途。

子寄存器

同一个物理寄存器可以按不同宽度访问: | 64 位 | 32 位 | 16 位 | 低 8 位 | 高 8 位(历史) | |---|---|---|---|---| | rax | eax | ax | al | ah | | rbx | ebx | bx | bl | bh | | rcx | ecx | cx | cl | ch | | rdx | edx | dx | dl | dh | 关键规则:写 32 位子寄存器会把对应 64 位寄存器高 32 位清零。

mov rax, -1       ; rax = 0xffffffffffffffff
mov eax, 1        ; rax = 0x0000000000000001
写 ax 或 al 不会清理其余高位。混用宽度时必须跟踪未被覆盖的位。 r8 到 r15 使用 r8d、r8w、r8b 等名称。sil、dil、bpl、spl 是低 8 位名称。

特殊寄存器

  • rip 保存下一条将执行指令的地址,不能像普通寄存器那样直接 mov 写入。
  • rflags 保存状态和控制标志,如 ZF、CF、SF、OF。
  • xmm0-xmm15 用于浮点和 SIMD;System V 还用它们传递浮点参数。
  • 段寄存器在 64 位用户态作用有限,但 fs/gs 常用于线程本地存储。 rip 相对寻址是位置无关代码的基础:
    lea rdi, [rip+message]
    

虚拟内存

进程使用虚拟地址,不直接使用物理内存地址。操作系统和硬件页表把虚拟页映射到物理页或文件。 每个映射通常具有权限: - r:可读 - w:可写 - x:可执行 权限组合是理解崩溃和安全保护的关键。向只读页写入或从不可执行页取指通常会触发异常,由操作系统转化为 SIGSEGV 等信号。

常见进程地址空间

典型 Linux ELF 进程包含:

高地址
+------------------+
| 栈 stack          | 向低地址增长
+------------------+
| 内存映射区         | 共享库、mmap、文件
+------------------+
| 堆 heap           | 通常向高地址扩展
+------------------+
| .bss              | 未显式初始化的全局数据
| .data             | 可写的已初始化全局数据
| .rodata           | 只读常量、字符串
| .text             | 可执行代码
+------------------+
低地址
这是概念图,不保证实际地址连续或顺序完全固定。ASLR、PIE、链接方式和加载器会影响映射位置。

节与段

ELF section 主要服务于链接和静态组织,例如 .text、.symtab。ELF segment 主要服务于运行时加载,程序头描述如何映射。 不要简单认为“一个节就是一个内存映射”。链接器可以把多个权限相同的节组合进一个可加载段。 常见节: | 节 | 典型内容 | |---|---| | .text | 机器指令 | | .rodata | 字符串和只读常量 | | .data | 有初值的可写全局变量 | | .bss | 启动时清零的全局变量,不占等量文件空间 | | .plt/.got | 动态链接相关跳板和地址表 |

内存访问

在 Intel 语法中,方括号表示访问内存:

mov rax, rbx       ; 复制寄存器值
mov rax, [rbx]     ; 从 rbx 指向的地址读取 8 字节
mov [rbx], rax     ; 向该地址写入 8 字节
操作宽度必须由寄存器或 byte/dword/qword ptr 等信息确定:
mov byte ptr [rdi], 0
mov dword ptr [rdi+4], 7
错误的地址、权限或宽度都可能造成异常或数据破坏。

栈的本质

栈是一段普通的可读写虚拟内存,rsp 按约定指向栈顶。x86-64 的栈通常从高地址向低地址增长:

push rax ; rsp -= 8,然后写入 [rsp]
pop rbx  ; 读取 [rsp],然后 rsp += 8
push/pop 只是常用操作。编译器也会直接调整 rsp:
sub rsp, 32 ; 为局部数据预留空间
add rsp, 32 ; 释放空间

call、返回地址与 ret

近调用的核心效果可以抽象为:

call target:
    rsp -= 8
    [rsp] = 下一条指令地址
    rip = target

ret:
    rip = [rsp]
    rsp += 8
返回地址因此位于栈上。若程序错误写越界并破坏控制数据,ret 可能取到错误地址。这是理解栈破坏风险的基础,不意味着所有局部数组旁边都必然紧邻返回地址。

典型栈帧

未优化代码常见如下序言和尾声:

push rbp
mov rbp, rsp
sub rsp, 32
; 函数主体
leave       ; 等价于 mov rsp, rbp; pop rbp
ret
概念布局可能是:
高地址
[rbp+8]  返回地址
[rbp]    保存的旧 rbp
[rbp-8]  局部变量
[rbp-16] 局部变量
低地址 <- rsp
优化编译器常省略帧指针、复用栈槽或完全不创建栈帧。不能把上图当作所有函数的固定模板。

栈对齐

System V AMD64 ABI 要求调用边界满足 16 字节对齐规则。由于 call 额外压入 8 字节返回地址,函数入口时 rsp 的低四位通常表现为 8。 正确对齐对 SIMD 指令、被调函数和库代码很重要。手写汇编调用 C 函数时必须计算 push 和局部空间带来的变化。

System V red zone

System V 为用户态叶函数提供 rsp 以下 128 字节的 red zone。函数若不再调用其他函数,可暂时使用这一区域而不移动 rsp。

mov [rsp-8], rax
Windows x64 没有 red zone,内核代码和使用 -mno-red-zone 等选项的目标也可能禁用它。System V ABI 要求用户态信号与中断处理程序保留该区域,因此普通 Linux 用户态信号不会破坏 red zone;不要把这一规则跨 ABI 或跨执行环境照搬。

堆与栈的区别

栈 堆
由调用约定和编译器自动管理为主 由分配器和程序显式管理为主
常存放返回地址、保存寄存器、局部对象 常存放动态生命周期对象
每个线程有自己的栈 进程内线程通常共享堆分配器状态
生命周期通常随函数调用 生命周期由 malloc/free 等决定
二者都是内存。越界、释放后使用和生命周期错误都可能发生,不能认为“堆一定比栈安全”。
## 指针、数组和结构体
C 数组索引最终变成地址计算:
int x = a[i];
可能对应:
mov eax, dword ptr [rdi+rsi*4]
其中 rdi 是数组首地址,rsi 是索引,4 是 int 大小。
结构体字段通常表现为固定偏移:
mov eax, dword ptr [rdi+8]
如果多个访问都以同一指针为基址,偏移 0、4、8、16 很可能暗示字段布局,但仍需考虑 union、继承、填充和数组。
## 空指针与非法地址
地址 0 通常不映射,因此解引用空指针会崩溃:
mov eax, [rax] ; 若 rax=0,通常触发 SIGSEGV
“非零”不代表“有效”。野指针可能指向未映射页、权限错误页、已释放对象,或一个可访问但语义错误的位置。
## 调试时查看状态
GDB 常用命令:
info registers
p/x $rax
x/8gx $rsp
x/16bx $rdi
info proc mappings
- x/8gx:按 8 个 giant word(8 字节)十六进制显示。
- x/16bx:按 16 个字节十六进制显示。
- 查看整数时注意端序和宽度;查看指针时再检查其映射权限。
## Windows x64 差异提示
Windows x64 同样使用 64 位寄存器和向低地址增长的栈,但:
- 没有 System V red zone。
- 调用者必须为被调函数预留 32 字节 shadow space。
- rdi、rsi 在 Windows ABI 中属于非易失寄存器。
- PE 节名、异常展开信息和线程环境访问方式不同。
这些差异主要来自 ABI 和操作系统,不是 mov、add 等指令本身改变了含义。
## 分析检查表
跟踪一条内存访问时,确认:
1. 基址寄存器当前保存什么?
2. 最终有效地址是多少?
3. 读取或写入多少字节?
4. 地址所在映射具有什么权限?
5. 数据属于栈、堆、全局区、代码还是映射文件?
6. 对 rsp 的修改是否保持 ABI 对齐?
7. 寄存器高位是否因子寄存器写入而保留或清零?
下一篇学习操作这些状态的指令:指令、寻址与控制流。