汇编语言:cmp 指令和条件转移
cmp 指令
cmp 指令的功能
cmp 指令是比较指令,其功能相当于不写回结果 sub 指令。它执行的是 操作对象1 − 操作对象2,然后根据这个结果去设置 flag,但不修改任何操作数。例如对于指令 cmp ax, ax,由于指令功能是比较 AX 与自身,等价于计算 AX - AX,因此结果恒为 0。执行该指令后的 flag 寄存器状态如下:
| 标志位 | 值 | 说明 |
|---|---|---|
| AX | 不变 | 操作数未发生改变 |
| ZF(零标志) | 1 | 结果为 0 |
| PF(奇偶标志) | 1 | 结果低 8 位中 1 的个数为偶数 |
| SF(符号标志) | 0 | 结果为非负 |
| CF(进位/借位标志) | 0 | 无借位 |
| OF(溢出标志) | 0 | 无溢出 |

例如对于以下下指令,本质是计算 AX - BX(8 - 3):
mov ax, 8
mov bx, 3
cmp ax, bx
执行后标志位的状态如下:
这三条指令执行后,本质是计算 AX - BX(8 - 3),仅影响标志位,不改变寄存器的值。具体情况如下:
| 标志位 | 值 | 说明 |
|---|---|---|
| AX | 8 | 保持不变 |
| BX | 3 | 保持不变 |
| ZF(零标志) | 0 | 结果不为 0 |
| PF(奇偶标志) | 1 | 结果低 8 位为 00000101,其中 1 的个数为 2(偶数) |
| SF(符号标志) | 0 | 结果为正(最高位为 0) |
| CF(借位标志) | 0 | 无借位(AX ≥ BX,无符号数比较:8 ≥ 3) |
| OF(溢出标志) | 0 | 正数减正数,结果为正,无溢出 |

无符号数的比较
对于无符号数,cmp 的结果只需要关注有没有借位,例如指令 cmp ax, bx 的不同结果和对应的 flag 寄存器的状态如下表所示:
| 比较关系 | 标志位表现 |
|---|---|
ax == bx |
ZF = 1 |
ax != bx |
ZF = 0 |
ax < bx |
CF = 1(有借位) |
ax >= bx |
CF = 0(无借位) |
ax > bx |
CF = 0 且 ZF = 0 |
ax <= bx |
CF = 1 或 ZF = 1 |
有符号数的比较
有符号数的比较相对比较复杂,因为需要同时关注结果的符号(SF)和是否发生了溢出(OF)。例如对于如下指令:
mov ah, 22H ; 34
mov bh, 0A0H ; -96(补码)
cmp ah, bh
cmp 指令计算 34 - (-96) = 130,但 8 位有符号数最大只能表示 127。因此计算结果溢出,AH 中得到 82H(-126 的补码),实际结果为负所以 SF = 1。但是逻辑上 34 > -96,但 SF = 1,说明溢出导致实际存下的结果不止只看 SF 位。

与之对应,例如如下指令:
mov ah, 8AH ; -118
mov bh, 70H ; 112
cmp ah, bh
-118 - 112 = -230 超出 -128~127,溢出后实际结果为 1AH(26),实际结果非负所以 SF = 0。逻辑上 -118 < 112,说明在有溢出的情况下,SF 不能代表逻辑上的正负。

因此对于有符号数的正负性,可以通过 SF 和 OF 进行联合判断,SF 指示实际存下来的结果是正还是负,OF 指示告诉结果是不是因为溢出才变成这样的。以 cmp ah, bh 为例,总结如下:
| SF | OF | 逻辑结论 | 原因 |
|---|---|---|---|
| 1 | 0 | ah < bh |
无溢出,实际负 = 逻辑负 |
| 1 | 1 | ah > bh |
有溢出,实际负是“假负”,逻辑应为正 |
| 0 | 0 | ah >= bh |
无溢出,实际非负 = 逻辑非负 |
| 0 | 1 | ah < bh |
有溢出,实际正是“假正”,逻辑应为负 |
一个直接的想法是:CPU 可以只给提供一个比较结果,比如一个 2 位编码表示 >、=、<。但实际上 8086 是通过 CF、ZF、SF、OF 这些标志位来表示,如此设计的原因有 3 点:
- 通用性:同一个 flag 寄存器既能无符号比较,也能进行有符号比较;
- 灵活性:程序员可以根据需要组合这些标志,比如只考虑无符号就忽略 OF;
- 历史兼容性:该设计在后来的 x86、ARM、RISC-V 中都得到了延续,只是命名不同。
条件转移指令
条件转移指的是指令可以根据某种条件,决定是否修改 IP。条件转移的依据是 flag 寄存器的内容,因此可以使用 cmp 对 flag 进行设置,然后用条件转移指令“消费”这些结果。条件转移指令的转移位移只有 -128~127 字节,机器码里只存位移,不存绝对地址。
对于无符号数,CPU 只关注数值是否相等吗、是否有借位、结果是否为零,对应的标志位是 ZF 和 CF。8086 中最常用的无符号条件转移指令有:
| 指令 | 含义 | 检测条件 | 等价于 |
|---|---|---|---|
je |
等于则转移 | ZF = 1 | jz |
jne |
不等于则转移 | ZF = 0 | jnz |
jb |
低于则转移 | CF = 1 | jc |
jnb |
不低于则转移 | CF = 0 | jnc |
ja |
高于则转移 | CF = 0 且 ZF = 0 | |
jna |
不高于则转移 | CF = 1 或 ZF = 1 |
指令的名字是给人看的语义,指令中的字母对应的单词如下:
| 字母 | 单词 |
|---|---|
j |
jump |
e |
equal |
ne |
not equal |
b |
below(低于) |
a |
above(高于) |
n |
not |
CPU 中只基于 flag 的值来工作,例如对于如下程序:
assume cs:code
code segment
start:
mov ax, 0
add ax, 0
je s
inc ax
s: inc ax
mov ax,4c00H
int 21H
code ends
end start
因为 add ax, 0 设置了 ZF=1,je 依然会跳转。cmp 和 je 的配合只是一种建议,不是强制规则,可以用 add、sub、and 等能影响 flag 的指令来控制 je。

例如考虑一个统计等于 8 的字节数量的程序:
assume cs:code, ds:data
data segment
db 8,11,8,1,8,5,63,38
data ends
code segment
start:
mov ax, data
mov ds, ax
mov bx, 0
mov ax, 0
mov cx, 8
s:
cmp byte ptr [bx], 8
jne next
inc ax
next:
inc bx
loop s
mov ax,4c00H
int 21H
code ends
end start
逻辑就是使用 cmp 判断当前的数是否等于 8,等于时 ZF = 1。jne next 在 ZF = 0 时(表示上一条指令的比较纠结伦是不等于 8)则转移跳过,等于就会用 inc ax 计数,。

当需要统计大于 8 的字节时:
assume cs:code, ds:data
data segment
db 8,11,8,1,8,5,63,38
data ends
code segment
start:
mov ax, data
mov ds, ax
mov bx, 0
mov ax, 0
mov cx, 8
s:
cmp byte ptr [bx], 8
jna next ; 不高于(<=)就跳过
inc ax
next:
inc bx
loop s
mov ax,4c00H
int 21H
code ends
end start

jna 指令在 CF=1 或 ZF=1 时发生转移,因此用它来检测 <= 8 在逻辑上等价于“大于 8 才计数”。

当需要统计小于 8 的字节时:
assume cs:code, ds:data
data segment
db 8,11,8,1,8,5,63,38
data ends
code segment
start:
mov ax, data
mov ds, ax
mov bx, 0
mov ax, 0
mov cx, 8
s:
cmp byte ptr [bx], 8
jnb next ; 不低于(>=)就跳过
inc ax
next:
inc bx
loop s
mov ax,4c00H
int 21H
code ends
end start
jnb 在 CF=0 时跳转,用其检测 >= 8在逻辑上等价于“小于 8 才计数”。

串传送指令
DF 标志
flag 的第 10 位是 DF(Direction Flag,方向标志位),它决定串操作指令执行后,SI 和 DI 是递增还是递减。DF 的值对应的行为如下表所示:
| DF 值 | 行为 | 适用场景 |
|---|---|---|
DF = 0 |
SI、DI 每次 +1(字节)或 +2(字) |
正向拷贝,低地址 → 高地址 |
DF = 1 |
SI、DI 每次 −1(字节)或 −2(字) |
逆向拷贝,高地址 → 低地址 |
CPU 提供两条指令来设置 DF:
| 指令 | 作用 |
|---|---|
cld |
Clear Direction Flag,令 DF = 0 |
std |
Set Direction Flag,令 DF = 1 |
单步搬运
movsb(Move String Byte)指令的功能把 DS:SI 指向的一个字节复制到 ES:DI,然后根据 DF 更新 SI 和 DI。用伪代码描述则是:
ES:[DI] = DS:[SI]
if (DF == 0) {
SI += 1
DI += 1
} else {
SI -= 1
DI -= 1
}
movsw(Move String Word)指令则是对字数据进行操作,功能是复制一个字(2 字节),SI 和 DI 每次增减 2。用伪代码描述则是:
ES:[DI] = DS:[SI]
if (DF == 0) {
SI += 2
DI += 2
} else {
SI -= 2
DI -= 2
}
rep 指令
rep(Repeat) 指令的功能是:只要 CX ≠ 0,就执行一次后面的串指令,然后 CX--。它不和任何特定指令绑定,通常是和 movsb / movsw 一起使用,例如 rep movsb。这条指令的逻辑等价于:
s:
movsb
loop s
使用 rep movsb 需要设置好以下内容:
| 寄存器 | 作用 |
|---|---|
DS:SI |
源地址 |
ES:DI |
目标地址 |
CX |
重复次数 |
DF |
拷贝方向 |
例如考虑一个将 data 段中的 'Welcome to masm!' 复制到它后面的空间中的程序:
assume cs:code, ds:data, es:data
data segment
db 'Welcome to masm!' ; 16 字节
db 16 dup (?) ; 目标空间
data ends
code segment
start:
mov ax, data
mov ds, ax
mov es, ax
mov si, 0
mov di, 16
mov cx, 16
cld ; 正向拷贝
rep movsb
mov ax, 4c00h
int 21h
code ends
end start
字符串的源地址是 data:0,字符串长 16 字节所以 CX = 16,目标地址是 data:16。设置 DF = 0 表示方向从低到高,cld 确保每次拷贝后 SI/DI 自动增加,指向下一个字节。

例如考虑一个将 F000H 段中的最后 16 个字符复制到 data 段的程序。源地址是 F000:FFFF,即段尾最后一个字节,data 段第 16 个字节对应偏移 15、长度 CX = 16,目标地址是 data:000F。DF = 1 表示方向从高地址向低地址,因为要复制的是最后 16 个字节,因此可以从最后一个字节开始倒着往前拷贝。
assume cs:code, ds:data, es:data
data segment
db 16 dup (?) ; 目标缓冲区
data ends
code segment
start:
mov ax, 0F000H ; 设置源段 F000H
mov ds, ax
mov si, 0FFFFH ; 指向源尾
mov ax, data ; 设置目标段 data
mov es, ax
mov di, 15 ; 指向目标尾,偏移 15
mov cx, 16
std ; 逆向拷贝
rep movsb
mov ax, 4C00H
int 21H
code ends
end start
寄存器 di 初始化的值是 15 而不是 16,是因为 data 段中预留了 16 个字节(db 16 dup (0)),偏移范围是 0 ~ 15。要复制 16 个字节,最后一个字节应该放在偏移 15 处,movsb 是先复制再递减,所以 DI 须指向最后一个位置。如果写成 mov di, 16,第一次复制就会写到偏移 16,超出预留空间造成写入越界。

实验11
任务是编写一个子程序 letterc,将以 0 结尾的字符串中的小写字母 a~z 转换为大写,同时保持其他字符(如空格、标点、单引号等)不变。字符串以 0 作为结束符,因此使用 cmp al, 0 / je 的方式逐字符扫描,判断字符为 0 时跳转并返回。小写字母的 ASCII 范围是 61H~7AH,落在这个区间内的字符才需要转换,其余字符直接跳过。对于不是大写的字母,将字符的 bit 5 清零即可完成转换。该程序的代码如下:
assume cs:codesg, ds:datasg
datasg segment
db "Beginner's All-purpose Symbolic Instruction Code.", 0
datasg ends
codesg segment
begin:
mov ax, datasg
mov ds, ax
mov si, 0
call letterc
mov ax, 4c00h
int 21h
; 子程序:letterc
; 功能:将以 0 结尾的字符串中的小写字母转为大写
; 参数:ds:si = 字符串首地址
; 返回:无
letterc:
push ax ; 保存寄存器状态
push si
l0:
mov al, [si] ; 取当前字符
cmp al, 0
je l2 ; 若为 0,字符串结束,跳转返回
cmp al, 061H ; 比较是否小于 'a'
jb l1 ; 小于 'a',不是小写字母,跳过
cmp al, 07AH
ja l1 ; 大于 'z',也不是小写字母,跳过
and al, 11011111b ; 清零 bit 5,小写转大写
mov [si], al ; 写回原位置
l1:
inc si ; 指向下一个字符
jmp short l0 ; 继续循环
l2:
pop si ; 恢复寄存器状态
pop ax
ret
codesg ends
end begin
程序运行后,字符串中的全部小写字母被成功转换为大写,非字母字符(单引号、空格、连字符、点号)均保持不变。

参考资料
《汇编语言(第四版)》,王爽 编著,清华大学出版社

浙公网安备 33010602011771号