架构、数据与数值表示¶
阅读汇编前,需要先理解 CPU 看见的不是“变量”和“对象”,而是有限宽度的位、地址和指令。高级语言赋予数据类型,指令则只规定按多宽、以何种方式解释这些位。
从源码到运行¶
一个典型 C 程序经历以下阶段:
- 编译器把高级语言语义转换成目标架构指令。 - 汇编器把助记符编码成机器码并生成目标文件。 - 链接器合并目标文件,解析符号和重定位,连接库。 - 操作系统加载器映射 ELF 段,准备栈和入口地址。 - CPU 从rip 指向的位置取指、译码并执行。
汇编语言不是跨架构的统一语言。x86-64、AArch64、RISC-V 的寄存器和编码不同;本章只讨论 x86-64。
ISA、微架构与 ABI¶
这三个概念经常混淆: - ISA(指令集架构)规定软件可见的指令、寄存器、地址和异常行为。 - 微架构**是某款 CPU 实现 ISA 的内部方式,例如流水线、缓存和乱序执行。 - **ABI(应用二进制接口)规定函数参数、寄存器保存、数据布局和目标文件等软件约定。 两款处理器可以实现同一 x86-64 ISA,却有不同性能;两个系统可以都运行 x86-64,却因 ABI 不同而不能直接混用目标文件。
位、字节和常见宽度¶
- 1 bit 是一个二进制位,值为 0 或 1。
- 1 byte 通常是 8 bit。
- x86 术语中的 word 是 16 bit,doubleword 是 32 bit,quadword 是 64 bit。
- x86-64 的“64 位”主要表示通用寄存器宽度和地址模型,不代表所有数据都是 64 位。
| 名称 | 位数 | 字节数 | Intel 常见标记 |
|---|---:|---:|---|
| byte | 8 | 1 |
byte ptr| | word | 16 | 2 |word ptr| | dword | 32 | 4 |dword ptr| | qword | 64 | 8 |qword ptr| 例如mov eax, dword ptr [rdi]从内存读取 4 字节,而不是 8 字节。
二进制、十进制和十六进制¶
二进制适合描述位,十六进制适合紧凑显示机器数据。每个十六进制数字恰好对应 4 bit:
常见前缀: - 二进制:0b1010
- 十六进制:0x2a
- 十进制:42
进制只改变书写方式,不改变底层位模式。42、0x2a 和 0b101010 是同一个数值。
快速换算时可按四位分组:
无符号整数¶
一个 n 位无符号整数的范围是:
8 位无符号整数范围为 0 到 255。固定宽度运算溢出后保留低 n 位: CPU 会通过进位标志CF 记录无符号意义上的进位或借位,但是否把它当作错误由程序决定。
二进制补码与有符号整数¶
x86 使用二进制补码表示有符号整数。n 位补码范围是:
求一个正数的相反数,可以按固定宽度执行“逐位取反再加一”: 同一位模式取决于解释方式: 加法器不需要知道数据是否有符号;区别主要体现在扩展、除法和条件判断中。溢出:CF 与 OF¶
CF 和 OF 表达不同问题:
- CF:无符号运算是否产生最高位之外的进位或借位。
- OF:有符号结果是否超出当前宽度可表示范围。
8 位示例:
ja 和 jg 随意互换。
零扩展与符号扩展¶
把较窄整数放入较宽位置时,必须决定高位如何补齐: - 零扩展在高位补 0,适合无符号值。 - 符号扩展复制原符号位,保持有符号值不变。
movzx eax, byte ptr [rdi] ; 0xff -> 255
movsx eax, byte ptr [rdi] ; 0xff -> -1
movsxd rax, eax ; 32 位有符号扩展到 64 位
小端序¶
x86-64 使用小端序:多字节值的最低有效字节放在最低地址。
把 0x12345678 以 32 位整数写到地址 0x1000:
| 地址 | 字节 |
|---|---|
| 0x1000 | 78 |
| 0x1001 | 56 |
| 0x1002 | 34 |
| 0x1003 | 12 |
调试器若按字节查看会显示 78 56 34 12,按 32 位整数查看仍是 0x12345678。端序改变字节排列,不改变单个字节内的位顺序。
端序对字符串通常不明显,因为字符数组按地址顺序存放;它主要影响整数、指针和网络协议字段的解释。
位运算¶
常见位运算直接作用于位模式:
| 运算 | 作用 | 常见用途 |
|---|---|---|
| and | 两位都为 1 才为 1 | 掩码、清位、对齐 |
| or | 任一位为 1 即为 1 | 置位、合并标志 |
| xor | 两位不同为 1 | 翻转位、清零寄存器 |
| not | 每一位取反 | 构造补码 |
| shl | 逻辑左移 | 乘以 2 的幂、移位 |
| shr | 逻辑右移,高位补 0 | 无符号除以 2 的幂 |
| sar | 算术右移,复制符号位 | 有符号缩放近似 |
xor eax, eax 常用于把 eax 置零,并同时清零 rax 高 32 位。
字符、字符串与布尔值¶
字符在内存中也是整数编码。ASCII 中:
C 字符串是以0x00 结尾的字节序列。机器指令不会自动知道数组长度;若代码依赖终止符,就必须保证终止符存在且访问不越界。
C 的布尔判断通常把 0 视为假、非 0 视为真,但比较指令常通过 setcc 生成规范的 0 或 1。
浮点数概览¶
常见 float 和 double 使用 IEEE 754 表示,包含符号、指数和尾数。它们不是补码整数,不能直接用整数比较规则解释。
x86-64 现代代码通常使用 XMM 寄存器和 SSE/AVX 指令:
NaN、正负无穷和舍入误差。反汇编中看到 ucomiss/ucomisd 时,应额外考虑无序比较和 NaN。
指针与地址¶
指针是一个被程序解释为地址的整数位模式。在常见 x86-64 用户态进程中,指针占 8 字节,但并非全部 64 位组合都是有效规范地址。 要区分三个概念: - 地址:某个虚拟内存位置的编号。 - 地址中保存的值:需要访问内存才能得到。 - 符号:链接器或调试信息为地址提供的名称。
对齐¶
若对象地址是其对齐值的整数倍,就称对象对齐。例如 8 字节对象常按 8 字节边界放置。 对齐可以改善访问效率,也是 ABI 对结构体和栈布局的一部分。编译器可能插入填充字节:
value 往往位于偏移 4,结构体大小往往是 8,而不是 5。逆向时不能仅把字段大小相加推断布局。
机器码与变长指令¶
x86-64 指令长度可变,通常为 1 到 15 字节。前缀、操作码、ModR/M、SIB、位移和立即数共同构成编码。 这意味着: - 指令地址不会固定增加 4。 - 从错误字节位置开始反汇编可能得到另一串看似有效的指令。 - 数据与代码混合时,线性扫描可能误判。 - 正确的控制流和入口点对反汇编非常重要。 初学阶段不必手工记忆编码,但应知道反汇编器是在解释字节,不是在恢复源码。
C 与汇编的数据宽度¶
在常见 Linux x86-64 LP64 模型中:
| C 类型 | 常见宽度 |
|---|---:|
| char | 1 字节 |
| short | 2 字节 |
| int | 4 字节 |
| long | 8 字节 |
| 指针 | 8 字节 |
Windows x64 使用 LLP64:long 仍是 4 字节,指针是 8 字节。跨平台逆向时,这是结构体和 API 参数分析的重要差异。
分析检查表¶
看到一个立即数或内存值时,依次确认:
1. 它的操作宽度是多少?
2. 它被当作有符号、无符号、指针还是浮点数?
3. 是否发生截断、零扩展或符号扩展?
4. 内存字节是否按小端序重组?
5. 运算后代码检查的是 CF、OF、ZF 还是其他标志?
6. 数据布局是否包含对齐和填充?
下一篇把这些位和值放入实际运行位置:寄存器、内存与栈。