第 6 章 80x86 指令系统⚓︎
约 4715 个字 451 行代码 预计阅读时间 29 分钟
指令结构⚓︎
操作数共有 3 种类型:立即数(常数)(idata)、寄存器 (reg)、变量 (mem)
注意
我们约定:
- 用方括号表示可选的前缀或命令
- 用竖线表示实际使用时,在这些指令中选择其一(或)
数据传送指令⚓︎
通用数据传送指令⚓︎
- 功能:赋值,等价于
dest = src;
- 格式:
dest
可以是寄存器 / 内存,src
可以是立即数 / 寄存器 / 内存,但两者不能同时为内存 - 注:
- 该指令不影响任何标志位
dest
和src
必须等宽- 不能将立即数或段寄存器赋值给段寄存器
- 不能直接对
cs
进行赋值 - (任何指令都)不能引用
ip
和fl
- 功能:将
op
压入栈内 -
等价操作:
-
格式:
op
可以是 16/32 位的寄存器 / 内存 - 注:
- 该指令不影响任何标志位
- 不支持 8 位宽度的操作数
- 功能:将从栈中弹出的数据放入
op
中 -
等价操作:
-
格式:同
push
指令 - 注:同
push
指令
- 功能:交换
op1
与op2
-
等价操作:
-
格式:
op1
、op2
可以是寄存器 / 内存,但不能同时为内存 - 注:
- 该指令不影响任何标志位
- 操作数中不能有段寄存器
输入输出指令⚓︎
- 功能:从
port
号端口读取一个字节并保存到al
中,等价操作为al = [port];
,其中[port]
表示port
号端口的值 - 格式:
port
可以是立即数或寄存器dx
- 注:立即数的取值范围为 [00h, 0FFh],寄存器的取值范围为 [0000h, 0FFFFh],使用时要注意
- 功能:把
al
的值写入port
号端口,等价操作为[port] = al;
- 格式:同
in
指令 - 注:同
in
指令
地址传送指令⚓︎
- 功能:取变量
src
的偏移地址,并赋值给dest
,等价操作为dest = offset src;
- 格式:
src
可以是寄存器 / 内存
- 功能:取出保存在变量
src
中的远指针,先将远指针的段地址部分赋值给ds
,再将远指针的偏移地址部分赋值给dest
-
等价操作:
-
格式:
dest
可以是寄存器或 32 位内存
- 功能:取出保存在变量
src
中的远指针,先将远指针的段地址部分赋值给es
,再将远指针的偏移地址部分赋值给dest
-
等价操作:
-
格式:同
lds
指令
标志寄存器传送指令⚓︎
- 功能:将标志寄存器
fl
的低 8 位赋值给ah
,等价操作为ah = fl & 0FFh
- 格式:
lahf
- 功能:将
ah
赋值给fl
的低 8 位 -
等价操作:
-
格式:
sahf
转换指令⚓︎
扩充指令⚓︎
- 功能:将
al
中的值符号扩展至ax
中,即把字节扩充至字 -
等价操作:
-
格式:
cbw
- 功能:将
ax
中的值符号扩展至dx:ax
(dx
、ax
分别存储一个值的高 16 位和低 16 位) ,即把字扩充至双字 -
等价操作:
-
格式:
cwd
- 功能:将
eax
中的值符号扩展至edx:eax
(edx
、eax
分别存储一个值的高 32 位和低 32 位) ,即把双字扩充至四字 -
等价操作:
-
格式:
cdq
- 功能:将
src
符号扩展至dest
中 -
等价操作:
-
格式:
- 若
dest
是 16 位寄存器,则src
可以是 8 位的寄存器 / 内存 - 若
dest
是 32 位寄存器,则src
可以是 8/16 位的寄存器 / 内存
- 若
- 功能:把
src
零扩展至dest
中 -
等价操作:
-
格式:同
movsx
指令
换码指令⚓︎
- 功能:把
byte ptr ds:[bx + al]
的值赋值给al
,等价操作为al = byte ptr ds:[bx + al]
- 格式:
xlat
算术运算指令⚓︎
加法指令⚓︎
- 功能:等价于
dest += src;
- 格式:
dest
可以是寄存器 / 内存,src
可以是立即数 / 寄存器 / 内存,但两者不能同时为内存
- 功能:等价于
op++;
- 格式:
op
可以是寄存器 / 内存 - 注:
inc
指令不影响CF
- 功能:带进位加法 (add with carry),等价于
dest += src + CF;
- 格式:同
add
指令 - 注:可以用该指令模拟 32 位加法
减法指令⚓︎
- 功能:等价于
dest -= src;
- 格式:同
add
指令
- 功能:等价于
op--;
- 格式:同
inc
指令
- 功能:带借位减法 (subtract with borrow),等价于
dest -= src + CF
- 格式:同
add
指令 - 注:可以用该指令模拟 32 位减法
- 功能:计算
op
的相反数,等价于op = -op;
- 格式:同
inc
指令
- 功能:比较
op1
和op2
,等价于temp = op1 - op2
- 格式:同
add
指令 - 注:
cmp
指令并不会保存op1 - op2
的差,但会影响状态标志cmp
指令后通常会跟随jcc
条件跳转指令,跟无符号数、符号数比较相关的jcc
类指令请见 [ 条件跳转指令 ] 一节
乘法指令⚓︎
- 功能:无符号数乘法
src
为 8 位宽度时:ax = al * src;
src
为 16 位宽度时:dx:ax = ax * src;
src
为 32 位宽度时:edx:eax = eax * src;
- 格式:
src
可以是寄存器 / 内存
- 功能:符号数乘法,同
mul
指令分为 3 种情况 - 格式:同
mul
指令
除法指令⚓︎
- 功能:无符号数除法
src
为 8 位宽度时:al = ax / src;
ah = ax % src;
src
为 16 位宽度时:ax = dx:ax / src;
dx = dx:ax % src;
src
为 32 位宽度时:eax = edx:eax / src;
edx = edx:eax % src;
- 格式:同
mul
指令 - 注:若除数为 0,或保存商的寄存器无法容纳商时都会发生除法溢出,此时 CPU 会在除法指令上方插入并执行一条
int 00h
指令
- 功能:符号数除法,同
div
指令分为 3 种情况 - 格式:同
mul
指令 - 注:除法溢出的触发和解决方法同
div
指令
浮点运算指令⚓︎
浮点数的存储格式和求值公式:
浮点数寄存器:
- FPU 有 8 个浮点数寄存器,用于浮点运算,宽度均为 80 位,名称为
st(i)
(\(i \in [0, 7]\)) ,其中st(0)
可简写为st
- 这 8 个浮点数寄存器构成了一个 FPU 堆栈
- 栈顶的浮点数寄存器的物理编号为 TOP(3 位二进制数,位于 FPU 状态寄存器的第 11 至第 13 位
) ,逻辑编号恒为 0 st(i)
的逻辑编号为i
,物理编号p = (TOP + i) % 8
- 栈顶的浮点数寄存器的物理编号为 TOP(3 位二进制数,位于 FPU 状态寄存器的第 11 至第 13 位
浮点运算相关指令:
fadd
、fsub
、fmul
、fdiv
:加减乘除的浮点数版本,具体细节不再赘述- 其他指令:
- 功能:将
op
(浮点数)压入 FPU 堆栈 - 格式:
op
可以是 32 位 /64 位 /80 位的内存,或者浮点数寄存器st(i)
- 功能:先将
op
(整数)转化为浮点数类型,然后将其压入 FPU 堆栈 - 格式:
op
可以是 32 位 /64 位 /80 位的内存
- 功能:把
st(0)
保存到op
中 - 格式:
op
可以是 32 位 /64 位的内存,或者浮点数寄存器st(i)
- 功能:先把
st(0)
保存到op
中,再把st(0)
从 FPU 堆栈中弹出 - 格式:同
fld
指令
十进制调整指令⚓︎
BCD 码(Binary Coded Decimal):用二进制编码的十进制数,分为压缩 BCD 码和非压缩 BCD 码
-
压缩 BCD 码:用 4 个二进制位表示 1 个十进制位
- 因此 8 个二进制位最多表示从 00 到 99 共 100 个压缩 BCD 码
-
非压缩 BCD 码:用 8 个二进制位表示 1 个十进制位
- 8 位上的高 4 位没有意义,可以为任意值
- 因此 16 个二进制位最多表示从 0000h 到 0909h 共 100 个非压缩 BCD 码
压缩 BCD 码调整指令⚓︎
- 功能:压缩 BCD 码的加法调整
-
等价操作:
-
格式:
daa
- 功能:压缩 BCD 码的减法调整
-
等价操作:
-
格式:
das
非压缩 BCD 码调整指令⚓︎
- 功能:非压缩 BCD 码的加法调整
-
等价操作:
-
格式:
aaa
- 功能:非压缩 BCD 码的减法调整
-
等价操作:
-
格式:
aas
逻辑运算指令⚓︎
- 功能:与运算,等价操作为
dest &= src;
- 格式:同
add
指令
- 功能:或运算,等价操作为
dest |= src;
- 格式:同
add
指令
- 功能:异或运算,等价操作为
dest ^= src;
- 格式:同
add
指令
- 功能:取反运算,等价操作为
op = ~op;
- 格式:
op
可以是寄存器 / 内存
- 功能:位检测指令,它计算
dest & src
而不保存结果,但会影响状态标志。等价操作为temp = dest & src;
- 格式:同
add
指令
移位指令⚓︎
- 功能:逻辑左移,等价操作为
dest <<= count & 1Fh
- 格式:
dest
可以是寄存器 / 内存,count
可以是立即数 / 寄存器cl
- 注:若源代码开头有
.386
汇编指示语句,则立即数可以是一个 8 位大小的任意数,否则立即数只能等于 1
- 功能:逻辑右移,等价操作为
dest >>= count & 1Fh
- 格式:同
shl
指令 - 注:同
shl
指令
- 功能:算术左移,等价与
shl
- 格式:同
shl
指令 - 注:同
shl
指令
- 功能:算术右移,等价操作同
shr
,实际上略有区别,见下面图示 - 格式:同
shl
指令 - 注:同
shl
指令
- 功能:循环左移
-
等价操作:
-
格式:同
shl
指令 - 注:同
shl
指令
- 功能:循环右移
-
等价操作:
-
格式:同
shl
指令 - 注:同
shl
指令
- 功能:带进位循环左移
-
等价操作:
-
格式:同
shl
指令 - 注:同
shl
指令
- 功能:带进位循环右移
-
等价操作:
-
格式:同
shl
指令 - 注:同
shl
指令
字符串操作指令⚓︎
与字符串操作指令相关的指令前缀(可用可不用)包括:
rep
:重复repe
:若相等则重复repz
:若结果为 0 则重复repne
:若不相等则重复repnz
:若结果不为 0 则重复
其中第 2、3 个前缀等价,第 4、5 个前缀等价。这些指令前缀有一个共同的限制:重复执行最多cx
次字符串操作。
字符串复制指令⚓︎
- 功能:以字节为单位复制字符串
-
等价操作:
-
格式:
字符串比较指令⚓︎
- 功能:以字节为单位比较字符串
-
等价操作:
if (sizeof(operand) == 1) { // cmpsb 指令(1字节) temp = byte ptr ds:[si] - byte ptr es:[di]; // 比较两个字符串的字节数据 old_fl = fl; if (df == 0) { si++; di++; } else { si--; di--; } fl = old_fl; // 寄存器 fl 记录了比较结果,位于其 zf 位上 } else if (operand == 2) { // cmpsw 指令(2字节) temp = word ptr ds:[si] - word ptr es:[di]; // ... } else if (operand == 4) { // cmpsw 指令(4字节) temp = dword ptr ds:[si] - dword ptr es:[di]; // ... }
again: // 循环cx遍 if (cx == 0) { goto done; } if (sizeof(operand) == 1) { temp = byte ptr ds:[si] - byte ptr es:[di]; old_fl = fl; if (df == 0) { si++; di++; } else { si--; di--; } fl = old_fl; } else if (operand == 2) { temp = word ptr ds:[si] - word ptr es:[di]; // ... } else if (operand == 4) { temp = dword ptr ds:[si] - dword ptr es:[di]; // ... } if (zf == 1) { // 若前缀为 repe 且比较结果为相等,则重复,否则结束 if (prefix == repe) goto again; else goto done; } else if (zf == 0) { // 若前缀为 repne 且比较结果为不等,则重复,否则结束 if (prefix == repne) goto again; else goto done; } done:
-
格式:
搜索字符串指令⚓︎
个人感觉“搜索”这个词可能描述得不太准确,因为这一类指令实质上还是比较运算,只是比较的内容不是两个完整的字符串,而是将字符串的某几个字节(字符)与寄存器的值进行比较,但目前我也想不到比较合适的词汇,所以还是保留了这个标题。
- 功能:在 es:di 指向的目标字符串中搜索寄存器 al 的值(即比较 1 字节内容)
-
等价操作:
if (sizeof(operand) == 1) { // scasb 指令(1字节) temp = al - byte ptr es:[di]; // 比较 old_fl = fl; if (df == 0) di++; else di--; fl = old_fl; } else if (sizeof(operand) == 2) { // scasw 指令(2字节) temp = ax - word ptr es:[di]; // ... } else if (sizeof(operand) == 4) { // scasd 指令(4字节) temp = eax - dword ptr es:[di]; // ... }
again: if (cx == 0) goto done; if (sizeof(operand) == 1) { temp = al - byte ptr es:[di]; old_fl = fl; if (df == 0) di++; else di--; cx--; fl = old_fl; } else if (sizeof(operand) == 2) { temp = ax - word ptr es:[di]; // ... } else if (sizeof(operand) == 4) { temp = eax - dword ptr es:[di]; // ... } if (zf == 1) { if (prefix == repe) // 相等重复 goto again; else goto done; } else if (zf == 0) { if (prefix == repne) // 不等重复 goto again; else goto done; } done:
-
格式:
- 功能:在 es:di 指向的目标字符串中搜索寄存器 ax 的值(即比较 2 字节内容)
- 等价操作:见
scasb
指令 - 格式:
- 功能:在 es:di 指向的目标字符串中搜索寄存器 eax 的值(即比较 4 字节内容)
- 等价操作:见
scasb
指令 - 格式:
写入字符串指令⚓︎
- 功能:把 al 的值写入 es:di 指向的目标字符串中
-
等价操作:
again: if (cx == 0) goto done; if (sizeof(operand) == 1) { // stosb 指令(1字节) byte ptr es:[di] = al; // 写入 old_fl = fl; if (df == 0) di++; else di--; fl = old_fl; } else if (sizeof(operand) == 2) { // stosw 指令(2字节) word ptr es:[di] = ax; // ... } else if (sizeof(operand) == 4) { // stosd 指令(4字节) dword ptr es:[di] = eax; // ... } cx--; goto again; done:
-
格式:
读取字符串指令⚓︎
- 功能:从 ds:si 指向的源字符串中读取一个字节的数据,并保存到 al 中
- 等价操作:
if (sizeof(operand) == 1) { // lodsb 指令(1字节)
al = byte ptr ds:[si]; // 读取
old_fl = fl;
if (df == 0)
si++;
else
si--;
fl = old_fl;
} else if (sizeof(operand) == 2) { // lodsw 指令(2字节)
ax = word ptr ds:[si];
// ...
} else if (sizeof(operand) == 4) { // lodsd 指令(4字节)
eax = dword ptr ds:[si];
// ...
}
- 格式:
控制转移指令⚓︎
无条件跳转指令⚓︎
- 功能:短跳,跳到目标地址 dest
- 等价操作:
delta = idata8
delta |= (0 - ((delta & 80h) >> 7 & 1)) << 8; // 符号扩展至16位
ip += 2 + delta;
// dest = cs + ip
- 格式:
- 注:
- 该指令的机器码为 2 字节:
0E8h, idata8
,其中idata8
是一个 8 位符号数,表示短跳的跳转距离,故取值范围为 [-128, 127],超过该范围编译时会报错 - 编译器会自动判断跳转距离,因此
short
修饰可省略不写 - 短跳指令转化为机器码后,跳转距离
idata8
按以下公式计算:\(idata8 = dest - (\$ + 2)\),其中 \(\$\) 表示当前这条跳转指令自身的偏移地址
- 该指令的机器码为 2 字节:
- 功能:近跳,跳到目标地址 dest
- 等价操作:
- 格式:
- 注:
- 该指令的机器码为 3 字节:
0E9h, idata16_L8, idata16_H8
,后两者分别是 16 位符号数idata16
的低 8 位和高 8 位,表示近跳的跳转距离,故取值范围为 [-32768, 32767] - 编译器会自动判断跳转距离,因此
near ptr
修饰可省略不写 - 短跳指令转化为机器码后,跳转距离
idata16
按以下公式计算:\(idata16 = dest - (\$ + 3)\),其中 \(\$\) 表示当前这条跳转指令自身的偏移地址 - 该指令还可以将 16 位寄存器或内存作为跳转目标地址
- 该指令的机器码为 3 字节:
- 功能:远跳,跳到目标地址 dest
- 等价操作:
- 格式:
- 注:
- 该指令的机器码为 5 字节:
0E9h, idata32_L16, idata32_H16
,后两者分别是 32 位符号数idata32
的低 16 位和高 16 位,表示远跳的跳转目标地址 - 该指令的
dest
必须是已定义的标号,不能是某个常数 - 该指令还可以将 32 位内存作为跳转目标地址
- 该指令的机器码为 5 字节:
条件跳转指令⚓︎
- 功能:条件跳转
- 注:
- jcc 实际上指的是一类指令(具体见下面的表格
) ,并没有一个名为jcc
的指令 - jcc 指令的跳转距离均为 1 字节
- jcc 实际上指的是一类指令(具体见下面的表格
- jcc 指令汇总:
jcc 指令 | 含义 | 跳转条件 | 说明 |
---|---|---|---|
ja | 无符号大于则跳 | cf == 0 && zf == 0 |
|
jae | 无符号大于等于则跳 | cf == 0 |
|
jb | 无符号小于则跳 | cf == 1 |
|
jbe | 无符号小于等于则跳 | cf == 1 || zf == 1 |
|
je | 相等则跳 | zf == 1 |
|
jne | 不等则跳 | zf == 0 |
|
jg | 符号大于则跳 | sf == of && zf == 0 |
|
jge | 符号大于等于则跳 | sf == of |
|
jl | 符号小于则跳 | sf != of |
|
jle | 符号小于等于则跳 | sf != of || zf == 1 |
|
jc | 有进位则跳 | cf == 1 |
|
jnc | 无进位则跳 | cf == 0 |
|
jz | 有零标志则跳 | zf == 1 |
|
jne | 无零标志则跳 | zf == 0 |
|
js | 有符号位则跳 | sf == 1 |
|
jns | 无符号位则跳 | sf == 0 |
|
jo | 有溢出则跳 | of == 1 |
|
jno | 无溢出则跳 | of == 0 |
|
jp | 有奇偶校验标志则跳 | pf == 1 |
|
jnp | 无奇偶校验标志则跳 | pf == 0 |
|
jcxz | cx=0 则跳 | cx == 0 |
|
jecxz | ecx=0 则跳 | ecx == 0 |
循环指令⚓︎
- 功能:循环
- 等价操作:
- 格式:
- 注:
- loop 的跳转距离为 1 字节,即跳转范围为 [-128, 127]
- 需要注意 loop 先会对 cx 减 1 然后再与 0 判断,所以在 loop 前令 cx=0 反而会循环最大次数 10000h 次
- 功能:若等于 0 则循环
- 等价操作:
- 格式:
- 注:loopz = loope
- 功能:若不等于 0 则循环
- 等价操作:
- 格式:
- 注:loopnz = loopne
子程序调用与返回指令⚓︎
- 功能:近调用,目标地址为 dest
- 等价操作:
- 格式:
- 注:
- 该指令的机器码为 3 字节:
0E8h, idata16_L8, idata16_H8
,后两者分别是 16 位符号数idata16
的低 8 位和高 8 位,表示近调用的跳转距离,故取值范围为 [-32768, 32767] - 编译器会自动判断跳转距离,因此
near ptr
修饰可省略不写 - 短跳指令转化为机器码后,跳转距离
idata16
按以下公式计算:\(idata16 = dest - (\$ + 3)\),其中 \(\$\) 表示当前这条跳转指令自身的偏移地址 - 该指令还可以将 16 位寄存器或内存作为跳转目标地址
- 该指令的机器码为 3 字节:
怎么好像和jmp near ptr dest
差不多呀 ...
- 功能:近返回
- 等价操作:
- 格式:
- 功能:远调用,目标地址为 dest
- 等价操作:
- 格式:
- 注:
- 该指令的机器码为 5 字节:
9Ah, idata32_L16, idata32_H16
,后两者分别是 32 位符号数idata32
的低 16 位和高 16 位,表示远调用的目标地址 - 该指令的
dest
必须是已定义的标号,不能是某个常数 - 该指令还可以将 32 位内存作为跳转目标地址
- 该指令的机器码为 5 字节:
- 功能:远返回
- 等价操作:
back_ip = word ptr ss:[sp];
back_cs = word ptr ss:[sp+2];
sp += 4;
if (idata16)
sp += idata16;
ip = back_ip;
cs = back_cs;
- 格式:
中断和中断返回指令⚓︎
- 功能:中断
- 等价操作:
old_fl = fl;
if = 0;
tf = 0;
sp -= 6;
word ptr ss:[sp] = ip + 2;
word ptr ss:[sp+2] = cs;
word ptr ss:[sp+4] = old_fl;
ip = word ptr 0000:[idata8 * 4]
cs = word ptr 0000:[idata8 * 4 + 2]
- 格式:
- 注:
- 该指令的机器码为 2 字节:
0CDh, idata8
,其中idata8
是中断号 - 该指令的目标地址是一个 32 位的远指针,称为中断向量(interrupt vector),被保存在 0000:idata8*4 处
- [0000:0000, 0000:03FFh] 这个内存区间称为中断向量表,一共存放了从
int 00h
到int 0FFh
共 256 个中断向量 - 中断大全
- 该指令的机器码为 2 字节:
- 功能:软件断点中断
- 等价操作:
old_fl = fl;
if = 0;
tf = 0;
sp -= 6;
word ptr ss:[sp] = ip + 1;
word ptr ss:[sp+2] = cs;
word ptr ss:[sp+4] = old_fl;
ip = word ptr 0000:[000Ch]
cs = word ptr 0000:[000Eh]
- 格式:
- 功能:溢出中断
- 等价操作:
if (of == 1) {
old_fl = fl;
if = 0;
tf = 0;
sp -= 6;
word ptr ss:[sp] = ip + 1;
word ptr ss:[sp+2] = cs;
word ptr ss:[sp+4] = old_fl;
ip = word ptr 0000:[0010h]
cs = word ptr 0000:[0012h]
}
- 格式:
- 功能:中断返回
- 等价操作:
back_ip = word ptr ss:[sp];
back_cs = word ptr ss:[sp+2];
back_fl = word ptr ss:[sp+4];
sp += 6;
fl = back_fl;
ip = back_ip;
cs = back_cs;
- 格式:
评论区