Skip to content

LV011-第11章-标志寄存器

CPU 内部的寄存器中,有一种特殊的寄存器(对于不同的处理机,个数和结构都可能不同)具有以下 3 种作用:

(1)用来存储相关指令的某些执行结果;

(2)用来为 CPU 执行相关指令提供行为依据;

(3)用来控制 CPU 的相关工作方式。

这种特殊的寄存器在 8086CPU 中,被称为 标志寄存器。8086CPU 的标志寄存器有 16 位,其中存储的信息通常被称为 程序状态字(PSW)。我们已经使用过 8086CPU 的 ax、bx、cx、dx、si、di、bp、sp、IP、cs、ss、ds、es 等 13 个寄存器了,本章中的标志寄存器(以下简称 flag)是我们学习的最后一个寄存器。

flag 和其他寄存器不一样,其他寄存器是用来存放数据的,都是整个寄存器具有一个含义。而 flag 寄存器是按位起作用的,也就是说,它的每一位都有专门的含义,记录特定的信息。

8086CPU 的 flag 寄存器的结构如 图 11.1 所示。

image-20251028165235969

flag 的 1、3、5、12、13、14、15 位在 8086CPU 中没有使用,不具有任何含义。而 0、2、4、6、7、8、9、10、11 位都具有特殊的含义。

在这一章中,我们学习标志寄存器中的 CF、PF、ZF、SF、OF、DF 标志位,以及一些与其相关的典型指令。

11.1 ZF 标志

flag 的第 6 位是 ZF,零标志位(Zero Flag)。它记录相关指令执行后,其结果是否为 0。如果结果为 0,那么 zf = 1;如果结果不为 0,那么 zf = 0。

比如,指令:

assembly
mov ax,1
sub ax,1

执行后,结果为 0,则 zf = 1,表示“结果是 0”

assembly
mov ax,2
sub ax,1

执行后,结果不为 0,则 zf = 0,表示“结果非 0”。

对于 zf 的值的理解——它记录的信息是“是否为 0”,在计算机中 1 表示逻辑真,0 表示逻辑假。

注意,在 8086CPU 的指令集中,有的指令的执行是影响标志寄存器 的,比如,add、sub、mul、div、inc、or、and 等,它们大都是 运算指令(进行逻辑或算术运算);有的指令的执行 对标志寄存器没有影响,比如,mov、push、pop 等,它们大都是 传送指令。在使用一条指令的时候,要注意这条指令的全部功能,其中包括,执行结果对标志寄存器的哪些标志位造成影响。

11.2 PF 标志

flag 的第 2 位是 PF,奇偶标志位(Parity Flag)。它记录相关指令执行后,其结果的 所有 bit 位中的 1 的个数是否为偶数。如果 1 的个数为偶数,pf = 1,如果为奇数,那么 pf = 0。

比如,指令:

assembly
mov al,1
add al,10

执行后,结果为 00001011B,其中有 3(奇数)个 1,则 pf = 0;

assembly
mov al,1
or al,2

执行后,结果为 00000111B,其中有 2(偶数)个 1,则 pf = 1;

assembly
sub al,al

执行后,结果为 00000000B,其中有 0(偶数)个 1,则 pf = 1。

11.3 SF 标志

flag 的第 7 位是 SF,符号标志位(Sign Flag)。它记录相关指令执行后,其结果是否为负。如果结果为负,sf = 1;如果非负,sf = 0。

计算机中通常用补码来表示有符号数据。计算机中的一个数据可以看作是有符号数,也可以看成是无符号数。比如:

  • 00000001B, 可以看作为无符号数 1,或看作为有符号数+1;
  • 10000001B, 可以看作为无符号数 129,也可以看作为有符号数-127。

这也是说,对于同一个二进制数据,计算机可以将它当作无符号数据进行运算,也可以当作有符号数据进行运算。比如:

assembly
mov al,10000001B  
add al,1

结果:(al)= 10000010B。

可以将 add 指令进行的运算当作无符号数的运算,那么 add 指令相当于计算 129+1,结果为 130(10000010B); 也可以将 add 指令进行的运算当作有符号数的运算,那么 add 指令相当于计算 -127+1,结果为 -126(10000010B)。

不管我们如何看待,CPU 在执行 add 等指令的时候,就已经包含了两种含义,也将得到同一种信息来记录的两种结果。关键在于我们的程序需要哪一种结果。

SF 标志,就是 CPU 对有符号数运算结果的一种记录,它记录数据的正负。在我们将数据当作有符号数来运算的时候,可以通过它来得知结果的正负。如果我们将数据当作无符号数来运算,SF 的值则没有意义,虽然相关的指令影响了它的值。

这也是说,CPU 在执行 add 等指令时,必然是要影响 SF 标志位的值的。至于我们需要不需要这种影响,那就看我们如何看待指令所进行的运算了。

比如:

assembly
mov al,10000001B  
add al,1

执行后,结果为 10000010B,sf = 1,表示:如果指令进行的是有符号数运算,那么结果为负;

assembly
mov al,10000001B  
add al,01111111B

执行后,结果为 0,SF = 0,表示:如果指令进行的是有符号数运算,那么结果为非负。

某些指令将影响标志寄存器中的多个标记位,这些被影响的标记位比较全面地记录了指令的执行结果,为相关的处理提供了所需的依据。比如指令 sub al, al 执行后,ZF、PF、SF 等标记位都要受到影响,它们分别为:1、1、0。

11.a 检测点 11.1

写出下面每条指令执行后,ZF、PF、SF 等标记位的值。

 sub  al, al  ZF =  PF =  SF =  mov  al, 1  ZF =  PF =  SF =  push ax  ZF =  PF =  SF =  pop bx  ZF =  PF =  SF =  add al, bl  ZF =  PF =  SF =  add al, 10  ZF =  PF =  SF =  mul al  ZF =  PF =  SF = 

解析:

  • sub al, al 的值为 0,al = 0,所以 ZF = 1,PF = 1,SF = 0
  • 传送指令 mov、push、pop 执行后,标志寄存器值不变, ZF = 1,PF = 1,SF = 0
  • 但是执行完上面三条传送指令后,al = 1,bx = ax→bl = 1(ah 不知道其实没关系)
  • add al, bl 的值为 2,al = 2,0000 0010B,所以 ZF = 0,PF = 0,SF = 0
  • add al,10 的值为 12,al = 12,0000 1100B,所以 ZF = 0,PF = 1,SF = 0
  • mul al 的值为 al * al = 12 * 12 = 144,al = 144,1001 0000B,所以 ZF = 0,PF = 1,SF = 0

11.4 CF 标志

flag 的第 0 位是 CF,进位标志位(Carry Flag)。一般情况下,在进行无符号数运算的时候,它记录了运算结果的最高有效位移更高位的进位值,或从更高位的借位值。

对于位数为 N 的无符号数来说,其对应的二进制信息的最高位,即第 N-1 位,就是它的最高有效位,而假想存在的第 N 位,就是相对于最高有效位的更高位,如下图所示。

image-20251028165413353

我们知道,当两个数据相加的时候,有可能产生从最高有效位移更高位的进位。比如,两个 8 位数据:98H+98H,将产生进位。由于这个进位值在 8 位数中无法保存,我们在前面的课程中,就只是简单地说明这个进位值丢失了。其实 CPU 在运算的时候,并不丢弃这个进位值,而是记录在一个特殊的寄存器的某一位上。8086CPU 就用 flag 的 CF 位来记录这个进位值。比如,下面的指令:

assembly
mov al,98H
add al,al ;执行后:(al)=30H, CF=1,CF 记录了从最高有效位移更高位的进位值
add al,al ;执行后:(al)=60H, CF=0, CF 记录了从最高有效位向更高位的进位值

而当两个数据做减法的时候,有可能向更高位借位。比如,两个 8 位数:97H-98H,将产生借位,借位后,相当于计算 197H-98H。而 flag 的 CF 也可以用来记录这个借位值。比如,下面的指令:

assembly
mov al,97H 
sub al,98H ;执行后:(al)=FFH, CF=1, CF 记录了向更高位的借位值
sub al,al  ;执行后:(al)=0, CF=0, CF 记录了向更高位的借位值

11.5 OF 标志

在进行有符号数运算的时候,如果结果超过了机器所能表示的范围称为 溢出

那么,什么是机器所能表示的范围呢? 比如说,指令运算的结果用 8 位寄存器或内存单元来存放,比如,add al,3,那么对于 8 位的有符号数据,机器所能表示的范围就是 -128~127。同理,对于 16 位有符号数据,机器所能表示的范围是 -32768~32767。 如果运算结果超出了机器所能表达的范围,将产生溢出。 注意,这里所讲的 溢出,只是对有符号数运算而言。下面我们看两个溢出的例子。

assembly
mov al,98
add al,99

执行后将产生溢出。因为 add al,99 进行的有符号数运算是:

(al)=(al)+99 = 98+99 = 197。

而结果 197 超过了机器所能表示的 8 位有符号数的范围:-128~127。

assembly
mov al,0FOH ;FOH,为有符号数 -16 的补码
add al,088H ;88H,为有符号数 -120 的补码

执行后,将产生溢出。因为 add al,088H 进行的有符号数运算是:

(al)=(al)+(-120)+(-16)+(-120)=-136

而结果 -136 超过了机器所能表示的 8 位有符号数的范围:-128~127。

如果在进行有符号数运算时发生溢出,那么运算的结果将不正确。就上面的两个例子来说:

assembly
mov al,98
add al,99

add 指令运算的结果是(al)= 0C5H,因为进行的是有符号数运算,所以 al 中存储的是有符号数,而 C5H 是有符号数 -59 的补码。如果我们用 add 指令进行的是有符号数运算,则 98+99 =-59 这样的结果让人无法接受。造成这种情况的原因,就是实际的结果 197,作为一个有符号数,在 8 位寄存器 al 中存放不下。

同样,对于:

assembly
mov al,0F0H ;FOH,为有符号数 -16 的补码
add al,088H ;88H,为有符号数 -120 的补码

add 指令运算的结果是(al)= 78H,因为进行的是有符号数运算,所以 al 中存储的是有符号数,而 78H 表示有符号数 120。如果我们用 add 指令进行的是有符号数运算,则 -16-120 = 120 这样显然不正确。造成这种情况的原因,就是实际的结果 -136,作为一个有符号数,在 8 位寄存器 al 中存放不下。

由于在进行有符号数运算时,可能发生溢出而造成结果的错误。则 CPU 需要对指令执行后是否产生溢出进行记录。 flag 的第 11 位是 OF,溢出标志位(Overflow Flag)。一般情况下,OF 记录了有符号数运算的结果是否发生了溢出。如果发生溢出,OF = 1;如果没有,OF = 0。

一定要注意 CF 和 OF 的区别:CF 是对无符号数运算有意义的标志位,而 OF 是对有符号数运算有意义的标志位。 比如:

assembly
mov al,98    ;01100010B
add al,99    ;01100011B

add 指令执行后:CF = 0,OF = 1。前面我们讲过,CPU 在执行 add 等指令的时候,就包含了两种含义:无符号数运算和有符号数运算。对于无符号数运算,CPU 用 CF 位来记录是否产生了进位;对于有符号数运算,CPU 用 OF 位来记录是否产生了溢出,当然,还要用 SF 位来记录结果的符号。对于无符号数运算,98+99 没有进位,CF = 0;对于有符号数运算,98+99 发生溢出,OF = 1。

assembly
mov al,0F0H ;11110000B,有符号数代表-0010000B=-16
add al,88H  ;10001000B,有符号数代表-1111000B=-120

add 指令执行后:CF = 1,OF = 1。对于无符号数运算,0F0H+88H 有进位,CF = 1;对于有符号数运算,0F0H+88H 发生溢出,OF = 1。

assembly
mov al,0F0H ;11110000B,有符号数代表-0010000B=-16
add al,78H  ;01111000B,有符号数代表+1111000B=120

add 指令执行后:CF = 1,OF = 0。对于无符号数运算,0F0H+78H 有进位,CF = 1;对于有符号数运算,0F0H+78H 不发生溢出,OF = 0。

我们可以看出,CF 和 OF 所表示的进位和溢出,是分别对无符号数和有符号数而言的,它们之间没有任何关系

11.b 检测点 11.2

写出下面每条指令执行后,ZF、PF、SF、CF、OF 等标志位的值。

指令CFOFSFZFPF计算
sub al, al00011al = 0
mov al, 10H00011al = 10H
add al, 90H00101al = A0H
UNS: 10H+90H = AH = 160
S: 16-112 =-96
mov al, 80H00101al = 80H
add al, 80H1101(计算结果是 0 而非 -256)1al = 00H
UNS: 80H+80H = 100H = 256
S:-128-128 =-256
mov al, 0FCH11011al = 0FCH(1111 1100B)
add al, 05H10000al = 01H
UNS: 0FCH+05H = 101H
S:-4+5 = 1
mov al, 7DH10000al = 7DH(0111 1101B)
add al, 0BH011(高位为 1 可以看成负数)01al = 88H
UNS: 0BH+7DH = 88H
S: 125+11 = 137

11.6 adc 指令

adc 是 带进位加法指令,它利用了 CF 位上记录的进位值。

指令格式:adc 操作对象 1,操作对象 2

功能:操作对象 1 = 操作对象 1 + 操作对象 2 + CF

比如指令 adc ax, bx 实现的功能是:(ax)=(ax)+(bx)+CF

例:

assembly
mov ax,2  
mov bx,1  
sub bx,ax  
adc ax,1

执行后,(ax)= 4。adc 执行时,相当于计算:(ax)+1+CF = 2+1+1 = 4。

assembly
mov ax,1  
add ax,ax  
adc ax,3

执行后,(ax)= 5。adc 执行时,相当于计算:(ax)+3+CF = 2+3+0 = 5。

assembly
mov al,98H  
add al,al  
adc al,3

执行后,(al)= 34H。adc 执行时,相当于计算:(al)+3+CF = 30H+3+1 = 34H。

可以看出,adc 指令比 add 指令多加了一个 CF 位的值。

为什么要加上 CF 的值呢?CPU 为什么要提供这样一条指令呢?

先来看一下 CF 的值的含义。在执行 adc 指令的时候加上去的 CF 的值的含义,是由 adc 指令前面的指令决定的,也就是说,关键在于所加上的 CF 值是被什么指令设置的。显然,如果 CF 的值是被 sub 指令设置的,那么它的含义就是借位值;如果是被 add 指令设置的,那么它的含义就是进位值。我们来看一下两个数据:0198H 和 0183H 如何相加的:

0198+01183031B

可以看出,加法可以分两步来进行:① 低位相加;② 高位相加再加上低位相加产生的进位值。

下面的指令和 add ax, bx 具有相同的结果:

assembly
add al,bl
adc ah,bh

看来 CPU 提供 adc 指令的目的,就是来进行加法的第二步运算的。adc 指令和 add 指令相配合就可以对更大的数据进行加法运算。我们来看一个例子:

【编程】计算 1EF000H + 201000H,结果放在 ax(高 16 位)和 bx (低 16 位)中。 因为两个数据的位数都大于 16,用 add 指令无法进行计算。我们将计算分两步进行,先将低 16 位相加,然后将高 16 位和进位值相加。程序如下。

assembly
mov ax,001EH
mov bx,0F000H
add bx,1000H
adc ax,0020H

adc 指令执行后,也可能会产生进位值,所以也会对 CF 位进行设置。由于有这样的功能,我们就可以对任意大的数据进行加法运算。看一个例子: 【编程】计算 1EF0001000H + 2010001EFOH,结果放在 ax (最高 16 位),bx (次高 16 位),cx(低 16 位)中。

计算分 3 步进行:

(1) 先将低 16 位相加,完成后,CF 中记录本次相加的进位值;

(2) 再将次高 16 位和 CF(来自低 16 位的进位值)相加,完成后,CF 中记录本次相加的进位值;

(3) 最后高 16 位和 CF(来自次高 16 位的进位值)相加,完成后,CF 中记录本次相加的进位值。

程序如下。

assembly
mov ax,001EH
mov bx,0F000H
mov cx,1000H
add cx,1EFOH
adc bx,1000H
adc ax,0020H

下面编一个子程序,对两个 128 位数据进行相加。

名称:add128 功能:两个 128 位数据相加。 参数:ds: si 指向存储第一个数的内存空间,因数据为 128 位,所以需要 8 个字单元,由低地址单元到高地址单元依次存放 128 位数据由低到高的各个字。运算结果存储在第一个数的存储空间中。

ds: di 指向存储第二个数的内存空间。

程序如下:

assembly
add128: 
    push ax
    push cx
    push si
    push di
    
    sub ax,ax ;将 CF 设置为 0
    
    mov cx,8
s:
    mov ax,[si]
    adc ax,[di]
    mov [si],ax
    inc si
    inc si
    inc di
    inc di
    loop s
    
    pop di
    pop si
    pop cx
    pop ax
ret

inc 和 loop 指令不影响 CF 位,思考一下,上面的程序中,能不能将 4 个 inc 指令,用

assembly
add si,2
add di,2

来取代?答案是不能,ADD 会改变 CF 的值从而影响结果。

11.7 sbb 指令

sbb 是 带借位减法指令,它利用了 CF 位上记录的借位值。

指令格式:sbb 操作对象 1,操作对象 2

功能:操作对象 1 = 操作对象 1-操作对象 2-CF

比如指令 sbb ax, bx 实现的功能是:(ax)=(ax)-(bx)-CF

sbb 指令执行后,将对 CF 进行设置。利用 sbb 指令可以对任意大的数据进行减法运算。比如,计算 003E1000H-00202000H,结果放在 ax, bx 中,程序如下:

assembly
mov bx,1000H
mov ax,03EH
sub bx,2000H
sbb ax,0020H

sbb 和 adc 是基于同样的思想设计的两条指令,在应用思路上和 adc 类似。在这里,我们就不再进行过多的讨论。通过学习这两条指令,我们可以进一步领会一下标志寄存器 CF 位的作用和意义。

11.8 cmp 指令

cmp 是 比较指令(compare)cmp 的功能相当于减法指令,只是不保存结果。cmp 指令执行后,将对标志寄存器产生影响。其他相关指令通过识别这些被影响的标志寄存器位来得知比较结果。

cmp 指令格式:cmp 操作对象 1,操作对象 2

功能:计算操作对象 1-操作对象 2 但并不保存结果,仅仅根据计算结果对标志寄存器进行设置。

比如,指令 cmp ax, ax,做(ax)-(ax)的运算,结果为 0,但并不在 ax 中保存,仅影响 flag 的相关各位。指令执行后:zf = 1,pf = 1,sf = 0,cf = 0,of = 0。

下面的指令:

assembly
mov ax,8  
mov bx,3  
cmp ax,bx

执行后:(ax)= 8,zf = 0,pf = 1,sf = 0,cf = 0,of = 0。

其实,我们通过 cmp 指令执行后,相关标志位的值就可以看出比较的结果。

assembly
cmp ax,bx

如果(ax)=(bx) 则(ax)-(bx)= 0,所以:zf = 1;

如果(ax)≠(bx) 则(ax)-(bx)≠0,所以:zf = 0;

如果(ax)<(bx) 则(ax)-(bx)将产生借位,所以:cf = 1;

如果(ax)≥(bx) 则(ax)-(bx)不必借位,所以:cf = 0;

如果(ax)>(bx) 则(ax)-(bx)既不必借位,结果又不为 0,所以:cf = 0 并且 zf = 0;

如果(ax)≤(bx) 则(ax)-(bx)既可能借位,结果可能为 0,所以:cf = 1 或 zf = 1。

现在我们可以看出比较指令的设计思路,即:通过做减法运算,影响标志寄存器,标志寄存器的相关位置记录了比较的结果。反过来看上面的例子。

指令 cmp ax, bx 的逻辑含义是比较 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,说明(ax)≤ (bx)

同 add、sub 指令一样,CPU 在执行 cmp 指令的时候,也包含两种含义:进行 无符号数运算 和进行 有符号数运算。所以利用 cmp 指令可以对无符号数进行比较,也可以对有符号数进行比较。上面所讲的是用 cmp 进行无符号数比较时,相关标志位对比较结果的记录。下面我们再来看一一下如果用 cmp 来进行有符号数比较时,CPU 用哪些标志位对比较结果进行记录。我们以 cmp ah, bh 为例进行说明。

assembly
cmp ah,bh

如果(ah)=(bh)则(ah)-(bh)= 0,所以:zf = 1;

如果(ah)≠(bh)则(ah)-(bh)≠0,所以:zf = 0;

所以,根据 cmp 指令执行后 zf 的值,就可以知道两个数据是否相等。

我们继续看,如果(ah)<(bh)则可能会发生什么情况呢?

对于有符号数运算,在(ah)<(bh)情况下,(ah)-(bh)显然可能引起 sf = 1,即结果为负。比如:

(ah)= 1,(bh)= 2;则(ah)-(bh)= 0FFH,0FFH 为 -1 的补码,因为结果为负,所以 sf = 1。

(ah)= 0FEH,(bh)= 0FFH;则(ah)-(bh)=-2-(-1)= 0FFH,因为结果为负,所以 sf = 1。

通过上面的例子,我们是不是可以得到这样的结论:“cmp 操作对象 1 操作对象 2”指令执行后,sf = 1,就说明操作对象 1 < 操作对象 2?——当然不是。

我们再看两个例子。

(ah)= 22H,(bh)= 0A0H;则(aH)-(bH)= 34-(-96)= 82H,82H 是 -126 的补码所以 sf = 1

这里虽然 sf = 1,但是并不能说明(aH)<(bH)因为显然 34>-96。

两个有符号数 A 和 B 相减,得到的负数,那么可以肯定 A < B,这个思路没有错,关键在于我们根据什么来判断得到的是一个负数。CPU 将 cmp 指令得到的结果记录在 flag 的相关标志位中。我们可以根据指令执行后,相关标志位的值来判断比较的结果。单纯地考查 sf 的值不可能知道结果的正负。因为 sf 记录的只是可以在计算机中存放的相应位数的正负结果。比如 add ah, al 执行后,sf 记录的是 ah 中的 8 位二进制信息所表示的数据的正负。cmp ah, bh 执行后,sf 记录的是(aH)-(bH)所得到的 8 位结果数据的正负,虽然这个结果没有在我们能够使用的寄存器或内存单元中保存,但是在指令执行的过程中,它暂存在 CPU 内部的暂存器中。

所得到的 相应结果的正负,并不能说明,运算所应该得到的结果的正负。这是因为 运算的过程中可能发生溢出。如果有这样的情况发生,那么,sf 的值就不能说明任何问题。比如:

assembly
mov ah,22H  
mov bh,0A0H  
sub ah,bh

结果 sf = 1,运算实际得到的结果是(aH)= 82H,但是在逻辑上,运算所应该得到的结果是:34-(-96)= 130。就是因为 130 这个结果作为一个有符号数超出了 -128~127 这个范围,在 ah 中不能表示,而 ah 中的结果被 CPU 当作有符号数解释为 -126。而 sf 被用来记录这个实际结果的正负,所以 sf = 1。但 sf = 1 不能说明在逻辑上,运算所得的正确结果的正负。

又比如:

assembly
mov ah,08AH  
mov bh,070H  
cmp ah,bh

结果 sf = 0,运算实际得到的结果是 1AH,但是在逻辑上,运算所应该得到的结果是:(-118)-112 =-230。sf 记录实际结果的正负,所以 sf = 0。但 sf = 0 不能说明在逻辑上,运算所得的正确结果。

但是逻辑上的结果的正负,才是 cmp 指令所求的真正结果,因为我们就是要靠它得到两个操作对象的比较信息。所以 cmp 指令所作的比较结果,不是仅仅靠 sf 就能记录的,因为它只能记录实际结果的正负。

我们考虑一下,两种结果之间的关系,实际结果的正负,和逻辑上真正结果的正负,它们之间有多大的距离呢?从上面的分析中,我们知道,实际结果的正负,之所以不能说明逻辑上真正结果的正负,关键的原因在于发生了溢出。如果 没有溢出发生的话,那么,实际结果的正负和逻辑上真正结果的正负就一致 了。

所以,我们应该在考查 sf(得知实际结果的正负)的同时考查 of(得知有没有溢出),就可以得知逻辑上真正结果的正负,同时就可以知道比较的结果。

下面,我们以 cmp ah, bh 为例,总结一下 CPU 执行 cmp 指令后,sf 和 of 的值是如何来说明比较的结果的。

(1) 如果 sf = 1,而 of = 0

of = 0,说明没有溢出,逻辑上真正结果的正负 = 实际结果的正负;

因 sf = 1,实际结果为负,所以逻辑上真正的结果为负,所以 (ah)<(bh)。

(2) 如果 sf = 1,而 of = 1 of = 1,说明有溢出,逻辑上真正结果的正负 ≠ 实际结果的正负;

因 sf = 1,实际结果为负。

实际结果为负,而又有溢出,这说明了由于溢出导致了实际结果为负,简单分析一下,就可以看出,如果因为溢出导致了实际结果为负,那么逻辑上真正的结果必然为正

这样,sf = 1,of = 1,说明了 (ah)>(bh)。

(3) 如果 sf = 0,而 of = 1

of = 1,说明有溢出,逻辑上真正结果的正负 ≠ 实际结果的正负;

因 sf = 0,实际结果非负。而 of = 1 说明有溢出,则结果非 0,所以,实际结果为正。 实际结果为正,而又有溢出,这说明了由于溢出导致了实际结果非负,简单分析一下,就可以看出,如果因为溢出导致了实际结果为正,那么逻辑上真正的结果必然为负

这样,sf = 0,of = 1,说明了 (ah)<(bh)。

(4) 如果 sf = 0,而 of = 0

of = 0,说明没有溢出,逻辑上真正结果的正负 = 实际结果的正负;

因 sf = 0,实际结果非负,所以逻辑上真正的结果非负,所以 (ah)≥(bh)。

上面,我们深入讨论了 cmp 指令在进行有符号数和无符号数比较时,对 flag 相关标志位的影响,和 CPU 如何通过相关的标志位来表示比较的结果。在学习中,要注意领会 8086CPU 这种工作机制的设计思想。实际上,这种设计思想对于各种处理器来说是普遍的。

下面的内容中我们将学习一些根据 cmp 指令的比较结果(即 cmp 指令执行后,相关标志位的值)进行工作的指令。

11.9 检测比较结果的条转移指令

“转移”指的是它能够修改 IP,而“条件”指的是它可以根据某种条件,决定是否修改 IP。

比如,jcxz 就是一个条件转移指令,它可以检测 cx 中的数值,如果(cx)= 0,就修改 IP,否则什么也不做。所有有条件转移指令的转移位都足[-128,127]。

除了 jcwx 之外,CPU 还提供了其他条件转移指令,大多数条件转移指令都检测标志寄存器的相关标志位,根据检测的结果来决定是否修改 IP。它们检测的是哪些标志位呢?就是被 cmp 指令影响的那些,表示比较结果的标志位。这些条件转移指令通常都和 cmp 相配合使用,就好像 call 和 ret 指令通常相配合使用一样。

因为 cmp 指令可以同时进行两种比较,无符号数比较和有符号数比较,所以根据 cmp 指令的比较结果进行转移的指令也分为两种,即 根据无符号数的比较结果进行转移的条件转移指令(它们检测 zf、cf 的值)和 根据有符号数的比较结果进行转移的条件转移指令(它们检测 sf、of 和 zf 的值)。

面是常用的根据无符号数的比较结果进行比较的条件转移指令。

指令含义检测的相关标志位指令英文含义
je等于则转移zf = 1jump if equal: =
jne不等于则转移zf = 0jump if not equal: ≠
jb低于则转移cf = 1jump below: <
jnb不低于则转移cf = 0jump not below: >=
ja高于则转移cf = 0 且 zf = 0jump if above: >
jna不高于则转移cf = 1 或 zf = 1jump if not above: <=

注意观察一下它们所检测的标志位,都是 cmp 指令进行无符号数比较的时候记录比较结果的标志位。比如 je,检测 zf 位,当 zf = 1 的时候进行转移,如果在 je 前面使用了 cmp 指令,那么 je 对 zf 的检测,实际上就是间接地检测 cmp 的比较结果是否为两数相等。下面看一个例子。

编程实现如下功能:

如果 (ah)=(bh) 则 (ah)=(ah)+(ah),否则 (ah)=(ah)+(bh)。

assembly
    cmp ah,bh
    je s
    add ah,bh
    jmp short ok
s:
    add ah,ah
ok:
    ... ...

上面的程序执行时,如果(ah)=(bh),则 cmp ah, bh 使 zf = 1,而 je 检测 zf 是否为 1,如果为 1,将转移至标号 s 处执行指令 add ah, ah。这也可以说,cmp 比较 ah, bh 后所得到的相等的结果使得 je 指令进行转移。从而很好地体现了 je 指令的逻辑含义,相等则转移。

虽然 je 的逻辑含义是“相等则转移”,但它进行的操作是 zf = 1 时则转移。“相等则转移”这种逻辑含义,是通过 cmp 指令配合使用来体现的,因为是 cmp 指令为 “zf = 1”赋予了“两数相等”的含义。

至于究竟在 je 之前使不使用 cmp 指令,在于我们的安排。je 检测的是 zf 位置,不管 je 前面是什么指令,只要 CPU 执行 je 指令时,zf = 1,那么就会发生转移,比如:

assembly
    mov ax,0  
    add ax,0  
    je s  
    inc ax  
s: 
    inc ax

执行后,(ax)= 1。add ax,0 使得 zf = 1,所以 je 指令将进行转移。可在这个时候发生的转移的确不带“相等则转移”的含义。因为此时的 je 指检测到的 zf = 1,不是由 cmp 等比较指令设置的,而是由 add 指令设置的,并不具有“两数相等”的含义。但无论“zf = 1”的含义如何,是什么指令设置的,只要是 zf = 1,就可以使得 je 指令发生转移

CPU 提供了 cmp 指令,也提供了 je 等条件转移指令,如果将它们配合使用,可以实现根据比较结果进行转移的功能。但这只是“如果”,只是一种合理的建议,事实上常用的方法。但究竞是否配合使用它们,完全是自己的事情。这就好像 call 和 ret 指令的关系一样。

对于 jne、jb、jnb、ja、jna 等指令和 cmp 指令配合使用的思想和 je 相同,可以自己分析一下。

虽然我们分别讨论了 cmp 指令和与其比较结果相关的有条件转移指令,但是它们经常在一起配合使用。所以我们联合应用它们的时候,不必再考虑 cmp 指令对相关标志位的影响和 je 等指令对相关标志位的检测。因为相关的标志位,只是为了 cmp 和 je 等指令传递比较结果。我们可以直接考虑 cmp 和 je 等指令配合使用时,表现出来的逻辑含义。它们在联合使用的时候表现出来的功能有些像高级语言中的 IF 语句。

我们来看下面的一组程序。

data 段中的 8 个字节如下:

assembly
data segment
    db 8,11,8,1,8,5,63,38
data ends

【编程一】统计 data 段中数值为 8 的字节的个数,用 ax 保存统计结果。

编程思路:初始设置(ax)= 0,然后用循环依次比较每个字节值,找到一个和 8 相等的数就将 ax 的值加 1。程序如下。

assembly
    mov ax,data
    mov ds,ax
    mov bx,0            ;ds:bx 指向第一个字节
    mov ax,0            ;初始化累加器
    mov cx,8
s: 
    cmp byte ptr [bx],8 ;和 8 进行比较
    jne next            ;如果不相等转到 next,继续循环
    inc ax              ;如果相等就将计数值加 1
next: 
    inc bx
    loop s              ;程序执行后:(ax)=3

这个程序也可以写成这样:

assembly
    mov ax,data
    mov ds,ax
    mov bx,0            ;ds:bx 指向第一个字节
    mov ax,0            ;初始化累加器
    mov cx,8
s: 
    cmp byte ptr [bx],8 ;和 8 进行比较
    je ok               ;如果相等转到 ok
    jmp short next      ;如果不相等转到 next,继续循环
ok: 
    inc ax              ;如果相等就将计数值加 1
next:
    inc bx
    loop s

比起第一个程序,它直接地遵循了“等于 8 则计数值加 1”的原则,用 je 指令检测等于 8 的情况,但是没有第一个程序精简。第一个程序用 jne 检测不等于 8 的情况,从而间接地检测等于 8 的情况。要注意在使用 cmp 和条件转移指令时的这种编程思想。

【编程二】统计 data 段中数值大于 8 的字节的个数,用 ax 保存统计结果。

编程思路:初始设置(ax)= 0,然后用循环依次比较每个字节值,找到一个大于 8 的就将 ax 的值加 1。程序如下。

assembly
    mov ax,data
    mov ds,ax
    mov ax,0            ;初始化累加器
    mov bx,0            ;ds:bx 指向第一个字节
    mov cx,8
s: 
    cmp byte ptr [bx],8 ;和 8 进行比较
    jna next            ;如果不大于 8 转到 next,继续循环
    inc ax              ;如果大于 8 就将计数值加 1
next: 
    inc bx
    loop s

程序执行后:(ax)= 3

【编程三】统计 data 段中数值小于 8 的字节的个数,用 ax 保存统计结果。

编程思路:初始设置(ax)= 0,然后用循环依次比较每个字节的值,找到一个小于 8 的就将 ax 的值加 1。程序如下。

assembly
    mov ax,data
    mov ds,ax
    mov ax,0            ;初始化累加器
    mov bx,0            ;ds:bx 指向第一个字节
    mov cx,8
s: 
    cmp byte ptr [bx],8 ;和 8 进行比较
    jnb next            ;如果不小于 8 转到 next,继续循环
    inc ax              ;如果小于 8 就将计数值加 1
next: 
    inc bx
    loop s

程序执行后:(ax)= 2

上面讲解了根据无符号数的比较结果进行转移的条件转移指令。根据有符号数的比较结果进行转移的条件转移指令的工作原理和无符号的相同,只是检测了不同的标志位。我们在这里主要探讨的是 cmp、标志寄存器的相关位、条件转移指令三者配合应用的原则,这个原则具有普遍性,而不是逐条讲解条件转移指令。对这些指令感兴趣的读者可以查看相关的指令手册。

11.c 检测点 11.3

(1) 补全下面的程序,统计 F000:0 处 32 个字节中,大小在[32,128]的数据的个数。

assembly
    mov ax,0f000h
    mov ds,ax
    
    mov bx,0
    mov dx,0
    mov cx,32
s:
    mov al,[bx]
    cmp al,32
    /*________*/
    cmp al,128
    /*________*/
    inc dx
s0:
    inc bx
    loop s

解析:

先判断,al <32 则跳转到 s0,再判断 al> 128,则跳转到 s0,所以这两个判断都不跳转的情形就是 al∈[32,128],此时 bx+1。

assembly
jb s0
ja s0

(2) 补全下面的程序,统计 F000:0 处 32 个字节中,大小在 (32,128) 的数据的个数。

assembly
    mov ax,0f000h
    mov ds,ax
    mov bx,0
    mov dx,0
    mov cx,32
s:
    mov al,[bx]
    cmp al,32
    /*________*/
    cmp al,128
    /*________*/
    inc dx
s0:
    inc bx
    loop s

解析:

先判断,al <=32 则跳转到 s0,再判断 al> = 128,则跳转到 s0,所以这两个判断都不跳转的情形就是 al∈(32,128),此时 bx+1。

assembly
jna s0
jnb s0

11.10 DF 标志和串传送指令

flag 的第 10 位是 DF,方向标志位(Direction Flag)。在串处理指令中,控制每次操作后 si、di 的增减。

  • df = 0 每次操作后 si、di 递增;
  • df = 1 每次操作后 si、di 递减。

我们来看下面的一个串传送指令(move string byte)。

格式:movsb

功能:执行 movsb 指令相当于进行下面几步操作。

(1)

assembly
((es)*16+(di))=((ds)*16+(si))

(2)

assembly
如果 df = 0 则:(si)=(si)+1  
              (di)=(di)+1
如果 df = 1 则:(si)=(si)-1
              (di)=(di)-1

用汇编语法描述 movsb 的功能如下。

assembly
mov es:[di],byte ptr ds:[si] ;8086 并不支持这样的指令,这里只是个描述
如果 df=0:
inc si
inc di
如果 df=1:
dec si
dec di

可以看出,movsb 的功能是将 ds: si 指向的内存单元中的字节送入 es: di 中,然后根据标志寄存器 df 位的值,将 si 和 di 递增或递减。

当然,也可以传送一个字(move string word),指令如下。

格式:movsw

movsw 的功能是将 ds: si 指向的内存单元中的字送入 es: di 中,然后根据标志寄存器 df 位的值,将 si 和 di 递增 2 或递减 2。

用汇编语法描述 movsw 的功能如下。

assembly
mov es:[di],word ptr ds:[si] ;8086 并不支持这样的指令,这里只是个描述
如果 df=0:
add si,2
add di,2
如果 df=1:
sub si,2
sub di,2

movsb 和 movsw 进行的是串传送操作中的一个步骤,一般来说,movsb 和 movsw 都和 rep(Repetition) 配合使用,格式如下:

assembly
rep movsb

用汇编语法来描述 rep movsb 的功能就是:

assembly
s:
    movsb
    loop s

可见,rep 的作用是根据 cx 的值,重复执行后面的串传送指令。由于每执行一次 movsb 指令 si 和 di 都会递增或递减指向后一个单元或前一个单元,则 rep movsb 就可以循环实现(cx)个字符的传送。

同理,也可以使用这样的指令: rep movsw。

相当于:

assembly
s:
    movsw
    loop s

由于 flag 的 df 位决定着串传送指令执行后,si 和 di 改变的方向,所以 CPU 应该提供相应的指令来对 df 位进行设置,从而使程序员能够决定传送的方向。

8086CPU 提供下面两条指令对 df 位进行设置。

  • cld 指令:将标志寄存器的 df 位置 0 (Clear Direction Flag)
  • std 指令:将标志寄存器的 df 位置 1 (Set Direction Flag)

我们来看下面的两个程序。

【编程问题一】用串传送指令,将 data 段中的第一个字符串复制到它后面的空间中。

assembly
data segment  
    db 'Welcome to masm!'
    db 16 dup (0)  
data ends

我们分析一下,使用串传送指令进行数据的传送,需要给它提供一些必要的信息,它们是:

  • ① 传送的原始位置: ds: si
  • ② 传送的目的位置: es: di
  • ③ 传送的长度: cx
  • ④ 传送的方向: df.

在这个问题中,这些信息如下。

  • ① 传送的原始位置: data: 0;
  • ② 传送的目的位置: data: 0010;
  • ③ 传送的长度: 16;
  • ④ 传送的方向: 因为正向传送(每次串传送指令执行后,si 和 di 递增)比较方便,所以设置 df = 0.

好了,明确了这些信息之后,我们来编写程序:

assembly
mov ax,data
mov ds,ax
mov si,0    ;ds:si 指向 data:0
mov es,ax
mov di,16   ;es:di 指向 data:0010
mov cx,16   ;(cx)=16, rep 循环 16 次
cld         ;设置 df=0,正向传送
rep movsb

【编程问题二】用串传送指令,将 F000H 段中的最后 16 个字符复制到 data 段中。

assembly
data segment
    db 16 dup (0)
data ends

我们还是先来看一下应该为串传送指令提供什么样的信息。

要传送的字符串位于 F000H 段的最后 16 个单元中,那么它的最后一个字符的位置:F000: FFFF,是显而易见的。可以将 ds: si 指向 F000H 段的最后一个单元,将 es: di 指向 data 段中的最后一个单元,然后逆向(即从高地址向低地址)传送 16 个字节即可。

  • ① 传送的原始位置:F000: FFFF;
  • ② 传送的目的位置:data: 000F;
  • ③ 传送的长度:16;
  • ④ 传送的方向:因为逆向传送(每次串传送指令执行后,si 和 di 递减)比较方便,所以设置 df = 1。

程序如下。

assembly
mov ax,0f000h
mov ds,ax
mov si,0ffffh   ;ds:si 指向 f000:ffff
mov ax,data
mov es,ax
mov di,15       ;es:di 指向 data:000F
mov cx,16       ;(cx)=16,rep 循环 16 次
std             ;设置 df=1,逆向传送
rep movsb

11.11 pushf 和 popf

pushf 的功能是 将标志寄存器的值压栈,而 popf 是 从栈中弹出数据,送入标志寄存器中

pushf 和 popf,为 直接访问标志寄存器 提供了一种方法。

11.d 检测点 11.4

下面的程序执行后:(ax)=?

assembly
mov ax,0
push ax
popf
mov ax,0fff0h
add ax,0010h
pushf
pop ax
and al,11000101B
and ah,00001000B

解析:

1514131211109876543210
OFDFIFTFSFZFAFPFCF
  • 1~3 步就是将 ax = 0,标志寄存器设为全零。

  • ax = 0fff0→add ax,0010h→ax = 0000h→OF = 0、ZF = 1、PF = 1、CF = 1、SF = 0(对于 OF 的值,有符号数要单独算一下:0FFF0H =-16,0010H = 16,-16+16 在[-65536,65535]之间,不溢出 OF = 0)

  • ax = 0000 0000 0100 0101B

  • and al,11000101B→ax = 00000000 01000101B

  • and ah,00001000B→ax = 00000000 01000101B→ax = 45H

11.12 标志寄存器在 Debug 中的表示

在 Debug 中,标志寄存器是按照有意义的各个标志位单独表示的。在 Debug 中,我们可以看到下面的信息。

assembly
AX=0000 BX=0000 CX=0000 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000 
DS=**** ES=**** SS=**** CS=**** IP=0100 NV UP EI PL NZ NA PO NC 
                                        ↑  ↑     ↑  ↑     ↑  ↑ 
                                        OF DF    SF ZF    PF CF

下面列出 Debug 对我们已知的标志位的表示。

标志位置值为 1 的标记值为 0 的标记助记
cf0CYNCCarry Yes / Not Carry
pf2PEPOParity Even / Parity Odd
af 4ACNAAuxiliary Carry / No Auxiliary Carry
zf6ZRNZZero / Not Zero
sf7NGPLNegative / Positive
if 9EIDIEnable Interrupts / Disable Interrupts
df10DNUPDown / Up
of11OVNVOverflow / Not Overflow
  • ”是补充内容,补充了 中断允许标志 Interrupt Enable Flag(IF),及 辅助进位标志 Auxiliary Carry Flag(AF)
  • 关联助记:进位从低位开始(CF: 0),奇 标志(PF: 2),辅助一般是四号位(AF: 4),您(zero flag)是真的 6(ZF: 6),服(sign flag 符号寄存器)气(SF: 7),数到 9 差不多就要中断了(IF: 9),方向紧跟实事(DF: 10),溢出在最高位(OF: 11)
  • 口诀:低进奇偶辅四位,您六服气中断九,实事方向,溢出最高。