汇编语言: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 无溢出

image

例如对于以下下指令,本质是计算 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 正数减正数,结果为正,无溢出

image

无符号数的比较

对于无符号数,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 位。
image

与之对应,例如如下指令:

mov ah, 8AH     ; -118
mov bh, 70H     ; 112
cmp ah, bh

-118 - 112 = -230 超出 -128~127,溢出后实际结果为 1AH(26),实际结果非负所以 SF = 0。逻辑上 -118 < 112,说明在有溢出的情况下,SF 不能代表逻辑上的正负。
image

因此对于有符号数的正负性,可以通过 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 点:

  1. 通用性:同一个 flag 寄存器既能无符号比较,也能进行有符号比较;
  2. 灵活性:程序员可以根据需要组合这些标志,比如只考虑无符号就忽略 OF;
  3. 历史兼容性:该设计在后来的 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。
image

例如考虑一个统计等于 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 计数,。
image

当需要统计大于 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

image

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

当需要统计小于 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 才计数”。
image

串传送指令

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 自动增加,指向下一个字节。
image

例如考虑一个将 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,超出预留空间造成写入越界。
image

实验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

程序运行后,字符串中的全部小写字母被成功转换为大写,非字母字符(单引号、空格、连字符、点号)均保持不变。
image

参考资料

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

posted @ 2026-08-18 14:46  乌漆WhiteMoon  阅读(37)  评论(0)    收藏  举报