x86汇编与逆向工程学习笔记
书籍信息:《x86汇编与逆向工程:软件破解与防护的艺术》
作者:斯蒂芬妮·多马斯、克里斯托弗·多马斯
笔记数量:77个笔记
📑 目录
1. 译者序
1.1 x86架构概述
x86架构是一种基于**CISC(Complex Instruction Set Computing,复杂指令集计算)**思想设计的处理器架构。
CISC vs RISC:
- CISC架构:包含大量指令,可以执行复杂的操作
- 浮点运算
- 字符串操作
- 位操作
- RISC架构:精简指令集,指令简单但执行效率高
2. x86架构基础
2.1 x86简介
2.1.1 历史发展
x86架构已经存在了几十年,并且这些年来有了很大发展。这种架构最初是在1974年由英特尔(Intel)推出的。
x86历史上的主要里程碑:
| 处理器 | 推出年份 | 位数 | 说明 |
|---|---|---|---|
| Intel 8080 | 1974 | 8位 | 首个x86系列微处理器 |
| Intel 8086 | 1978 | 16位 | 奠定了x86架构基础 |
| Intel 80386 | 1985 | 32位 | 引入32位架构 |
| Intel Prescott / AMD Opteron / Athlon 64 | 2003-2004 | 64位 | 进入64位时代 |
2.1.2 向后兼容性
在近50年的历史中,x86架构不断加入新的特性,同时仍保持向后兼容。即使有些特性被认为无人使用,也从未被从系统中移除。
💡 重要特性:针对1978年发布的Intel 8086处理器编写的程序,现在依然可以在最新的x86芯片上运行,无需修改!
2.1.3 架构文档
最新的Intel软件开发者手册已超过5000页,但也只是初步揭示了这个架构的能力。
🔗 参考资源:Intel软件开发者手册
2.1.4 术语说明
- x86:所有从Intel 8086 16位架构演变出来的架构的总称
- Intel 80286架构(16位和32位)
- Intel 80386架构(增加了64位架构)
- x64:特指x86的64位版本
2.2 汇编语法
2.2.1 语法的重要性
虽然指令集架构(ISA)定义了诸如寄存器、数据格式和机器指令等因素,但它并未规定语法。
关键点:
- 汇编语言的语法完全由汇编器确定
- 没有通用的汇编语言标准语法
- 没有特定的x86汇编语法
- 汇编语法存在成百上千种变体
2.2.2 主流语法
大多数x86汇编工具使用两种主流的x86语法:
- AT&T语法
- Intel语法
在这两个主要分支下,有数百种特定于汇编器的变体。
2.2.3 选择Intel语法的原因
本书选择Intel语法,原因如下:
| 原因 | 说明 |
|---|---|
| Intel支持 | Intel是占据主导地位的处理器开发商,他们使用的是Intel语法 |
| 工具使用 | 大部分主要的逆向工程工具(如IDA Pro)都使用Intel语法 |
| 可读性 | 人们普遍认为,Intel语法比AT&T语法更清晰、易读、易写 |
语法对比示例:
; Intel语法(本书使用)
mov eax, 5
mov eax, [ebx]
add eax, ebx
; AT&T语法(对比)
movl $5, %eax
movl (%ebx), %eax
addl %ebx, %eax
2.3 数据表示
2.3.1 基本单位
**位(bit)是计算机使用的基本单位,但位太小,提供的应用空间有限。因此,计算机将字节(byte)**作为最小的内存单元来运作。
位 (bit) → 字节 (byte) → 字 (word)
单位关系:
- 1字节 = 8位
- 字(word):计算机一次最佳访问的字节数量,通常是2的幂
2.3.2 数据单位术语表
| 术语 | 位数 | 说明 |
|---|---|---|
| 位(bit) | 1 | 取0或1 |
| 半字节(nibble) | 4 | 半个字节 |
| 字节(byte) | 8 | 基本内存单元 |
| 双字节 | 16 | 两个字节 |
| 字(word) | 16(x86) | 取决于架构 |
| 双字(DWORD) | 32 | 两个字 |
| 四字(QWORD) | 64 | 四个字 |
| 八字(octoword) | 128 | 八个字 |
2.3.3 x86架构的特殊性
⚠️ 重要:在传统的32位架构中,一个字是32位。但这是x86架构的一个独特之处。
由于x86保持了与原始16位架构的向后兼容性:
- x86架构中,一个字是16位
- 一个双字(DWORD)是32位
传统32位架构:字 = 32位
x86架构: 字 = 16位,双字 = 32位
2.3.4 有效位
在二进制数中:
- LSB(Least Significant Bit,最低有效位):最右边的位
- MSB(Most Significant Bit,最高有效位):最左边的位
示例:00000000000000000000000000011001
MSB ←─────────────────────────────── LSB
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 1
2³¹ 2⁰
2.3.5 字节序(Endianness)
字节序描述了这些字节在内存中的存储顺序。
小端序(Little-Endian):
- 最低有效字节先被存储(在最低地址处)
- x86架构使用小端序
大端序(Big-Endian):
- 最高有效字节先被存储(在最低地址处)
示例:存储32位值 0x12345678
小端序(x86):
地址: 0x1000 0x1001 0x1002 0x1003
数据: 78 56 34 12
(LSB) (MSB)
大端序:
地址: 0x1000 0x1001 0x1002 0x1003
数据: 12 34 56 78
(MSB) (LSB)
💡 提示:x86是小端序系统,数据的最低有效字节位于基地址偏移0处。这对人类来说看起来是"反向"的,因为我们按照大端序进行阅读和写作。
2.4 寄存器
2.4.1 寄存器概述
寄存器为处理器提供了高速访问数据的途径。由于寄存器在物理上位于CPU内部,它们的延迟远低于内存。
CPU内部寄存器(纳秒级) << 内存访问(微秒级)
寄存器分类:
| 类别 | 名称 | 说明 | 是否可写 |
|---|---|---|---|
| 通用寄存器 | GPR | 用于一般数据、地址等的存储,可以直接操作 | ✅ 是 |
| 特殊寄存器 | SPR | 用于存储程序状态 | ❌ 否(只读) |
2.4.2 通用寄存器(32位)
通用寄存器在应用程序中承担了大部分重要的工作,负责存储从内存中获取的数据,进行数据处理,并存储计算结果。
每个通用寄存器都可以存储32位的数据。每个寄存器都有一个传统角色并且以角色命名,但通用寄存器可以被用于任何目的。
32位通用寄存器列表:
| 寄存器 | 全称 | 传统用途 | 示例 |
|---|---|---|---|
| eax | 累加器(Accumulator) | 保存算术运算的结果 | eax += ebx |
| ebx | 基址(Base) | 存储内存块的基址 | [ebx+5] 访问数组第5个元素 |
| ecx | 计数器(Counter) | 用于计数 | for(i=0; i<10; i++) 中的 i |
| edx | 数据(Data) | 存储数据 | sub edx, 7 |
| esi | 源索引(Source Index) | 存储源数组中的索引 | array[i] = array[k] 中的 k |
| edi | 目标索引(Destination Index) | 存储目标数组的索引 | array[i] = array[k] 中的 i |
| ebp | 基址指针(Base Pointer) | 存储当前栈帧的基址 | 栈帧管理 |
| esp | 栈指针(Stack Pointer) | 存储当前栈帧顶部的地址 | 栈操作 |
代码示例:
; eax - 累加器示例
mov eax, 10
add eax, 5 ; eax = 15
; ebx - 基址寄存器示例
mov ebx, 0x1000 ; 数组基址
mov eax, [ebx+5] ; 访问数组第5个元素(偏移5)
; ecx - 计数器示例
mov ecx, 10 ; 循环计数器
loop_start:
; 循环体
dec ecx ; ecx--
jnz loop_start ; 如果ecx != 0,继续循环
; esi/edi - 字符串/数组操作示例
mov esi, source_array ; 源数组地址
mov edi, dest_array ; 目标数组地址
mov ecx, 100 ; 复制100个元素
rep movsb ; 重复复制字节
2.4.3 特殊寄存器
特殊寄存器用于特定任务,并且不允许直接修改。
| 寄存器 | 全称 | 功能 | 说明 |
|---|---|---|---|
| eip | 指令指针(Instruction Pointer) | 存储下一条要执行的指令的地址 | 不能直接修改(如 mov eip, 1 无效) |
| eflags | 标志(Flags) | 存储标志,保存系统状态和先前执行指令的结果 | 标志值为真或假 |
错误示例:
; ❌ 错误:不能直接修改eip
mov eip, 1 ; 这行代码无法汇编
; ✅ 正确:使用通用寄存器
mov eax, 1 ; 这行代码可以汇编
2.4.4 寄存器命名规则
32位寄存器的命名:
- 所有32位寄存器名称都以字母
e开头(表示"扩展") - 这些32位寄存器是从原始的16位寄存器中"扩展"出来的
寄存器层次结构:
32位寄存器(eax) → 16位寄存器(ax) → 8位寄存器(ah, al)
示例:eax寄存器的各个部分:
eax = 0x01234567
┌─────────────────────────────────────┐
│ eax (32位) │
│ 0x01234567 │
├─────────────────────────────────────┤
│ ah (高8位) │ al (低8位) │
│ 0x45 │ 0x67 │
├──────────────────┴──────────────────┤
│ ax (16位) │
│ 0x4567 │
└─────────────────────────────────────┘
访问规则:
- 32位:
eax,ebx,ecx,edx,esi,edi,ebp,esp - 16位:删除
e前缀 →ax,bx,cx,dx,si,di,bp,sp - 8位(仅限eax, ebx, ecx, edx):
- 低8位:
al,bl,cl,dl(l = low) - 高8位:
ah,bh,ch,dh(h = high)
- 低8位:
代码示例:
mov eax, 0x01234567
; 此时:
; eax = 0x01234567
; ax = 0x4567
; ah = 0x45
; al = 0x67
mov al, 0xFF ; 只修改低8位,eax变成0x012345FF
mov ax, 0x0000 ; 只修改低16位,eax变成0x01230000
2.4.5 64位寄存器
在64位的x86架构中,所有的指令和行为与32位的x86一样。不过,64位的架构有更多、更大的寄存器。
64位寄存器命名规则:
- 对于32位x86中存在的寄存器,64位版本把
e换成reax→raxebx→rbxecx→rcx- 等等...
- 32位名称仍然可以获取寄存器的低32位
ax,al,ah等名称的用法保持不变
64位新增寄存器:
r8,r9,r10,r11,r12,r13,r14,r15
寄存器对比表:
| 32位 | 64位 | 说明 |
|---|---|---|
| eax | rax | 累加器 |
| ebx | rbx | 基址 |
| ecx | rcx | 计数器 |
| edx | rdx | 数据 |
| esi | rsi | 源索引 |
| edi | rdi | 目标索引 |
| ebp | rbp | 基址指针 |
| esp | rsp | 栈指针 |
| - | r8-r15 | 64位新增寄存器 |
2.5 内存访问
2.5.1 为什么需要内存访问
32位(或64位)的系统只有有限的寄存器。忽略用于追踪栈的特殊寄存器和通用寄存器(如esp和ebp)后,只剩下六个可用于一般计算的寄存器(eax、ebx、ecx、edx、esi和edi)。
这就是程序还需要能够读取内存数据和向内存中写入数据的原因。
2.5.2 内存访问语法
在x86汇编Intel语法中,内存访问是使用 [] 符号来表示的。
语法格式:
[地址表达式]
示例:
; 绝对地址访问
mov eax, [0x12345678] ; 将地址0x12345678处的32位值加载到eax
; 使用寄存器存储地址
mov ebx, 0x12345678
mov eax, [ebx] ; 将ebx指向的地址处的值加载到eax
; 基址加偏移
mov eax, [ebx+4] ; 访问ebx+4地址处的值
; 索引寻址
mov eax, [ebx+edi*4] ; 访问ebx+edi*4地址处的值
2.5.3 数据长度说明
💡 提示:传统上,32位的x86架构应该有32位的字。然而,为了向后兼容16位的x86架构,所以字的长度是16位,而双字则是32位。
数据长度由目标寄存器决定:
mov eax, [0x100] ; 移动32位(双字)到eax
mov ax, [0x100] ; 移动16位(字)到ax
mov al, [0x100] ; 移动8位(字节)到al
2.6 寻址模式
x86架构支持多种寻址模式,用于灵活地访问内存中的数据。
2.6.1 绝对寻址
绝对寻址采用固定值来指定地址。
语法:
[固定地址]
示例:
; 十进制地址
mov eax, [1]
; 十六进制地址
mov eax, [0x1234]
; 算术运算结果
mov eax, [0x1337+0777]
; 使用标签
mov eax, [my_variable]
代码示例:
section .data
my_variable dd 42 ; 定义一个32位变量,值为42
section .text
mov eax, [0x1000] ; 访问绝对地址0x1000
mov ebx, [0x1337+0x100] ; 访问计算后的地址
mov ecx, [my_variable] ; 使用标签访问
2.6.2 间接寻址
间接寻址使用寄存器来指定地址。
语法:
[寄存器]
规则:
- ✅ 可以使用16位通用寄存器:
[ax],[bx],[cx],[dx],[si],[di],[bp],[sp] - ✅ 可以使用32位通用寄存器:
[eax],[ebx],[ecx],[edx],[esi],[edi],[ebp],[esp] - ❌ 不能使用8位通用寄存器:
[al],[ah],[bl]等 - ❌ 不能使用特殊寄存器:
[eip],[eflags]等
示例:
mov ebx, 0x12345678
mov eax, [ebx] ; ✅ 正确:使用32位寄存器
mov ecx, [eax] ; ✅ 正确:eax存储地址,访问该地址的值
; mov edx, [al] ; ❌ 错误:不能使用8位寄存器寻址
2.6.3 基址加偏移量寻址
基址加偏移量寻址用于访问数组等通过基址和偏移量在内存中储存的变量。
语法:
[基址寄存器 + 偏移量]
示例:
; 访问数组元素
mov ebx, array_base ; 数组基址
mov eax, [ebx+0] ; 访问第0个元素
mov eax, [ebx+4] ; 访问第1个元素(假设每个元素4字节)
mov eax, [ebx+8] ; 访问第2个元素
mov eax, [ebx+20] ; 访问第5个元素(偏移20字节)
; 访问结构体成员
mov ebx, struct_ptr
mov eax, [ebx+0] ; 第一个成员
mov ecx, [ebx+4] ; 第二个成员(偏移4字节)
实际应用:
; C代码:int arr[10]; arr[5] = 100;
; 汇编实现:
mov ebx, arr ; 数组基址
mov dword [ebx+20], 100 ; arr[5] = 100(假设int是4字节,5*4=20)
2.6.4 索引寻址
索引寻址使用一个索引寄存器、一个比例因子和一个偏移量来指定地址。
语法:
[索引寄存器 * 比例因子 + 偏移量]
比例因子:必须是 1、2、4或8
适用场景:当数组中的元素大于一个字节时,手动计算偏移量很烦琐,索引寻址更方便。
示例:
; 访问int数组(每个元素4字节)
mov esi, 2 ; 索引 = 2
mov eax, [esi*4] ; 访问arr[2],比例因子4(int大小)
; 访问short数组(每个元素2字节)
mov esi, 3 ; 索引 = 3
mov ax, [esi*2] ; 访问arr[3],比例因子2(short大小)
; 访问char数组(每个元素1字节)
mov esi, 5 ; 索引 = 5
mov al, [esi*1] ; 访问arr[5],比例因子1(char大小)
; 或简写为:
mov al, [esi] ; 比例因子1可以省略
; 带偏移量的索引寻址
mov ebx, array_base ; 数组基址
mov esi, 2 ; 索引
mov eax, [ebx+esi*4] ; 访问array_base[2]
对比:手动计算 vs 索引寻址:
; ❌ 手动计算(容易出错)
mov esi, 2
mov eax, esi
shl eax, 2 ; 乘以4(左移2位)
add eax, array_base
mov eax, [eax] ; 访问arr[2]
; ✅ 索引寻址(简洁清晰)
mov esi, 2
mov eax, [array_base+esi*4] ; 直接访问arr[2]
2.6.5 基址-索引寻址
基址-索引寻址组合了索引寻址和基址加偏移量寻址的元素。
语法:
[基址寄存器 + 索引寄存器 * 比例因子 + 偏移量]
组成要素:
- 基址寄存器:存储数组或结构体的基址
- 索引寄存器:存储索引值
- 比例因子:1、2、4或8(对应数据类型大小)
- 偏移量:可选的固定偏移
示例:
; 完整形式
mov ebx, array_base ; 基址
mov edi, 2 ; 索引
mov eax, [ebx+edi*4+0x1000] ; 访问array_base[2],额外偏移0x1000
; 实际应用:二维数组访问
; C代码:int matrix[10][10]; matrix[2][3] = 100;
; 假设:matrix[i][j] = matrix_base + (i*10 + j)*4
mov ebx, matrix ; 基址
mov esi, 2 ; 行索引 i
mov edi, 3 ; 列索引 j
; 计算:i*10*4 + j*4 = (i*10 + j)*4
mov eax, esi
mov ecx, 10
mul ecx ; eax = i*10
add eax, edi ; eax = i*10 + j
mov ecx, 4
mul ecx ; eax = (i*10 + j)*4
add ebx, eax ; ebx = matrix + offset
mov dword [ebx], 100 ; matrix[2][3] = 100
; 或使用基址-索引寻址(更简洁)
mov ebx, matrix
mov esi, 2
mov edi, 3
; 使用lea计算地址
lea eax, [ebx+esi*40+edi*4] ; 40 = 10*4(每行10个int)
mov dword [eax], 100
寻址模式总结表:
| 寻址模式 | 语法 | 示例 | 用途 |
|---|---|---|---|
| 绝对寻址 | [地址] | [0x1000] | 访问固定地址 |
| 间接寻址 | [寄存器] | [eax] | 通过寄存器存储的地址访问 |
| 基址+偏移 | [基址+偏移] | [ebx+4] | 访问数组元素或结构体成员 |
| 索引寻址 | [索引*比例+偏移] | [esi*4] | 访问数组,自动计算偏移 |
| 基址-索引 | [基址+索引*比例+偏移] | [ebx+edi*4+0x1000] | 复杂数据结构访问 |
3. x86指令集
3.1 x86指令格式
3.1.1 操作数类型
x86指令的操作数可以是:
| 操作数类型 | 说明 | 示例 |
|---|---|---|
| 寄存器 | 通用寄存器 | eax, ebx, ecx |
| 立即数 | 数字或常数 | 5, 0x1234, 12345 |
| 内存位置 | 由地址指定 | [0x1000], [eax], [ebx+4] |
3.1.2 重要限制
⚠️ 关键规则:虽然x86指令可以包含上述任何内容,但最多只能包含一个内存位置。
示例:
; ✅ 正确:只有一个内存位置
mov eax, [0x1000] ; 寄存器 ← 内存
mov [0x1000], eax ; 内存 ← 寄存器
mov eax, 5 ; 寄存器 ← 立即数
add eax, ebx ; 寄存器 ← 寄存器
; ❌ 错误:两个内存位置
; mov [0x1000], [0x2000] ; 不能直接内存到内存
; add [eax], [ebx] ; 不能两个操作数都是内存
; ✅ 解决方法:通过寄存器中转
mov eax, [0x2000] ; 先加载到寄存器
mov [0x1000], eax ; 再写入目标内存
3.2 常用x86指令
x86汇编语言包括数百种指令。以下是最常用的指令分类:
指令分类表
| 类别 | 指令 | 说明 |
|---|---|---|
| 算术指令 | add, sub, mul, div, inc, dec | 数学运算 |
| 位操作指令 | and, or, xor, not, shl, shr, sal, sar | 位运算和移位 |
| 栈指令 | call, ret, push, pop | 函数调用和栈操作 |
| 数据移动 | mov, lea | 数据传输 |
| 执行流程 | jmp, 条件跳转指令 | 控制流 |
| 比较指令 | test, cmp | 比较操作 |
| 其他 | nop | 无操作 |
3.2.1 mov(数据移动)
功能:将数据从一个位置移动到另一个位置(实际上是复制,不是移动)。
语法:
mov destination, source
重要说明:
- 尽管名字叫"移动",但实际上是复制数据
- 源数据不会被移除,而是被复制到目标位置
示例:
; 立即数到寄存器
mov eax, 5 ; eax = 5
; 寄存器到寄存器
mov ebx, eax ; ebx = eax
; 内存到寄存器
mov eax, [0x100] ; eax = 内存地址0x100处的32位值
mov dx, [0x100] ; dx = 内存地址0x100处的16位值
; 寄存器到内存
mov [0x100], eax ; 将eax的值写入内存地址0x100
; 立即数到内存
mov dword [0x100], 42 ; 将42写入内存地址0x100(32位)
; 使用寄存器存储地址
mov ebx, 0x12345678
mov eax, [ebx] ; 将ebx指向的地址处的值加载到eax
数据长度说明:
mov eax, [0x100] ; 移动32位(双字)到eax
mov ax, [0x100] ; 移动16位(字)到ax
mov al, [0x100] ; 移动8位(字节)到al
内存布局示例:
地址 数据(十六进制)
0x100 78 56 34 12 ; 小端序存储的32位值0x12345678
mov eax, [0x100] ; eax = 0x12345678
mov ax, [0x100] ; ax = 0x5678
mov al, [0x100] ; al = 0x78
3.2.2 inc、dec(递增/递减)
功能:将指定的值增加或减少1。
语法:
inc operand ; operand = operand + 1
dec operand ; operand = operand - 1
等价于:
inc≈ C语言中的i++或++idec≈ C语言中的i--或--i
操作数:可以是寄存器或内存地址(只需要一个操作数)
示例:
; 寄存器操作
mov eax, 5
inc eax ; eax = 6
dec eax ; eax = 5
; 内存操作
mov dword [0x12345678], 10
inc dword [0x12345678] ; 内存地址0x12345678处的值变为11
dec dword [0x12345678] ; 内存地址0x12345678处的值变为10
; 循环计数器
mov ecx, 10
loop_start:
; 循环体代码
dec ecx ; ecx--
jnz loop_start ; 如果ecx != 0,继续循环
对比C代码:
// C代码
int i = 5;
i++; // i = 6
i--; // i = 5
// 对应的汇编
mov eax, 5
inc eax ; i++
dec eax ; i--
3.2.3 add、sub(加法/减法)
功能:对特定值进行加法或减法计算。
语法:
add destination, value ; destination = destination + value
sub destination, value ; destination = destination - value
操作数:
- destination:必须是寄存器或内存地址(会被修改)
- value:可以是寄存器、内存地址或立即数
示例:
; 寄存器 + 立即数
mov eax, 10
add eax, 5 ; eax = 15
sub eax, 3 ; eax = 12
; 寄存器 + 寄存器
mov eax, 10
mov ebx, 5
add eax, ebx ; eax = 15
; 内存 + 立即数
mov dword [0x1000], 20
add dword [0x1000], 10 ; 内存地址0x1000处的值变为30
; 寄存器 + 内存
mov eax, 5
add eax, [0x1000] ; eax = eax + [0x1000]
; 复杂表达式
mov eax, 100
mov ebx, 50
add eax, ebx ; eax = 150
sub eax, 25 ; eax = 125
对比C代码:
// C代码
int a = 10;
int b = 5;
a += b; // a = 15
a -= 3; // a = 12
// 对应的汇编
mov eax, 10 ; a
mov ebx, 5 ; b
add eax, ebx ; a += b
sub eax, 3 ; a -= 3
3.2.4 mul(乘法)
功能:执行无符号整数乘法运算。
语法:
mul operand
特点:
- 只接受一个操作数
- 隐式使用
eax寄存器作为另一个操作数 - 结果存储在
edx:eax中(64位结果)
工作原理:
edx:eax = eax * operand
结果存储:
- 低32位:存储在
eax中 - 高32位:存储在
edx中 - 即使结果小于32位,
edx和eax也会被修改
示例:
; 32位乘法:eax * operand → edx:eax
mov eax, 10
mov ebx, 5
mul ebx ; edx:eax = 10 * 5 = 50
; eax = 50, edx = 0
; 更大的数
mov eax, 0x10000 ; 65536
mov ebx, 0x10000 ; 65536
mul ebx ; edx:eax = 65536 * 65536 = 4294967296
; eax = 0, edx = 1 (因为结果超过32位)
; 使用内存操作数
mov eax, 20
mov dword [0x1000], 3
mul dword [0x1000] ; edx:eax = 20 * 3 = 60
; eax = 60, edx = 0
64位结果示例:
计算:0x10000 * 0x10000 = 0x100000000
结果:
edx = 0x00000001 (高32位)
eax = 0x00000000 (低32位)
edx:eax = 0x0000000100000000
对比C代码:
// C代码(32位)
unsigned int a = 10;
unsigned int b = 5;
unsigned long long result = (unsigned long long)a * b;
// result = 50
// 对应的汇编
mov eax, 10 ; a
mov ebx, 5 ; b
mul ebx ; edx:eax = a * b
; 如果需要64位结果,edx:eax已经包含了
3.2.5 div(除法)
功能:执行无符号除法运算。
语法:
div operand
特点:
- 只接受一个操作数(除数)
- 隐式使用
edx:eax作为被除数(64位) - 结果存储在
eax(商)和edx(余数)中
工作原理:
被除数:edx:eax (64位)
除数:operand
商:eax = (edx:eax) / operand
余数:edx = (edx:eax) % operand
重要提示:
- 即使余数是零,
edx也会被修改 - 对于32位除法,如果被除数小于64位,需要先将
edx清零
示例:
; 32位除法:edx:eax / operand → eax(商), edx(余数)
mov eax, 10 ; 被除数
mov edx, 0 ; 高32位清零(重要!)
mov ebx, 3 ; 除数
div ebx ; eax = 10 / 3 = 3, edx = 10 % 3 = 1
; 5除以2
mov eax, 5
mov edx, 0 ; 必须清零edx
mov ebx, 2
div ebx ; eax = 2 (商), edx = 1 (余数)
; 64位除法
mov eax, 0x00000000 ; 低32位
mov edx, 0x00000001 ; 高32位 (edx:eax = 0x100000000 = 4294967296)
mov ebx, 0x10000 ; 除数 = 65536
div ebx ; eax = 65536 (商), edx = 0 (余数)
; 使用内存操作数
mov eax, 100
mov edx, 0
mov dword [0x1000], 7
div dword [0x1000] ; eax = 14, edx = 2
常见错误:
; ❌ 错误:忘记清零edx
mov eax, 10
div ebx ; edx可能包含垃圾数据,导致错误结果
; ✅ 正确:先清零edx
mov eax, 10
mov edx, 0 ; 清零高32位
div ebx ; 正确执行
对比C代码:
// C代码
unsigned int a = 10;
unsigned int b = 3;
unsigned int quotient = a / b; // 3
unsigned int remainder = a % b; // 1
// 对应的汇编
mov eax, 10 ; a
mov edx, 0 ; 清零edx(重要!)
mov ebx, 3 ; b
div ebx ; eax = 商, edx = 余数
3.2.6 and、or、xor(位运算)
功能:执行布尔位运算。
语法:
and destination, source ; destination = destination & source
or destination, source ; destination = destination | source
xor destination, source ; destination = destination ^ source
操作数:
- destination:必须是寄存器或内存地址(会被修改)
- source:可以是寄存器、内存地址或立即数
真值表:
| A | B | AND | OR | XOR |
|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 |
| 0 | 1 | 0 | 1 | 1 |
| 1 | 0 | 0 | 1 | 1 |
| 1 | 1 | 1 | 1 | 0 |
示例:
; AND操作
mov eax, 0x12345678
and eax, 0x0000FFFF ; eax = 0x00005678 (保留低16位)
; OR操作
mov eax, 0x12340000
or eax, 0x00005678 ; eax = 0x12345678 (设置低16位)
; XOR操作
mov eax, 0x12345678
xor eax, 0xFFFFFFFF ; eax = 0xEDCBA987 (按位取反)
; 清零寄存器(常用技巧)
xor eax, eax ; eax = 0 (比 mov eax, 0 更高效)
; 设置所有位为1
mov eax, 0
or eax, 0xFFFFFFFF ; eax = 0xFFFFFFFF
; 掩码操作
mov eax, 0x12345678
and eax, 0x1 ; 只保留最低位,eax = 0x0
and dword [0xdeadbeef], 0x1 ; 掩盖除最低位外的所有位
; 内存操作
mov dword [0x1000], 0x12345678
and dword [0x1000], 0xFFFF0000 ; 只保留高16位
常用技巧:
; 技巧1:快速清零
xor eax, eax ; eax = 0 (1字节指令,比mov eax, 0快)
; 技巧2:按位取反
mov eax, 0x12345678
xor eax, 0xFFFFFFFF ; eax = ~0x12345678
; 技巧3:设置特定位
mov eax, 0
or eax, 0x00000001 ; 设置最低位为1
; 技巧4:清除特定位
mov eax, 0xFFFFFFFF
and eax, 0xFFFFFFFE ; 清除最低位
; 技巧5:切换特定位
mov eax, 0x12345678
xor eax, 0x00000001 ; 切换最低位
对比C代码:
// C代码
unsigned int a = 0x12345678;
a &= 0x0000FFFF; // 保留低16位
a |= 0x00000001; // 设置最低位
a ^= 0xFFFFFFFF; // 按位取反
// 对应的汇编
mov eax, 0x12345678
and eax, 0x0000FFFF
or eax, 0x00000001
xor eax, 0xFFFFFFFF
3.2.7 not(按位取反)
功能:计算值的补码(按位取反)。
语法:
not operand ; operand = ~operand
说明:
- 把所有的
0变成1 - 把所有的
1变成0 - 只接受一个操作数
示例:
; 8位寄存器
mov ch, 0xAA ; ch = 10101010
not ch ; ch = 01010101 = 0x55
; 32位寄存器
mov eax, 0x12345678
not eax ; eax = 0xEDCBA987
; 内存操作
mov dword [2020], 0xFFFFFFFF
not dword [2020] ; [2020] = 0x00000000
; 实际应用
mov eax, 0
not eax ; eax = 0xFFFFFFFF (所有位为1)
NOT vs XOR:
; 方法1:使用NOT
mov eax, 0x12345678
not eax ; eax = ~0x12345678
; 方法2:使用XOR(需要立即数)
mov eax, 0x12345678
xor eax, 0xFFFFFFFF ; eax = ~0x12345678 (相同效果)
对比C代码:
// C代码
unsigned char ch = 0xAA;
ch = ~ch; // ch = 0x55
unsigned int a = 0x12345678;
a = ~a; // a = 0xEDCBA987
// 对应的汇编
mov ch, 0xAA
not ch
mov eax, 0x12345678
not eax
3.2.8 shr、shl(逻辑移位)
功能:执行逻辑移位操作。
语法:
shr register, count ; 向右逻辑移位(零扩展)
shl register, count ; 向左逻辑移位(零扩展)
说明:
- shr:向右移位,左边用
0填充(零扩展) - shl:向左移位,右边用
0填充 - count:移位的位数(立即数)
移位效果:
shr(右移):
原值:1011 0100
右移1位:0101 1010 (左边补0)
shl(左移):
原值:1011 0100
左移1位:0110 1000 (右边补0)
示例:
; 右移
mov eax, 0x12345678
shr eax, 1 ; eax = 0x091A2B3C (除以2)
shr eax, 4 ; eax = 0x01234567 (除以16)
; 左移
mov eax, 0x12345678
shl eax, 1 ; eax = 0x2468ACF0 (乘以2)
shl eax, 4 ; eax = 0x23456780 (乘以16)
; 8位寄存器
mov al, 0b10101010 ; al = 0xAA
shl al, 1 ; al = 0x54
shr al, 2 ; al = 0x15
; 内存操作
mov dword [0x1000], 0x12345678
shr dword [0x1000], 8 ; [0x1000] = 0x00123456
应用:快速乘除:
; 乘以2的幂
mov eax, 10
shl eax, 1 ; eax = 20 (10 * 2)
shl eax, 2 ; eax = 40 (10 * 4)
shl eax, 3 ; eax = 80 (10 * 8)
; 除以2的幂
mov eax, 80
shr eax, 1 ; eax = 40 (80 / 2)
shr eax, 2 ; eax = 10 (80 / 4)
shr eax, 3 ; eax = 5 (80 / 8)
对比C代码:
// C代码
unsigned int a = 0x12345678;
a >>= 4; // 右移4位
a <<= 2; // 左移2位
// 对应的汇编
mov eax, 0x12345678
shr eax, 4
shl eax, 2
3.2.9 sar、sal(算术移位)
功能:执行算术移位操作。
语法:
sar register, count ; 向右算术移位(符号扩展)
sal register, count ; 向左算术移位
说明:
- sar:向右移位,符号扩展(复制最高有效位)
- sal:向左移位,与
shl相同(零扩展) - 关键区别:
sar进行符号扩展,shr进行零扩展
移位效果对比:
逻辑右移(shr):
原值:1000 0000 (0x80, 有符号-128)
右移3位:0001 0000 (0x10, 无符号16)
算术右移(sar):
原值:1000 0000 (0x80, 有符号-128)
右移3位:1111 0000 (0xF0, 有符号-16) ← 符号位被复制
示例:
; 左移:sal和shl相同
mov al, 0b00000100 ; al = 4
shl al, 3 ; al = 0b00100000 = 32
sal al, 3 ; al = 0b00100000 = 32 (相同)
; 右移:sar和shr不同(对于有符号数)
mov al, 0b10000000 ; al = 0x80 (有符号-128)
shr al, 3 ; al = 0b00010000 = 0x10 (无符号16)
mov al, 0b10000000 ; 重新设置
sar al, 3 ; al = 0b11110000 = 0xF0 (有符号-16)
; 32位有符号数
mov eax, 0x80000000 ; eax = -2147483648 (最小32位有符号数)
sar eax, 1 ; eax = 0xC0000000 = -1073741824
shr eax, 1 ; eax = 0x40000000 = 1073741824 (错误!)
应用场景:
; 有符号数除以2的幂(使用sar)
mov eax, -128
sar eax, 1 ; eax = -64 (正确)
; 无符号数除以2的幂(使用shr)
mov eax, 128
shr eax, 1 ; eax = 64 (正确)
; 错误示例:有符号数使用shr
mov eax, -128
shr eax, 1 ; eax = 2147483584 (错误!应该是-64)
对比C代码:
// C代码
int a = -128; // 有符号数
a >>= 1; // 算术右移,a = -64
unsigned int b = 128; // 无符号数
b >>= 1; // 逻辑右移,b = 64
// 对应的汇编
mov eax, -128
sar eax, 1 ; 有符号右移
mov eax, 128
shr eax, 1 ; 无符号右移
3.2.10 nop(无操作)
功能:无操作指令,不执行任何操作。
语法:
nop
说明:
- 一字节的操作码:
0x90 - 不执行任何操作
- 不修改任何寄存器或标志位
合法应用场景:
| 场景 | 说明 |
|---|---|
| 时间调整 | 精确控制执行时间 |
| 内存对齐 | 对齐代码或数据到特定边界 |
| 风险防控 | 防止某些硬件问题 |
| 分支延迟槽 | RISC架构中的延迟槽填充 |
| 占位符 | 为未来补丁预留空间 |
安全领域应用:
- 黑客攻击
- 软件破解
示例:
; 时间延迟
nop
nop
nop ; 三个nop指令,用于精确时间控制
; 内存对齐
align 4 ; 对齐到4字节边界
nop ; 如果需要,填充nop
; 占位符
some_function:
; 功能代码
nop ; 预留位置,未来可能替换为其他指令
nop
ret
代码对齐示例:
section .text
; 对齐到16字节边界
align 16
nop ; 如果需要,填充nop指令
nop
; ... 其他代码
3.2.11 lea(加载有效地址)
功能:加载有效地址(Load Effective Address)。
语法:
lea destination, [source]
说明:
- 计算
source操作数的地址,并将其放在destination处 source必须是内存地址表达式destination可以是寄存器或内存地址- 类似于C语言中的
&操作符
重要特点:
lea不访问内存,只计算地址- 可以用于快速算术运算(不访问内存的地址计算)
示例:
; 基本用法:获取变量地址
mov ebx, 0x1000
lea eax, [ebx] ; eax = 0x1000 (ebx的值,不是[ebx]指向的值)
; 获取数组元素地址
mov ebx, array_base
lea eax, [ebx+4] ; eax = array_base + 4
; 索引寻址
mov ebx, array_base
mov esi, 2
lea eax, [ebx+esi*4] ; eax = array_base + 2*4 = array_base + 8
; 复杂地址计算
mov ebx, 0x1000
mov edi, 5
lea eax, [ebx+edi*4+0x100] ; eax = 0x1000 + 5*4 + 0x100 = 0x1114
LEA vs MOV 对比:
mov ebx, 0x1000
; MOV:访问内存
mov eax, [ebx] ; eax = 内存地址0x1000处的值
; LEA:只计算地址
lea eax, [ebx] ; eax = 0x1000 (地址本身,不访问内存)
LEA的巧妙用法:快速算术运算:
; 使用LEA进行快速乘法(不访问内存)
mov eax, 10
lea ebx, [eax*4] ; ebx = 40 (10 * 4,不访问内存,比mul快)
; 复杂计算
mov eax, 5
lea ebx, [eax*2+eax] ; ebx = 15 (5*2 + 5 = 15,快速计算)
lea ecx, [eax*8+3] ; ecx = 43 (5*8 + 3 = 43)
对比C代码:
// C代码
int arr[10];
int *ptr = &arr[2]; // 获取数组元素地址
// 对应的汇编
mov ebx, arr
lea eax, [ebx+8] ; eax = &arr[2] (假设int是4字节,2*4=8)
3.3 指令错误与参考资源
3.3.1 常见错误
💡 重要提示:记住,没有人能记住所有的x86指令。无论你是在编写x86代码,还是在阅读x86程序,只要遇到不理解的地方,就要自己查找相关信息,因此快速查找的能力很关键。
常见错误类型:
| 错误类型 | 示例 | 说明 |
|---|---|---|
| 两个内存操作数 | mov [eax], [ebx] | x86指令最多只能包含一个内存位置 |
| 直接修改特殊寄存器 | mov eip, 1 | 特殊寄存器不能直接修改 |
| 使用8位寄存器寻址 | mov eax, [al] | 8位寄存器不能用于寻址 |
| 忘记清零edx(除法) | div ebx(edx未清零) | 32位除法前必须清零edx的高32位 |
3.3.2 参考资源
官方文档:
快速参考:
- x86指令参考 ⭐ 推荐收藏
使用建议:
- 保持这个参考页面常开
- 遇到不熟悉的指令立即查找
- 理解指令的完整语法和副作用
📚 总结
关键要点
-
x86架构特点:
- CISC架构,指令丰富
- 保持向后兼容性
- 小端序存储
-
寄存器系统:
- 8个32位通用寄存器
- 特殊寄存器(eip, eflags)只读
- 64位架构扩展了寄存器
-
寻址模式:
- 绝对、间接、基址+偏移、索引、基址-索引
- 灵活的内存访问方式
-
指令限制:
- 最多只能包含一个内存操作数
- 特殊寄存器不能直接修改
-
学习建议:
- 多实践,多查阅参考文档
- 理解指令的副作用(如mul修改edx)
- 注意有符号和无符号运算的区别
📖 参考资源
- 书籍:《x86汇编与逆向工程:软件破解与防护的艺术》
- Intel官方文档:Intel软件开发者手册
- 快速参考:x86指令参考
本文档基于《x86汇编与逆向工程:软件破解与防护的艺术》一书的学习笔记整理而成。