x86/x64汇编
寄存器
用途说明
| 寄存器类型 | x86 (32位) | x64 (64位) | 用途说明 |
|---|---|---|---|
| 通用寄存器 | EAX, EBX, ECX… | RAX, RBX, RCX, R8-R15 | 数据计算/地址操作 |
| 指令指针 | EIP | RIP | 指向下一条指令 |
| 栈指针 | ESP | RSP | 管理函数调用栈 |
| 标志寄存器 | EFLAGS | RFLAGS | 存储运算状态(如进位) |
x64 重大改进:
- 新增 8 个通用寄存器 (R8-R15)
- 所有寄存器扩展至 64 位(如 RAX = 64位, EAX = 低32位)
寄存器位宽关系表
| 64位寄存器 | 低32位 | 低16位 | 高8位 | 低8位 | 用途说明 |
|---|---|---|---|---|---|
| RAX | EAX | AX | AH | AL | 累加器/返回值 |
| RBX | EBX | BX | BH | BL | 基址寄存器 |
| RCX | ECX | CX | CH | CL | 计数器/循环控制 |
| RDX | EDX | DX | DH | DL | 数据寄存器/I/O指针 |
| RSI | ESI | SI | - | SIL* | 源索引(字符串/数组) |
| RDI | EDI | DI | - | DIL* | 目的索引 |
| RBP | EBP | BP | - | BPL* | 栈基址指针 |
| RSP | ESP | SP | - | SPL* | 栈顶指针 |
| R8-R15 | R*D | R*W | - | R*B | x64新增通用寄存器 |
EFLAGS/RFLAGS 标志位对照表
| 位 | 名称 | 缩写 | 用途 | 影响指令示例 |
|---|---|---|---|---|
| 0 | Carry Flag | CF | 无符号运算进位/借位 | ADD, SUB, CMP |
| 1 | Reserved | - | 保留位(恒为1) | - |
| 2 | Parity Flag | PF | 结果低8位1的个数是否为偶数 | AND, OR, XOR |
| 3 | Reserved | - | 保留位 | - |
| 4 | Auxiliary Carry Flag | AF | BCD运算时低4位进位/借位 | AAA, DAA |
| 5 | Reserved | - | 保留位 | - |
| 6 | Zero Flag | ZF | 运算结果是否为0 | CMP, TEST |
| 7 | Sign Flag | SF | 运算结果的最高位(符号位) | MOV, ARITH |
| 8 | Trap Flag | TF | 单步调试(若=1则触发异常) | Debugger 使用 |
| 9 | Interrupt Enable Flag | IF | 是否允许可屏蔽中断 | STI, CLI |
| 10 | Direction Flag | DF | 字符串操作方向(0=递增,1=递减) | MOVSB, REP |
| 11 | Overflow Flag | OF | 有符号运算溢出 | ADD, SUB, IMUL |
| 12-13 | I/O Privilege Level | IOPL | I/O 特权级(0-3,仅内核可修改) | IN, OUT |
| 14 | Nested Task Flag | NT | 任务嵌套(保护模式) | CALL, IRET |
| 15 | Reserved | - | 保留位 | - |
| 16 | Resume Flag | RF | 调试异常后恢复执行 | Debugger 使用 |
| 17 | Virtual-8086 Mode | VM | 是否在虚拟8086模式 | IRET |
| 18 | Alignment Check | AC | 内存对齐检查(若=1则触发异常) | MOV, LOAD |
| 19 | Virtual Interrupt Flag | VIF | 虚拟中断标志(虚拟化扩展) | VMM |
| 20 | Virtual Interrupt Pending | VIP | 虚拟中断挂起(虚拟化扩展) | VMM |
| 21 | ID Flag | ID | 支持 CPUID 指令(若=1则可用) | CPUID |
| 22-31 | Reserved | - | 保留位 | - |
基本数据类型
| 类型名 | 位数 | 字节数 | 对应C类型 | 典型寄存器 | 内存表示范围 |
|---|---|---|---|---|---|
| BYTE | 8 | 1 | uint8_t | AL, BL, CL… | 0x00 ~ 0xFF |
| WORD | 16 | 2 | uint16_t | AX, BX, CX… | 0x0000 ~ 0xFFFF |
| DWORD | 32 | 4 | uint32_t | EAX, EBX, ECX… | 0x00000000 ~ 0xFFFFFFFF |
| QWORD | 64 | 8 | uint64_t | RAX, RBX, RCX… | 64位全范围 |
示例:
| 类型 | NASM语法示例 | 说明 |
|---|---|---|
| BYTE | mov al, byte [rdi] | 读取1字节 |
| WORD | mov ax, word [rsi] | 读取2字节(小端序) |
| DWORD | mov eax, dword [rbx] | 读取4字节 |
| QWORD | mov rax, qword [rcx] | 读取8字节 |
小端序注意:x86/x64 采用小端存储(低位字节在低地址)
例如0x12345678在内存中的排列:78 56 34 12
小端存储(Little-Endian)
x86/x64架构强制使用小端
小端存储(Little-Endian) 是一种内存数据存储格式,其核心规则是:
数据的低位字节存储在低地址,高位字节存储在高地址
(即”低对低,高对高“)
内存布局示例
| 数据类型 | 值 | 内存地址增长方向 → |
|---|---|---|
| BYTE | 0x41 | 41 |
| WORD | 0x1234 | 34 12 |
| DWORD | 0x12345678 | 78 56 34 12 |
| QWORD | 0x1122334455667788 | 88 77 66 55 44 33 22 11 |
指令
基本指令
数据传送指令
| 指令 | 格式 | 功能说明 | 示例 |
|---|---|---|---|
MOV | mov dest, src | 将 src 的值复制到 dest(不修改标志位) | mov rax, rcx |
XCHG | xchg op1, op2 | 交换两个操作数的值 | xchg rax, rbx |
LEA | lea dest, [addr] | 计算 addr 的有效地址(非解引用)并存入 dest | lea rax, [rbx+8] |
关键区别:
MOV传递 值,LEA传递 地址1
2mov eax, [ecx] ; 将 ecx 指向的内存值赋给 eax
lea eax, [ecx] ; 将 ecx 的地址值赋给 eax(等同于 mov eax, ecx)
算术运算指令
| 指令 | 格式 | 功能说明 | 等效高级语言代码 |
|---|---|---|---|
ADD | add dest, src | dest += src(设置标志位) | rax += rcx |
SUB | sub dest, src | dest -= src(设置标志位) | rax -= rcx |
INC | inc op | op += 1(不影响 CF 标志位) | rax++ |
DEC | dec op | op -= 1(不影响 CF 标志位) | rax-- |
注意:
INC/DEC不影响 CF(进位标志),但影响 ZF/SF/OFADD/SUB影响所有算术标志位(CF/ZF/SF/OF)
逻辑运算指令
| 指令 | 格式 | 功能说明 | 二进制规则 |
|---|---|---|---|
AND | and dest, src | 按位与(清零特定位) | 全1为1,否则为0 |
OR | or dest, src | 按位或(置位特定位) | 有1为1,全0为0 |
XOR | xor dest, src | 按位异或(翻转特定位) | 相同为0,不同为1 |
NOT | not op | 按位取反 | 0→1,1→0 |
典型用途:
- AND:屏蔽高位(
and al, 0x0F保留低4位) - OR:设置标志位(
or eax, 1强制最低位为1) - XOR:清零寄存器(
xor rax, rax比mov rax, 0更快)
比较指令
| 指令 | 格式 | 功能说明 | 等效操作 |
|---|---|---|---|
CMP | cmp op1, op2 | 计算 op1 - op2 并设置标志位 | sub op1, op2(不保存结果) |
标志位影响:
- ZF=1:结果为零(
op1 == op2) - CF=1:无符号数
op1 < op2 - SF≠OF:有符号数
op1 < op2
示例:
1 | cmp rax, 100 ; 比较 rax 和 100 |
内存操作与解引用
[]表示解引用(访问内存地址):1
2mov rax, [rbx] ; 读取 rbx 指向的内存到 rax
mov [rcx], rdx ; 将 rdx 的值写入 rcx 指向的内存- 地址计算:
1
2lea rax, [rbx + rcx*4 + 8] ; 计算地址:rax = rbx + rcx*4 + 8
mov rdx, [rax] ; 读取该地址的值
寻址方式
寻址方式对比表
| 寻址方式 | 示例 | 适用场景 |
|---|---|---|
| 立即寻址 | mov eax, 42 | 加载常量 |
| 寄存器寻址 | add rax, rbx | 高速数据运算 |
| 直接寻址 | mov ax, [0x1000] | 实模式/绝对地址访问 |
| 寄存器间接 | mov eax, [rdi] | 指针解引用 |
| 基址+偏移 | mov ecx, [rbp-8] | 栈帧局部变量 |
| 变址寻址 | mov al, [array + rsi] | 数组遍历 |
| 比例变址 | mov eax, [rbx+rsi*4] | 结构体数组 |
| RIP相对 | mov rax, [rip+0x200] | 位置无关代码 |
立即寻址(Immediate)
- 特点:操作数直接编码在指令中
- 用途:传递常量值
- 限制:源操作数只能是立即数
1 | mov rax, 0x1234 ; RAX = 0x1234 |
** 寄存器寻址(Register)**
- 特点:操作数位于寄存器内
- 优势:速度最快(无需访问内存)
- 注意:x86-64 扩展了寄存器数量(R8-R15)
1 | xor rax, rax ; 清零RAX(寄存器操作) |
直接寻址(Direct)
- 特点:使用固定内存地址
- 用途:访问全局变量或绝对地址
- 风险:现代系统禁用绝对地址(ASLR保护)
1 | mov al, [0x8048000] ; 读取绝对地址(实模式/内核态) |
寄存器间接寻址(Register Indirect)
- 特点:用寄存器存储内存地址
- 关键:
[]表示解引用 - 变种:
- 基址寻址:
mov eax, [rbx] - 变址寻址:
mov eax, [rsi]
- 基址寻址:
1 | mov rsi, array_ptr |
基址+偏移寻址(Base + Displacement)
- 特点:寄存器基址 + 常数偏移
- 用途:结构体/数组元素访问
1 | struct { |
变址寻址(Indexed)
- 特点:用变址寄存器(ESI/EDI/RSI/RDI)访问连续内存
- 语法:
mov al, [array + rsi] - 典型场景:数组遍历
1 | mov rsi, 0 ; 索引初始化 |
比例变址寻址(Scaled Index)
- 特点:变址寄存器 × 比例因子(1/2/4/8)
- 优势:高效访问数组(自动计算元素偏移)
- 比例因子:
- 1:BYTE
- 2:WORD
- 4:DWORD
- 8:QWORD
1 | ; 访问int数组(每个元素4字节) |
完整内存操作数格式(x86-32)[基址寄存器 + 变址寄存器 × 比例因子 + 偏移量]
示例:
1 | mov eax, [ebx + esi*4 + 16] ; EAX = *(EBX + ESI*4 + 16) |
x64 专属寻址模式
- RIP相对寻址(RIP-relative)
- 特点:基于当前指令指针(RIP)的偏移
- 语法:
mov rax, [rip + 0x100] - 优势:支持位置无关代码(PIC)
1 | section .data |
- 新增R8-R15寄存器参与寻址
1
mov rax, [r15 + r12*8 + 32] ; 64位模式特有
示例:
结构体访问
1 | ; C结构体: |
二维数组访问
1 | ; int arr[3][4] → 行优先存储 |
跳转指令
无条件跳转
| 指令 | 示例 | 功能说明 | 等效高级代码 |
|---|---|---|---|
JMP | jmp label | 直接跳转到目标地址 | goto label; |
JMP | jmp [rax] | 间接跳转(地址存储在内存) | goto *ptr; |
条件跳转(基于标志位)
| 指令 | 触发条件 | 检查的标志位 | 典型用途 |
|---|---|---|---|
JE/JZ | 相等/为零 | ZF=1 | if (a == b) |
JNE/JNZ | 不相等/非零 | ZF=0 | if (a != b) |
JG/JNLE | 有符号大于 | ZF=0 AND SF=OF | if (a > b) |
JGE/JNL | 有符号大于等于 | SF=OF | if (a >= b) |
JL/JNGE | 有符号小于 | SF≠OF | if (a < b) |
JLE/JNG | 有符号小于等于 | ZF=1 OR SF≠OF | if (a <= b) |
JA/JNBE | 无符号高于 | CF=0 AND ZF=0 | if (a > b)(无符号) |
JB/JNAE | 无符号低于 | CF=1 | if (a < b)(无符号) |
JC | 进位标志置位 | CF=1 | 溢出检查 |
JO | 溢出标志置位 | OF=1 | 有符号溢出处理 |
记忆技巧:
- J + E(Equal)/ G(Greater)/ L(Less)
- A(Above)/ B(Below)用于无符号比较
循环控制指令
| 指令 | 示例 | 功能说明 | 等效高级代码 |
|---|---|---|---|
LOOP | loop label | ECX-=1,若ECX≠0则跳转 | while (--ecx) |
LOOPE | loope label | ECX-=1,若ECX≠0且ZF=1则跳转 | 查找匹配元素 |
LOOPNE | loopne label | ECX-=1,若ECX≠0且ZF=0则跳转 | 查找非匹配元素 |
注意:x64 下默认使用 RCX,但 LOOP 仍用 ECX。
函数调用与返回
| 指令 | 示例 | 功能说明 | 栈变化 |
|---|---|---|---|
CALL | call func | 压入返回地址并跳转 | RSP -= 8 |
RET | ret | 弹出返回地址并跳转 | RSP += 8 |
RET | ret 8 | 返回并清理栈参数(+8字节) | RSP += 8 + 8 |
跳转范围对比
| 类型 | 指令前缀 | 跳转范围 | 示例 |
|---|---|---|---|
| 短跳转 | short | -128 ~ +127字节 | jmp short label |
| 近跳转 | - | ±2GB(x64) | jmp label |
| 远跳转 | far | 跨段跳转 | jmp far 0x10:0x2000 |
特殊跳转指令
| 指令 | 功能说明 | 典型应用场景 |
|---|---|---|
JCXZ | 若CX=0则跳转 | 循环前检查 |
JECXZ | 若ECX=0则跳转 | x86 32位模式 |
JRCXZ | 若RCX=0则跳转 | x64 模式 |
堆栈
基本说明
堆栈的特点
- 后进先出(LIFO):最后压入的数据最先弹出。
- 向下增长(低地址 → 高地址):
push减小RSP,pop增大RSP。 - 栈顶指针:
RSP(x64)或ESP(x32)始终指向栈顶。 - 栈基址指针:
RBP(x64)或EBP(x32)用于访问函数参数和局部变量。
堆栈操作指令
| 指令 | 功能 | 等效操作 | 影响 RSP |
|---|---|---|---|
push reg | 压入寄存器 | sub rsp, 8 + mov [rsp], reg | RSP -= 8 |
pop reg | 弹出到寄存器 | mov reg, [rsp] + add rsp, 8 | RSP += 8 |
call func | 调用函数 | push rip + jmp func | RSP -= 8 |
ret | 函数返回 | pop rip | RSP += 8 |
enter size, 0 | 进入函数(建立栈帧) | push rbp + mov rbp, rsp + sub rsp, size | RSP -= size+8 |
leave | 离开函数(恢复栈帧) | mov rsp, rbp + pop rbp | RSP = RBP + 8 |
仅限x86:
PUSHAD 把8个通用寄存器依次入栈
POPAD 出栈用8个寄存器接收
堆栈平衡(函数调用约定)
堆栈平衡 指在函数调用前后保持 RSP 一致,避免栈溢出或数据错乱
cdecl(C语言默认,x86)
- 调用者清理栈(Caller cleans the stack)
- 参数从右向左压栈
- 返回值在
EAX
1 | ; 调用者代码 |
stdcall(Win32 API)
- 被调用者清理栈(Callee cleans the stack)
- 参数从右向左压栈
- 返回值在
EAX
1 | ; 调用者代码 |
fastcall(x64 Windows/Linux)
- 前 4 个参数用寄存器传递(
RCX, RDX, R8, R9(Windows) /RDI, RSI, RDX, RCX(Linux)) - 剩余参数压栈
- 调用者清理栈
1 | ; x64 Windows 示例 |
栈帧(Stack Frame)
栈帧是函数调用时在堆栈上分配的一块内存,用于存储:
- 返回地址(
call时压入) - **旧的
RBP**(push rbp) - 局部变量
- 临时数据
栈帧布局(x64)
1 | 高地址 |
栈帧操作示例
1 | my_func: |
if-else分析
1 | int main() { |
反汇编代码:
1 | main: |
swicth
核心思想是:避免进行一连串的顺序比较(像 if-else-if 那样),而是通过计算直接跳转到目标地址
最常见的两种策略是跳转表和二分查找/决策树
策略一:跳转表 (Jump Table) - 最经典的方式
当 case 标签的值连续且紧凑时(例如 0, 1, 2, 3 或 10, 11, 12, 13),编译器会优先使用跳转表。这是一种空间换时间的策略,效率极高(O(1)时间复杂度)。
C 代码:
1 | int switch_example(int x) { |
对应的 x86 汇编 / 反汇编代码 (GCC -O2):
1 | switch_example: |
(注意:在实际的编译输出中,由于优化,case 1 的标签可能不会单独出现,返回值 20 可能会被内联处理。上面的代码为了清晰展示了逻辑结构。)
关键指令解释:
- 边界检查:
cmp edi, 2和ja .L8。先检查输入值是否在[0, 2]的范围内,如果不在,直接跳转到default块。 - 跳转表 (
.L4): 这是一个在只读数据段(通常是.rodata节)的数组。数组的每个元素(.quad,8字节)存储的是对应case代码块的内存地址。.L4[0]存储着.L7的地址(对应case 0).L4[1]存储着.L5的地址(对应case 1).L4[2]存储着.L6的地址(对应case 2)
- 计算并跳转:
jmp [QWORD PTR .L4[0+rax*8]]rax里是x的值。rax * 8是因为每个地址是 8 字节(64位系统)。- 这条指令计算出的内存地址是
.L4 + x * 8,然后从这个地址里取出真正的目标地址,最后跳转过去。
这个过程就像查字典一样,直接根据索引(x的值)找到目的地,而不是一个一个case地去比较。
策略二:二分查找/决策树 (Binary Search / Decision Tree)
当 case 值非常稀疏(例如 1, 50, 1000, 5000)时,构建一个巨大的跳转表(包含5000个元素)会浪费大量内存,其中大部分是无效的 default 项。此时,编译器会将其优化成类似 if-else-if 链的结构,但会采用二分查找的策略来提升效率(O(log n)时间复杂度)。
C 代码:
1 | int sparse_switch(int x) { |
对应的 x86 汇编 / 反汇编代码 (GCC -O2):
1 | sparse_switch: |
流程分析:
- 代码首先将输入值
x与中间值200比较。 - 根据比较结果,将搜索范围缩小到一半(大于200或小于等于200)。
- 在缩小后的范围内,再进行类似的比较,直到找到匹配项或确认无匹配。
这就像在字典里查单词,你不会从第一页开始一页一页翻,而是先翻到中间,根据结果决定往前还是往后翻,从而快速定位。
