三代总览
先看一张三代对照表。注意最后一行——内存机制从 x86-32 到 x64 几乎没变,这正是"进阶不费力"的原因:
| 对比项 | 8086 | x86-32 | x64 / AMD64 |
|---|---|---|---|
| 位数 / 年代 | 16 位 · 1978 | 32 位 · 1985 | 64 位 · 2003 |
| 通用寄存器 | AX BX … (8×16位) | EAX EBX … (8×32位) | RAX … + R8–R15 (16×64位) |
| 地址空间 | 1 MB | 4 GB | 实际 2⁴⁸ = 256 TB |
| 工作模式 | 实模式 | 保护模式 | 长模式 |
| 寻址 / 传参 | 段:偏移 | 平坦 + 分页,栈传参 | 平坦 + 分页,寄存器传参 |
| 内存机制 | 直接用物理内存 | 虚拟内存 + 分页 | 虚拟内存 + 分页(不变) |
位数一路翻倍(16→32→64),带来更宽更多的寄存器、更大的地址空间、更快的调用;但虚拟内存、分页、进程隔离、内存映射这套机制,从 x86-32 到 x64 原样保留。所以这一页你只需要学"增量"。
寄存器之变
更宽:RAX ⊃ EAX ⊃ AX ⊃ AH / AL
64 位的 RAX 是在你熟悉的名字上"套娃":它的低 32 位就是 EAX,低 16 位是 AX,AX 再拆成 AH/AL。老名字全都还能用,只是外面多了一圈:
在 x64 里,写 32 位寄存器会自动把对应 64 位寄存器的高 32 位清零。例如 mov eax, 5 之后,RAX 的高 32 位一定是 0。但写 AX 或 AL 不会清零高位。逆向读代码时这点很关键。
更多:8 个 → 16 个通用寄存器
x64 在原来 8 个的基础上,新增了 R8–R15 共 8 个。寄存器多了,更多变量能直接放在寄存器里,少访问内存、跑得更快,也让"寄存器传参"成为可能(第 4 章):
地址空间之变
x64 的寄存器是 64 位,理论上能表示 2⁶⁴ 个地址。但现实中没那么多内存,所以当前实现只用 48 位——这已经大到难以想象:
每个进程依然是独立的一套虚拟地址空间,只是从 4GB 变成 256TB 级别。
x64 仍然用分页 + 页表把虚拟地址翻译成物理地址,概念和 x86-32 一模一样。区别是:地址变长了,一级页表装不下,于是页表从 x86-32 的 2 级变成 x64 的 4 级(PML4 → PDPT → PD → PT)。对你来说,"页号 → 页帧"的原理不变。
既然只用 48 位,x64 要求地址的高 16 位必须是第 47 位的"符号扩展",这种地址叫规范地址(Canonical)。用户态地址因此集中在低半区,内核态在高半区。
x64 新增了RIP 相对寻址——可以用"相对当前指令的偏移"来访问数据,而不必写死绝对地址。这让位置无关代码(如 DLL)更好写,你在反汇编里会经常看到 [rip+0x…]。
调用约定之变
函数之间怎么传参数,叫调用约定。寄存器变多之后,x64 干脆把"传参"从栈搬到了寄存器——这是逆向时最直观的变化:
- 参数从右往左 push 到栈上
- 调用后要清理栈
- 每次调用都要读写内存,较慢
- 前 4 个参数放
RCX RDX R8 R9 - 多余的才用栈
- 寄存器比内存快,调用更快
上面是 Windows x64 的约定(前 4 参 RCX RDX R8 R9)。Linux / macOS(System V)用 RDI RSI RDX RCX R8 R9(前 6 参)。逆向时先确认平台。
以调用 add(10, 20, 30, 40, 50) 为例,看参数是怎么交的:
兼容与不变
你的 32 位老程序,照样能跑
x64 的"长模式"里专门留了一个兼容模式;64 位 Windows 则用 WoW64 子系统来运行 32 位程序。所以你的旧 32 位 EXE/DLL 在新系统上几乎总能直接用。
从 x86-32 到 x64,变的是位数、寄存器数量、传参方式;不变的是虚拟内存、分页、按需调页、进程隔离、内存映射、EXE/DLL。也就是说,前两课讲的机制在 64 位世界依然成立,只是地址从 32 位变成 64 位。
自测一下
这 6 道题检验你是否完成了从 x86-32 到 x64 的进阶。 尚未作答
mov eax, 5 后,RAX 的高 32 位会?