寄存器

用途说明

寄存器类型x86 (32位)x64 (64位)用途说明
通用寄存器EAX, EBX, ECX…RAX, RBX, RCX, R8-R15数据计算/地址操作
指令指针EIPRIP指向下一条指令
栈指针ESPRSP管理函数调用栈
标志寄存器EFLAGSRFLAGS存储运算状态(如进位)

x64 重大改进

  • 新增 8 个通用寄存器 (R8-R15)
  • 所有寄存器扩展至 64 位(如 RAX = 64位, EAX = 低32位)

寄存器位宽关系表

64位寄存器低32位低16位高8位低8位用途说明
RAXEAXAXAHAL累加器/返回值
RBXEBXBXBHBL基址寄存器
RCXECXCXCHCL计数器/循环控制
RDXEDXDXDHDL数据寄存器/I/O指针
RSIESISI-SIL*源索引(字符串/数组)
RDIEDIDI-DIL*目的索引
RBPEBPBP-BPL*栈基址指针
RSPESPSP-SPL*栈顶指针
R8-R15R*DR*W-R*Bx64新增通用寄存器

EFLAGS/RFLAGS 标志位对照表

名称缩写用途影响指令示例
0Carry FlagCF无符号运算进位/借位ADD, SUB, CMP
1Reserved-保留位(恒为1)-
2Parity FlagPF结果低8位1的个数是否为偶数AND, OR, XOR
3Reserved-保留位-
4Auxiliary Carry FlagAFBCD运算时低4位进位/借位AAA, DAA
5Reserved-保留位-
6Zero FlagZF运算结果是否为0CMP, TEST
7Sign FlagSF运算结果的最高位(符号位)MOV, ARITH
8Trap FlagTF单步调试(若=1则触发异常)Debugger 使用
9Interrupt Enable FlagIF是否允许可屏蔽中断STI, CLI
10Direction FlagDF字符串操作方向(0=递增,1=递减)MOVSB, REP
11Overflow FlagOF有符号运算溢出ADD, SUB, IMUL
12-13I/O Privilege LevelIOPLI/O 特权级(0-3,仅内核可修改)IN, OUT
14Nested Task FlagNT任务嵌套(保护模式)CALL, IRET
15Reserved-保留位-
16Resume FlagRF调试异常后恢复执行Debugger 使用
17Virtual-8086 ModeVM是否在虚拟8086模式IRET
18Alignment CheckAC内存对齐检查(若=1则触发异常)MOV, LOAD
19Virtual Interrupt FlagVIF虚拟中断标志(虚拟化扩展)VMM
20Virtual Interrupt PendingVIP虚拟中断挂起(虚拟化扩展)VMM
21ID FlagID支持 CPUID 指令(若=1则可用)CPUID
22-31Reserved-保留位-

基本数据类型

类型名位数字节数对应C类型典型寄存器内存表示范围
BYTE81uint8_tAL, BL, CL…0x00 ~ 0xFF
WORD162uint16_tAX, BX, CX…0x0000 ~ 0xFFFF
DWORD324uint32_tEAX, EBX, ECX…0x00000000 ~ 0xFFFFFFFF
QWORD648uint64_tRAX, RBX, RCX…64位全范围

示例:

类型NASM语法示例说明
BYTEmov al, byte [rdi]读取1字节
WORDmov ax, word [rsi]读取2字节(小端序)
DWORDmov eax, dword [rbx]读取4字节
QWORDmov rax, qword [rcx]读取8字节

小端序注意:x86/x64 采用小端存储(低位字节在低地址)
例如 0x12345678 在内存中的排列:78 56 34 12

小端存储(Little-Endian)

x86/x64架构强制使用小端

小端存储(Little-Endian) 是一种内存数据存储格式,其核心规则是:

数据的低位字节存储在低地址,高位字节存储在高地址
(即”低对低,高对高“)

内存布局示例

数据类型内存地址增长方向 →
BYTE0x4141
WORD0x123434 12
DWORD0x1234567878 56 34 12
QWORD0x112233445566778888 77 66 55 44 33 22 11

指令

基本指令

数据传送指令

指令格式功能说明示例
MOVmov dest, srcsrc 的值复制到 dest(不修改标志位)mov rax, rcx
XCHGxchg op1, op2交换两个操作数的值xchg rax, rbx
LEAlea dest, [addr]计算 addr 的有效地址(非解引用)并存入 destlea rax, [rbx+8]

关键区别

  • MOV 传递 LEA 传递 地址

    1
    2
    mov eax, [ecx]   ; 将 ecx 指向的内存值赋给 eax
    lea eax, [ecx] ; 将 ecx 的地址值赋给 eax(等同于 mov eax, ecx)

算术运算指令

指令格式功能说明等效高级语言代码
ADDadd dest, srcdest += src(设置标志位)rax += rcx
SUBsub dest, srcdest -= src(设置标志位)rax -= rcx
INCinc opop += 1(不影响 CF 标志位)rax++
DECdec opop -= 1(不影响 CF 标志位)rax--

注意

  • INC/DEC 不影响 CF(进位标志),但影响 ZF/SF/OF
  • ADD/SUB 影响所有算术标志位(CF/ZF/SF/OF)

逻辑运算指令

指令格式功能说明二进制规则
ANDand dest, src按位与(清零特定位)全1为1,否则为0
ORor dest, src按位或(置位特定位)有1为1,全0为0
XORxor dest, src按位异或(翻转特定位)相同为0,不同为1
NOTnot op按位取反0→1,1→0

典型用途

  • AND:屏蔽高位(and al, 0x0F 保留低4位)
  • OR:设置标志位(or eax, 1 强制最低位为1)
  • XOR:清零寄存器(xor rax, raxmov rax, 0 更快)

比较指令

指令格式功能说明等效操作
CMPcmp op1, op2计算 op1 - op2 并设置标志位sub op1, op2(不保存结果)

标志位影响

  • ZF=1:结果为零(op1 == op2
  • CF=1:无符号数 op1 < op2
  • SF≠OF:有符号数 op1 < op2

示例

1
2
cmp rax, 100     ; 比较 rax 和 100
jg above_100 ; 若 rax > 100(有符号)则跳转

内存操作与解引用

  • [] 表示解引用(访问内存地址):
    1
    2
    mov rax, [rbx]    ; 读取 rbx 指向的内存到 rax
    mov [rcx], rdx ; 将 rdx 的值写入 rcx 指向的内存
  • 地址计算
    1
    2
    lea 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
2
mov rax, 0x1234    ; RAX = 0x1234
add rbx, 10 ; RBX += 10(立即数10直接编码)

** 寄存器寻址(Register)**

  • 特点:操作数位于寄存器内
  • 优势:速度最快(无需访问内存)
  • 注意:x86-64 扩展了寄存器数量(R8-R15)
1
2
xor rax, rax       ; 清零RAX(寄存器操作)
add rcx, rdx ; RCX += RDX

直接寻址(Direct)

  • 特点:使用固定内存地址
  • 用途:访问全局变量或绝对地址
  • 风险:现代系统禁用绝对地址(ASLR保护)
1
mov al, [0x8048000]  ; 读取绝对地址(实模式/内核态)

寄存器间接寻址(Register Indirect)

  • 特点:用寄存器存储内存地址
  • 关键[] 表示解引用
  • 变种
    • 基址寻址mov eax, [rbx]
    • 变址寻址mov eax, [rsi]
1
2
mov rsi, array_ptr
mov eax, [rsi] ; EAX = *array_ptr

基址+偏移寻址(Base + Displacement)

  • 特点:寄存器基址 + 常数偏移
  • 用途:结构体/数组元素访问
1
2
3
4
5
struct {
int a; // +0
int b; // +4
};
mov ecx, [rdi+4] ; 访问结构体的b成员

变址寻址(Indexed)

  • 特点:用变址寄存器(ESI/EDI/RSI/RDI)访问连续内存
  • 语法mov al, [array + rsi]
  • 典型场景:数组遍历
1
2
3
4
5
6
mov rsi, 0          ; 索引初始化
loop_start:
mov al, [array + rsi]
inc rsi
cmp rsi, 10
jl loop_start

比例变址寻址(Scaled Index)

  • 特点:变址寄存器 × 比例因子(1/2/4/8)
  • 优势:高效访问数组(自动计算元素偏移)
  • 比例因子
    • 1:BYTE
    • 2:WORD
    • 4:DWORD
    • 8:QWORD
1
2
3
; 访问int数组(每个元素4字节)
mov esi, 2 ; 索引=2
mov eax, [array + rsi*4] ; 相当于array[2]

完整内存操作数格式(x86-32)
[基址寄存器 + 变址寄存器 × 比例因子 + 偏移量]
示例

1
mov eax, [ebx + esi*4 + 16] ; EAX = *(EBX + ESI*4 + 16)

x64 专属寻址模式

  1. RIP相对寻址(RIP-relative)
    • 特点:基于当前指令指针(RIP)的偏移
    • 语法mov rax, [rip + 0x100]
    • 优势:支持位置无关代码(PIC)
1
2
3
4
section .data
msg db "Hello",0
section .text
lea rsi, [rel msg] ; RIP相对寻址(推荐写法)
  1. 新增R8-R15寄存器参与寻址
    1
    mov rax, [r15 + r12*8 + 32] ; 64位模式特有

示例:
结构体访问

1
2
3
4
5
; C结构体:
; struct { char a; int b; short c; } s;
mov al, [rdi] ; s.a (offset=0)
mov ebx, [rdi+4] ; s.b (offset=4)
mov cx, [rdi+8] ; s.c (offset=8)

二维数组访问

1
2
3
4
5
; int arr[3][4] → 行优先存储
; arr[i][j] = [base + i*16 + j*4]
mov rsi, 1 ; i=1
mov rdi, 2 ; j=2
mov eax, [array + rsi*16 + rdi*4] ; eax = arr[1][2]

跳转指令

无条件跳转

指令示例功能说明等效高级代码
JMPjmp label直接跳转到目标地址goto label;
JMPjmp [rax]间接跳转(地址存储在内存)goto *ptr;

条件跳转(基于标志位)

指令触发条件检查的标志位典型用途
JE/JZ相等/为零ZF=1if (a == b)
JNE/JNZ不相等/非零ZF=0if (a != b)
JG/JNLE有符号大于ZF=0 AND SF=OFif (a > b)
JGE/JNL有符号大于等于SF=OFif (a >= b)
JL/JNGE有符号小于SF≠OFif (a < b)
JLE/JNG有符号小于等于ZF=1 OR SF≠OFif (a <= b)
JA/JNBE无符号高于CF=0 AND ZF=0if (a > b)(无符号)
JB/JNAE无符号低于CF=1if (a < b)(无符号)
JC进位标志置位CF=1溢出检查
JO溢出标志置位OF=1有符号溢出处理

记忆技巧

  • J + E(Equal)/ G(Greater)/ L(Less)
  • A(Above)/ B(Below)用于无符号比较

循环控制指令

指令示例功能说明等效高级代码
LOOPloop labelECX-=1,若ECX≠0则跳转while (--ecx)
LOOPEloope labelECX-=1,若ECX≠0且ZF=1则跳转查找匹配元素
LOOPNEloopne labelECX-=1,若ECX≠0且ZF=0则跳转查找非匹配元素

注意:x64 下默认使用 RCX,但 LOOP 仍用 ECX


函数调用与返回

指令示例功能说明栈变化
CALLcall func压入返回地址并跳转RSP -= 8
RETret弹出返回地址并跳转RSP += 8
RETret 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 减小 RSPpop 增大 RSP
  • 栈顶指针RSP(x64)或 ESP(x32)始终指向栈顶。
  • 栈基址指针RBP(x64)或 EBP(x32)用于访问函数参数和局部变量。

堆栈操作指令

指令功能等效操作影响 RSP
push reg压入寄存器sub rsp, 8 + mov [rsp], regRSP -= 8
pop reg弹出到寄存器mov reg, [rsp] + add rsp, 8RSP += 8
call func调用函数push rip + jmp funcRSP -= 8
ret函数返回pop ripRSP += 8
enter size, 0进入函数(建立栈帧)push rbp + mov rbp, rsp + sub rsp, sizeRSP -= size+8
leave离开函数(恢复栈帧)mov rsp, rbp + pop rbpRSP = RBP + 8

仅限x86:

PUSHAD 把8个通用寄存器依次入栈

POPAD 出栈用8个寄存器接收

堆栈平衡(函数调用约定)

堆栈平衡 指在函数调用前后保持 RSP 一致,避免栈溢出或数据错乱

cdecl(C语言默认,x86)

  • 调用者清理栈(Caller cleans the stack)
  • 参数从右向左压栈
  • 返回值在 EAX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
; 调用者代码
push arg3 ; 参数3
push arg2 ; 参数2
push arg1 ; 参数1
call func
add esp, 12 ; 调用者清理栈(3个参数 × 4字节)

; 被调用者代码
func:
push ebp
mov ebp, esp
; ... 函数体 ...
mov eax, return_value
leave
ret

stdcall(Win32 API)

  • 被调用者清理栈(Callee cleans the stack)
  • 参数从右向左压栈
  • 返回值在 EAX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
; 调用者代码
push arg3
push arg2
push arg1
call func ; 被调用者自己清理栈

; 被调用者代码
func:
push ebp
mov ebp, esp
; ... 函数体 ...
mov eax, return_value
leave
ret 12 ; 清理 12 字节(3个参数 × 4字节)

fastcall(x64 Windows/Linux)

  • 前 4 个参数用寄存器传递RCX, RDX, R8, R9(Windows) / RDI, RSI, RDX, RCX(Linux))
  • 剩余参数压栈
  • 调用者清理栈
1
2
3
4
5
6
7
8
; x64 Windows 示例
mov rcx, arg1 ; 第1个参数
mov rdx, arg2 ; 第2个参数
mov r8, arg3 ; 第3个参数
mov r9, arg4 ; 第4个参数
sub rsp, 32 ; 预留影子空间(Shadow Space)
call func
add rsp, 32 ; 调用者清理栈

栈帧(Stack Frame)

栈帧是函数调用时在堆栈上分配的一块内存,用于存储:

  • 返回地址call 时压入)
  • **旧的 RBP**(push rbp
  • 局部变量
  • 临时数据

栈帧布局(x64)

1
2
3
4
5
6
7
8
9
10
11
12
13
高地址
+-------------------+
| 返回地址 | (RBP+8)
+-------------------+
| 旧的 RBP | (RBP)
+-------------------+
| 局部变量1 | (RBP-8)
+-------------------+
| 局部变量2 | (RBP-16)
+-------------------+
| ... |
+-------------------+
低地址

栈帧操作示例

1
2
3
4
5
6
7
8
9
10
11
12
13
my_func:
push rbp ; 保存旧的 RBP
mov rbp, rsp ; 设置新的栈帧基址
sub rsp, 16 ; 分配 16 字节局部变量空间

mov [rbp-8], rcx ; 存储局部变量1
mov [rbp-16], rdx ; 存储局部变量2

; ... 函数逻辑 ...

mov rsp, rbp ; 恢复 RSP
pop rbp ; 恢复旧的 RBP
ret ; 返回

if-else分析

1
2
3
4
5
6
7
8
9
int main() {
int a = 10;
if (a > 5) {
a = 20;
} else {
a = 0;
}
return a;
}

反汇编代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
main:
push rbp
mov rbp, rsp
; int a = 10;
mov DWORD PTR [rbp-4], 10

; if (a > 5)
mov eax, DWORD PTR [rbp-4] ; 将变量 a 的值加载到寄存器 eax
cmp eax, 5 ; 比较 (eax - 5), 并设置标志位
jle .L2 ; 条件跳转: 如果 `a <= 5` (Less than or Equal), 则跳转到 .L2 标签(else 块)

; if 块: a = 20;
mov DWORD PTR [rbp-4], 20
jmp .L3 ; 无条件跳转: 跳过 else 块,直接到 .L3

.L2: ; else 块标签
; else 块: a = 0;
mov DWORD PTR [rbp-4], 0

.L3: ; if-else 之后的代码标签
; return a;
mov eax, DWORD PTR [rbp-4] ; 将返回值放入 eax
pop rbp
ret

swicth

核心思想是:避免进行一连串的顺序比较(像 if-else-if 那样),而是通过计算直接跳转到目标地址

最常见的两种策略是跳转表二分查找/决策树


策略一:跳转表 (Jump Table) - 最经典的方式

case 标签的值连续且紧凑时(例如 0, 1, 2, 3 或 10, 11, 12, 13),编译器会优先使用跳转表。这是一种空间换时间的策略,效率极高(O(1)时间复杂度)。

C 代码:

1
2
3
4
5
6
7
8
9
10
11
12
int switch_example(int x) {
switch (x) {
case 0:
return 10;
case 1:
return 20;
case 2:
return 30;
default:
return 0;
}
}

对应的 x86 汇编 / 反汇编代码 (GCC -O2):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
switch_example:
; x 在 edi 寄存器中
cmp edi, 2 ; 首先比较 x 和最大的 case 值 (2)
ja .L8 ; 如果 x > 2 (无符号比较), 跳转到 default (.L8)
mov eax, edi ; 将 x 的值复制到 eax
; 下一行是精髓:通过跳转表直接跳转
jmp [QWORD PTR .L4[0+rax*8]] ; 计算地址 .L4 + rax*8,从中取出目标地址并跳转

.L4:
; 这就是跳转表!不是一个指令,而是一个数据段,存储着代码块的地址
.quad .L7 ; case 0 的地址
.quad .L8 ; case 1 的地址 (注意:这里原代码是 case 1: return 20; 但标签是.L5)
.quad .L6 ; case 2 的地址

.L7: ; case 0
mov eax, 10
ret
.L5: ; case 1 (被优化了,见下文分析)
mov eax, 20
ret
.L6: ; case 2
mov eax, 30
ret
.L8: ; default
mov eax, 0
ret

(注意:在实际的编译输出中,由于优化,case 1 的标签可能不会单独出现,返回值 20 可能会被内联处理。上面的代码为了清晰展示了逻辑结构。)

关键指令解释:

  1. 边界检查: cmp edi, 2ja .L8。先检查输入值是否在 [0, 2] 的范围内,如果不在,直接跳转到 default 块。
  2. 跳转表 (.L4): 这是一个在只读数据段(通常是 .rodata 节)的数组。数组的每个元素(.quad,8字节)存储的是对应 case 代码块的内存地址
    • .L4[0] 存储着 .L7 的地址(对应 case 0
    • .L4[1] 存储着 .L5 的地址(对应 case 1
    • .L4[2] 存储着 .L6 的地址(对应 case 2
  3. 计算并跳转: 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
2
3
4
5
6
7
8
9
int sparse_switch(int x) {
switch (x) {
case 100: return 1;
case 200: return 2;
case 300: return 3;
case 400: return 4;
default: return 0;
}
}

对应的 x86 汇编 / 反汇编代码 (GCC -O2):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
sparse_switch:
; x 在 edi 中
cmp edi, 200 ; 先和中间值 200 比较
je .L13 ; 如果等于 200,跳转
jg .L14 ; 如果大于 200,跳转到 .L14 处理更大的数
cmp edi, 100 ; 如果小于等于 200,再和 100 比较
je .L15 ; 如果等于 100,跳转
jmp .L11 ; 否则,跳转到 default

.L14: ; 处理大于 200 的情况
cmp edi, 300
je .L16
cmp edi, 400 ; 继续比较下一个值
je .L17
jmp .L11 ; 都不匹配,跳转到 default

.L15: ; case 100
mov eax, 1
ret
.L13: ; case 200
mov eax, 2
ret
.L16: ; case 300
mov eax, 3
ret
.L17: ; case 400
mov eax, 4
ret
.L11: ; default
mov eax, 0
ret

流程分析:

  1. 代码首先将输入值 x 与中间值 200 比较。
  2. 根据比较结果,将搜索范围缩小到一半(大于200或小于等于200)。
  3. 在缩小后的范围内,再进行类似的比较,直到找到匹配项或确认无匹配。
    这就像在字典里查单词,你不会从第一页开始一页一页翻,而是先翻到中间,根据结果决定往前还是往后翻,从而快速定位。