指令操作码

本节介绍指令系统中不同操作码的具体细节,作为指令格式一节的参考。

不同计算机架构的指令操作码不相同,但是其中涉及到的功能却大同小异。本节以 x86 平台的指令为例,说明一下指令操作码的主要分类和功能。

数据传输指令

数据传送

数据传送指令在 x86 中就是 MOV,将第二个操作数(寄存器的内容、内存中的内容或常数值)复制到第一个操作数(寄存器或内存),可以实现 寄存器、内存之间的数据传送。

; 数据传送指令语法
mov <reg>, <reg>   ; 复制寄存器值
mov <reg>, <mem>   ; 从内存加载数据到寄存器
mov <mem>, <reg>   ; 把寄存器值存入内存
mov <reg>, <con>   ; 立即数赋值给寄存器
mov <mem>, <con>   ; 立即数赋值给内存
; 数据传送指令实例
mov eax, ebx       ; 把 ebx 复制到 eax
mov eax, [var]     ; 把变量 var 的值存入 eax
mov [var], eax     ; 把 eax 的值存入变量 var
mov ecx, 100       ; 将 100 赋值给 ecx
mov byte ptr [var], 5  ; 只修改 var 指向的 1 字节

栈操作

堆栈 指的是程序的运行栈,从高地址向低地址增长。PUSH 指令将数据压入栈顶,POP 指令从栈顶取出数据,并存入寄存器或者内存单元。

push <reg>    ; 将寄存器值压入堆栈
push <mem>    ; 将内存值压入堆栈
push <con>    ; 将立即数压入堆栈

pop <reg>     ; 从堆栈弹出值存入寄存器
pop <mem>     ; 从堆栈弹出值存入内存
push eax      ; 将 eax 压入栈
push 10       ; 将 10 压入栈
pop ebx       ; 弹出栈顶的值存入 ebx
; PUSH 指令等同于以下指令序列
sub esp, 4      ; esp 向低地址移动
mov [esp], eax  ; 把 eax 的值写入栈顶
; POP 指令等同于以下指令序列
mov ebx, [esp]  ; 从栈顶读取值
add esp, 4      ; esp 向高地址移动
  • 当执行 PUSH 指令时,需要将 ESP 的值 减去数据大小,然后将数据写入新地址处。
  • 当执行 POP 指令时,需要将栈顶的数据(即 [ESP] 处的值)读取到目标寄存器或内存,然后将 ESP 的值 加上数据大小,即弹出数据。

下图给出了一个入栈出栈指令的实例,通过 PUSHPOP 指令实现了寄存器 EAXEBX 内容交换:

0000 FFFDH
0000 FFFCH
0000 FFFBH
0000 FFFAH
ESP
0000 FFF9H
0000 FFF8H
0000 FFF7H
0000 FFF6H
0000 FFFFH
0000 FFFEH
0000 FFF5H
0000 FFF4H
0000 FFF3H
0000 FFF2H
0000 FFF1H
0000 FFF40
0000 FFFDH
0000 FFFCH
0000 FFFBH
0000 FFFAH
ESP
0000 FFF9H
0000 FFF8H
0000 FFF7H
0000 FFF6H
0000 FFFFH
0000 FFFEH
0000 FFF5H
0000 FFF4H
0000 FFF3H
0000 FFF2H
0000 FFF1H
0000 FFF40
Current
EAX
Value
0000 FFFDH
0000 FFFCH
0000 FFFBH
0000 FFFAH
ESP
0000 FFF9H
0000 FFF8H
0000 FFF7H
0000 FFF6H
0000 FFFFH
0000 FFFEH
0000 FFF5H
0000 FFF4H
0000 FFF3H
0000 FFF2H
0000 FFF1H
0000 FFF40
Current
EAX
Value
Current
EBX
Value
0000 FFFDH
0000 FFFCH
0000 FFFBH
0000 FFFAH
ESP
0000 FFF9H
0000 FFF8H
0000 FFF7H
0000 FFF6H
0000 FFFFH
0000 FFFEH
0000 FFF5H
0000 FFF4H
0000 FFF3H
0000 FFF2H
0000 FFF1H
0000 FFF40
Current
EAX
Value
Current
EBX
Value
EAX
0000 FFFDH
0000 FFFCH
0000 FFFBH
0000 FFFAH
ESP
0000 FFF9H
0000 FFF8H
0000 FFF7H
0000 FFF6H
0000 FFFFH
0000 FFFEH
0000 FFF5H
0000 FFF4H
0000 FFF3H
0000 FFF2H
0000 FFF1H
0000 FFF40
Current
EAX
Value
Current
EBX
Value
EBX
1. 初始状态
2, PUSH EAX
3, PUSH EBX
栈增长方向
4. POP EAX
5. POP EBX

算术和逻辑运算指令

加减

可以通过 ADDSUB 两个指令实现加减操作:

  • ADD 指令执行加法,将结果存入第一个操作数。
  • SUB 指令执行减法:第一个操作数减去第二个操作数。
add <reg/mem>, <reg/mem/con>   ; 加法
sub <reg/mem>, <reg/mem/con>   ; 减法
add eax, ebx    ; eax ← eax + ebx
sub eax, 10     ; eax ← eax - 10
add [var], cl   ; var ← var + cl

乘除

在 x86 架构中,乘除法使用专门的指令 MULIMULDIVIDIV,它们大多依赖默认寄存器(如 EAXEDX),操作前需准备好相关寄存器的值。

; 无符号乘法
mul <reg/mem>   ; EAX × 操作数 → EDX:EAX
; 有符号乘法
imul <reg/mem>              ; EAX × 操作数 → EDX:EAX
imul reg, <reg/mem>         ; reg ← 被乘数 × 操作数
imul reg, <reg/mem>, <imm>  ; reg ← 操作数 × 常数
; 无符号除法
div <reg/mem>   ; 被除数:EDX:EAX,商 → EAX,余数 → EDX
; 有符号除法
idiv <reg/mem>  ; 被除数:EDX:EAX,商 → EAX,余数 → EDX
; 无符号乘法
mov eax, 6
mov ebx, 4
mul ebx        ; → EAX = 24, EDX = 0

; 有符号乘法
mov eax, -6
mov ebx, 4
imul ebx       ; → EAX = -24, EDX = 0

imul ecx, ebx      ; ecx ← ecx * ebx
imul edx, ebx, 10  ; edx ← ebx * 10

; 无符号除法
mov edx, 0     ; 高位清零
mov eax, 20
mov ecx, 3
div ecx        ; → EAX = 6, EDX = 2 (20 ÷ 3)

; 有符号除法
mov eax, -20
cdq            ; EDX ← EAX 的符号扩展
mov ecx, 3
idiv ecx       ; → EAX = -6, EDX = -2

除法指令可能会触发 异常

  • 除数等于 0:无符号或有符号除法中,如果除数是 0,会导致数学上未定义,立即触发异常。
  • 结果溢出:特别常见于 IDIV 有符号除法中,被除数是最小负数 0x80000000,除以 -1 会得到 0x80000000(超出 32 位有符号整数范围)。

位操作

位操作用于对寄存器或内存中的二进制位直接进行按位运算,位操作包含如下类型:

  • AND:保留指定位,其它位清零;
  • OR:将指定位设置为 1;
  • XOR:将指定位翻转(0 ↔ 1);
  • NOT:将所有位取反(补码的按位非);
and <reg/mem>, <reg/mem/con>   ; 按位与:目标 ← 目标 & 源
or  <reg/mem>, <reg/mem/con>   ; 按位或:目标 ← 目标 | 源
xor <reg/mem>, <reg/mem/con>   ; 按位异或:目标 ← 目标 ^ 源
not <reg/mem>                  ; 按位取反:目标 ← ~目标
; 按位与:清除低位
and eax, 0xF0      ; eax ← eax & 0xF0,仅保留高 4 位

; 按位或:设置低位
or eax, 0x0F       ; eax ← eax | 0x0F,将低 4 位全部置为 1

; 按位异或:清零技巧
xor eax, eax       ; eax ← eax ^ eax,结果为 0

; 按位取反:翻转全部位
not eax            ; eax ← ~eax

自增自减

自增(INC)与自减(DEC)分别等价于对操作数加 1 或减 1,常用于循环计数或栈指针调整等场景。

  • INC 等同于 ADD 1
  • DEC 等同于 SUB 1
inc <reg/mem>    ; 加 1:目标 ← 目标 + 1
dec <reg/mem>    ; 减 1:目标 ← 目标 - 1
inc eax          ; eax ← eax + 1
dec ebx          ; ebx ← ebx - 1
inc byte [cnt]   ; 将内存中 cnt 所指的字节加 1

比较

CMP 指令用于比较两个操作数的差值,不保存结果,只更新条件标志位(如 ZFSFCFOF),常与条件跳转指令配合使用。

本质上,CMP A, B 等价于 SUB A, B,但不会改变 A 的值。

cmp <reg/mem>, <reg/mem/con>  ; 比较:目标 - 源,仅影响标志位
cmp eax, ebx       ; 比较 eax 和 ebx
je  equal_label    ; 若 eax == ebx,跳转
cmp byte [x], 0
jl  less_than_zero ; 若 [x] 为负数,跳转

比较运算和加减操作一样,会影响以下 条件标志

  • ZF(Zero Flag):结果是否为零
  • SF(Sign Flag):结果是否为负
  • CF(Carry Flag):是否产生了进位/借位(无符号溢出)
  • OF(Overflow Flag):是否有符号溢出

这些标志标志用于后续的 条件跳转,如 jejgjl 等。

移位

移位是一种常见的位运算操作,常用于实现快速的乘法、除法、符号扩展等功能。根据处理方式不同,移位可分为逻辑移位、算术移位和循环移位三类:

  • 逻辑移位(Logical Shift):用于无符号数,空位统一用 0 填充。
  • 算术移位(Arithmetic Shift):用于有符号数,右移时保持符号位不变。
  • 循环移位(Rotate Shift):将移出的位补回另一端,不丢失任何一位。
1
0
1
1
1
0
1
0
1
0
1
1
1
0
1
0
1
0
1
1
1
0
1
0
0
1
1
1
0
1
0
1
循环右移
循环左移
1
0
1
1
1
0
1
0
1
0
1
1
1
0
1
0
1
0
1
1
1
0
1
0
0
1
1
1
0
1
0
0
逻辑右移
逻辑左移
1
0
1
1
1
0
1
0
1
0
1
1
1
0
1
1
1
0
1
1
1
0
1
0
0
1
1
1
0
1
0
0
算术右移
算术左移
高位补充符号位
高位补充 0

常见的移位指令如下表所示:

指令含义说明
SHL/SAL左移(逻辑/算术)功能相同,等效于乘以 2 的幂
SHR逻辑右移高位补 0,用于无符号数
SAR算术右移高位补符号位(即保留符号),用于有符号数
ROL循环左移将最高位移出,补入最低位,位模式循环
ROR循环右移将最低位移出,补入最高位,位模式循环
逻辑移位

逻辑移位适用于 无符号整数,移位时将空出的位补为 0,不考虑操作数的符号。

  • 逻辑左移 (SHL):整体向左移动,低位补 0,高位移出丢弃;
  • 逻辑右移 (SHR):整体向右移动,高位补 0,低位移出丢弃。
; 逻辑移位的实例
mov al, 10000000b  ; 原值 -128(补码表示)
shr al, 1          ; 结果变为 01000000b,即十进制 64

尽管原数是负数,但使用 SHR 逻辑右移时,仍然将高位补 0,因此结果不再保留符号。

算术移位

算术移位适用于 有符号整数,在右移时会 保留符号位(最高位),使符号不变,符合数学意义上的除法。

  • 算术左移 (SAL):整体向左移动,低位补 0,高位移出丢弃;
  • 算术右移 (SAR):整体向右移动,最高位保持原符号位的值。
; 算术移位的实例
mov al, -16        ; 二进制补码:11110000(0xF0)
sar al, 1          ; 结果:11111000(0xF8)→ -8

由于保留了最高位 1,右移后结果仍为负数。

循环移位

循环移位(Rotate Shift)是一种将移出的位 重新从另一端补入 的移位方式,不改变位的总数,也不会丢弃任何一位,常用于加密、校验等需要 “保留所有信息” 的场景。

常见类型包括:

  • 循环左移 (ROL):将最高位移出后补入最低位;
  • 循环右移 (ROR):将最低位移出后补入最高位。
; 循环右移的实例
mov al, 10000001b  ; 原值:0x81
ror al, 1          ; 结果:11000000b(原最低位 1 补到了最高位)

与逻辑移位和算术移位不同,循环移位不引入新的位填充,因此所有位的内容只是位置发生变化,适用于 无符号与有符号数 的位模式操作,但不适合做乘除法运算。

控制转移指令

无条件跳转

无条件跳转到某个标签(label)。

标签是一个可识别的标识符,标签通常是一个有意义的名字,后跟一个冒号,用于标记程序中的某个位置或地址。

jmp label

跳转指令编译后通常使用 相对寻址,也就是 跳转偏移量 是相对于下一条指令的地址(即当前 PC + 指令长度) 来计算的。

对于 jmp label 指令,若 label 在距离当前指令之后 N 字节处,则相对寻址的偏移量可以通过以下公式计算:

偏移量 = 标签地址 - (当前 PC 值 + 指令长度)

条件跳转

CMP 指令后尝尝跟一个条件跳转指令,条件跳转指令会检查 标志寄存器(FLAGS)的标志,从而决定是否跳转到某个标签(条件成立时),如果选择不跳转的话,则继续向后执行。

指令全称跳转条件
JE/JZjump equal/zero(相等/零)ZF=1
JNE/JNZnot equal/zero(不等/非零)ZF=0
JG(大于)greater(大于)ZF=0 且 SF=OF
JL(小于)less(小于)SF≠OF
JGE(大于等于)greater equal(大于等于)SF=OF
JLE(小于等于)less equal(小于等于)ZF=1 或 SF≠OF
; 比较 eax 和 ebx 的值
cmp eax, ebx
; 执行条件跳转
je equal_label

子程序调用

在汇编语言中,调用一个子程序通常使用 CALL 指令,执行完子程序后使用 RET 指令返回。两者配合,实现了 从主程序跳转到子程序,再返回继续执行 的控制流程。

CALL 指令用于调用子程序,涉及以下步骤:

  • 保存返回地址:将当前指令的下一个地址(即返回地址)压入栈中,这样子程序返回时才知道从哪一条指令继续执行。
  • 跳转到子程序:将程序计数器设置为子程序的入口地址,开始执行子程序的代码。

RET 指令用于从子程序返回到调用函数,涉及以下步骤:

  • 从栈中弹出返回地址:从栈顶弹出一个值,并将这个值作为返回地址。这是之前 CALL 指令压入栈的地址。
  • 跳转到返回地址:将程序计数器设置为返回地址,继续执行从调用子程序的指令的下一条指令。
call subroutine
...
subroutine:
    ; 执行一些操作
    ret

陷阱指令

陷阱指令(Trap Instruction)是一类特殊的 同步异常触发指令,用于从 用户态切换到内核态,以请求操作系统执行特权操作。它们本质上是一种 软件中断机制,通常用于:

  • 实现 系统调用(如文件操作、进程控制等);
  • 支持 断点调试(例如 IDE 或 GDB 中设置断点);
  • 报告程序运行中出现的 异常情况(如除 0、非法访问)等。

陷阱的特点是由 程序主动触发,与硬件中断(如 I/O、时钟中断)区分开来。

INT

INT 指令用于产生一个软件中断,它后面跟着一个中断向量号(通常是一个字节大小的立即数),用于指定要调用的中断或服务例程:

INT n  ; n 为中断向量号(0~255)

执行后,CPU 根据中断向量号 n 查找 中断向量表(Interrupt Vector Table, IVT)中对应的处理程序地址,并跳转执行。常见用法如下:

mov eax, 1   ; 系统调用:exit
mov ebx, 0   ; 退出代码
int 0x80     ; 触发中断
补充

Trap 指令和 TF 标志位的区别

陷阱指令(如 INT) ≠ TF 标志位。

  • INT 指令显式触发陷阱,由程序执行。
  • TF 是 EFLAGS 寄存器中的一个位,设置为 1 后每执行一条指令就引发一次单步中断(INT 1),用于单步调试。
  • 二者都能进入内核态,但触发机制不同。

Trap 指令一般通过 INT 指令执行,TF 标志位通过 PUSHFPOPF 修改。

协处理器指令

开关中断

CLISTI 用于控制 CPU 的中断响应能力。具体来说,这两条指令用于修改处理器的中断标志(IF,Interrupt Flag),从而控制外部硬件中断的使能和禁止。

中断标志 (IF) 状态寄存器中的一个标志位。如果 IF 位被设置(即为 1),处理器将响应外部硬件中断。如果 IF 位被清除(即为 0),处理器将忽略外部硬件中断请求。

  • CLI:清除中断标志位(Clear Interrupt Flag),将 IF 位设置为 0,从而禁止处理器响应外部硬件中断。
  • STI:设置中断标志位(Set Interrupt Flag),将 IF 位设置为 1,从而允许处理器响应外部硬件中断。
cli   ; 关闭中断
sti   ; 开启中断
补充

IF 位控制的是 可屏蔽中断(IRQ),但不会影响 不可屏蔽中断(NMI)。

输入输出

INOUT 指令用于处理与外部设备的输入/输出(I/O)操作。这些指令让 CPU 可以直接与硬件端口通信,从而读取或发送数据。

寄存器
寄存器
端口
端口
电流特性的转换
电流特性的转换
端口
电流特性的转换
CPU
I/O 接口
in 指令
out 指令
键盘
显示器
外设
  • IN:从指定的 I/O 端口读取数据到寄存器。通过 IN 指令,可以从硬件设备读取状态信息或数据。
  • OUT将寄存器中的数据写入到指定的 I/O 端口。通过 OUT 指令,CPU 可以向设备发送控制命令或数据。
; 输入指令
in  reg, port      ; 从 I/O 端口 port 读取一个字节/字到 reg 寄存器

; 输出指令
out port, reg      ; 将 reg 寄存器中的值写入 I/O 端口 port
; 读取键盘控制器状态(端口 0x60)
in al, 0x60

; 将 AL 中的数据再次写回键盘控制器
out 0x60, al

注意 I/O 端口的 编址方式 有两种:统一编址 和 独立编址,IN/OUT 指令仅适用于独立编址,对于统一编址,使用 MOV 指令即可完成输入输出。

输入输出指令(如 IN / OUT)常用于 程序查询方式程序中断方式 中,由 CPU 发出指令与设备进行数据交换。相比之下,DMA 方式则由硬件控制器自动完成传输,通常不依赖此类指令。

程序查询方式 和 程序中断方式 这两种 I/O 方式的实现的核心逻辑可以参考下述代码:

// 用户态伪代码
write_to_device(data) {
    while (1) {
        if (kernel_read_port(STATUS_PORT) & READY_BIT) { // 轮询
            kernel_write_port(DATA_PORT, data);          // 实际执行 OUT
            break;
        }
    }
}

// 内核态驱动伪代码,用一个 C 函数封装了底层输入输出汇编指令
// 通过读取端口内容,获取设备状态
uint8_t kernel_read_port(uint16_t port) {
    asm("in %%dx, %%al" : "=a"(value) : "d"(port));
    return value;
}

// 通过端口向设备写入
void kernel_write_port(uint16_t port, uint8_t data) {
    asm("out %%al, %%dx" : : "a"(data), "d"(port));
}
// 用户程序只发起请求,挂起等待中断
read_from_device(buffer) {
    request_read();  // 向设备发送读取命令
}

// 内核:中断服务例程
interrupt_handler() {
    uint8_t data = inb(DATA_PORT);  // IN 指令读取设备数据
    kernel_buffer = data;
    wake_up_user();
}
  • 在程序查询方式中,CPU 通过反复执行输入输出指令轮询设备状态,当设备准备就绪后再通过输入输出指令执行读写操作。
  • 在中断方式中,当设备准备好数据后会向 CPU 发送中断信号;CPU 响应后进入中断服务程序,并在其中执行输入输出指令与设备进行数据交换。

字符串操作指令

字符串操作指令基本不考察,这里简单了解即可。

MOVS

复制字符串(ES:EDIDS:ESI)。

movs byte ptr es:[edi], byte ptr ds:[esi]  ; 复制 1 字节
movs dword ptr es:[edi], dword ptr ds:[esi]  ; 复制 4 字节
LODS

DS:ESI 加载数据到 AL/AX/EAX

lodsb  ; 读取 1 字节
lodsd  ; 读取 4 字节
STOS

AL/AX/EAX 存储到 ES:EDI

stosb  ; 存储 1 字节
stosd  ; 存储 4 字节
CMPS

比较两个字符串。

cmpsb  ; 比较字节
cmpsd  ; 比较 4 字节

总结

指令类别说明
MOV数据传输
PUSH/POP堆栈操作
ADD/SUB加减运算
MUL/DIV乘除运算
AND/OR/XOR/NOT逻辑运算
CMP比较
JMP无条件跳转
Jxx条件跳转
CALL/RET子程序调用
IN/OUT输入/输出
MOVS/LODS/STOS字符串操作
INT触发中断
CLI/STI控制中断