寄存器、内存与栈¶
运行中的程序可以粗略看成“寄存器状态 + 虚拟内存 + 操作系统资源”。寄存器速度快、数量少;内存容量大,通过地址访问;栈则是由 ABI 和指令共同维护的一段内存。
通用寄存器¶
x86-64 有 16 个主要的 64 位通用寄存器:
“通用”不代表没有约定。某些指令隐式使用特定寄存器,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 位清零。
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相对寻址是位置无关代码的基础:
虚拟内存¶
进程使用虚拟地址,不直接使用物理内存地址。操作系统和硬件页表把虚拟页映射到物理页或文件。
每个映射通常具有权限:
- r:可读
- w:可写
- x:可执行
权限组合是理解崩溃和安全保护的关键。向只读页写入或从不可执行页取指通常会触发异常,由操作系统转化为 SIGSEGV 等信号。
常见进程地址空间¶
典型 Linux ELF 进程包含:
高地址
+------------------+
| 栈 stack | 向低地址增长
+------------------+
| 内存映射区 | 共享库、mmap、文件
+------------------+
| 堆 heap | 通常向高地址扩展
+------------------+
| .bss | 未显式初始化的全局数据
| .data | 可写的已初始化全局数据
| .rodata | 只读常量、字符串
| .text | 可执行代码
+------------------+
低地址
节与段¶
ELF section 主要服务于链接和静态组织,例如 .text、.symtab。ELF segment 主要服务于运行时加载,程序头描述如何映射。
不要简单认为“一个节就是一个内存映射”。链接器可以把多个权限相同的节组合进一个可加载段。
常见节:
| 节 | 典型内容 |
|---|---|
| .text | 机器指令 |
| .rodata | 字符串和只读常量 |
| .data | 有初值的可写全局变量 |
| .bss | 启动时清零的全局变量,不占等量文件空间 |
| .plt/.got | 动态链接相关跳板和地址表 |
内存访问¶
在 Intel 语法中,方括号表示访问内存:
操作宽度必须由寄存器或byte/dword/qword ptr 等信息确定:
错误的地址、权限或宽度都可能造成异常或数据破坏。
栈的本质¶
栈是一段普通的可读写虚拟内存,rsp 按约定指向栈顶。x86-64 的栈通常从高地址向低地址增长:
push/pop 只是常用操作。编译器也会直接调整 rsp:
call、返回地址与 ret¶
近调用的核心效果可以抽象为:
返回地址因此位于栈上。若程序错误写越界并破坏控制数据,ret 可能取到错误地址。这是理解栈破坏风险的基础,不意味着所有局部数组旁边都必然紧邻返回地址。
典型栈帧¶
未优化代码常见如下序言和尾声:
概念布局可能是: 优化编译器常省略帧指针、复用栈槽或完全不创建栈帧。不能把上图当作所有函数的固定模板。栈对齐¶
System V AMD64 ABI 要求调用边界满足 16 字节对齐规则。由于 call 额外压入 8 字节返回地址,函数入口时 rsp 的低四位通常表现为 8。
正确对齐对 SIMD 指令、被调函数和库代码很重要。手写汇编调用 C 函数时必须计算 push 和局部空间带来的变化。
System V red zone¶
System V 为用户态叶函数提供 rsp 以下 128 字节的 red zone。函数若不再调用其他函数,可暂时使用这一区域而不移动 rsp。
-mno-red-zone 等选项的目标也可能禁用它。System V ABI 要求用户态信号与中断处理程序保留该区域,因此普通 Linux 用户态信号不会破坏 red zone;不要把这一规则跨 ABI 或跨执行环境照搬。
堆与栈的区别¶
| 栈 | 堆 |
|---|---|
| 由调用约定和编译器自动管理为主 | 由分配器和程序显式管理为主 |
| 常存放返回地址、保存寄存器、局部对象 | 常存放动态生命周期对象 |
| 每个线程有自己的栈 | 进程内线程通常共享堆分配器状态 |
| 生命周期通常随函数调用 | 生命周期由 malloc/free 等决定 |
| 二者都是内存。越界、释放后使用和生命周期错误都可能发生,不能认为“堆一定比栈安全”。 | |
| ## 指针、数组和结构体 | |
| C 数组索引最终变成地址计算: | |
| 可能对应: | |
其中 rdi 是数组首地址,rsi 是索引,4 是 int 大小。 |
|
| 结构体字段通常表现为固定偏移: | |
| 如果多个访问都以同一指针为基址,偏移 0、4、8、16 很可能暗示字段布局,但仍需考虑 union、继承、填充和数组。 | |
| ## 空指针与非法地址 | |
| 地址 0 通常不映射,因此解引用空指针会崩溃: | |
| “非零”不代表“有效”。野指针可能指向未映射页、权限错误页、已释放对象,或一个可访问但语义错误的位置。 | |
| ## 调试时查看状态 | |
| GDB 常用命令: | |
- 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. 寄存器高位是否因子寄存器写入而保留或清零? | |
| 下一篇学习操作这些状态的指令:指令、寻址与控制流。 |