标志寄存器

CPU中有一种特殊的寄存器——标志寄存器(不同CPU中的个数和结构都可能不同),主要有以下三种作用:

  1. 存储相关指令的某些执行结果
  2. 为CPU执行相关质量提供行为依据
  3. 控制CPU相关工作方式

8086CUP中的flag寄存器用来当作标志寄存器,大部分算数或者逻辑运算指令(add/sub/mul/div/inc/dec/or/and)会对标志寄存器有影响。而push/pop等传送指令则没有影响。而且有些指令的执行会影响多个标志位,比如指令sub al,al执行后,ZF、PF、SF等都会受影响。

8086CPU中的标志寄存器有16位,其中存储的信息通常被称为程序状态字(PSW),标志寄存器以下简称为flag。标志位如图:

  1. 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
  2. OF DF IF TF SF ZF AF PF CF

如上图所示,1,3,5,12,13,14,15位没有使用,没有任何意义,而其他几位都有不同的含义。

ZF标志

ZF(Zero Flag)位于flag第6位,零标志位,功能是记录相关指令执行后结果是否为0,如果结果为0,则ZF=1,否则ZF=0。如:

  1. mov ax,1
  2. sub ax,1

执行后结果为0,ZF=1。一般情况下,运算指令(如add,sub,mul,div,inc,or,and)影响标志寄存器,而传送指令(如mov,push,pop)不影响标志寄存器

PF标志

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

  1. mov al,1
  2. add al,10

执行后结果为00001011b,有3个1,所以PF=0。

SF标志

flag的第7位是SF(Symbol Flag)标志位,符号标志位,它记录相关指令执行后,结果是否为负,如果结果为负,则sf=1,结果为正,sf=0。计算机中通常用补码表示数据,一个数可以看成有符号数或无符号数,如:

  1. 00000001B,可以看成无符号1或有符号+1
  2. 10000001B,可以看成无符号129或有符号-127

也就是说对于同一个数字,可以当做有符号数运算也可以当做无符号数运算。这里SF位就是表示如果数据按照有符号数运算时,结果是否为负数。如:

  1. mov al,10000001b
  2. add al,1

这段代码结果是(al)=10000010b,可以将add指令进行的运算当做无符号运算,那么相当于129+1=130,也可以当做有符号运算,相当于-127+1=-126。SF标志就是在进行有符号运算的时候记录结果的符号的,当进行无符号运算的时候SF无意义(但还会影响SF,只是对我们来说没有意义了)。

CF标志

flag的第0位是CF(Carry Flag)标志位,进位标志位,一般情况下载进行无符号运算时,他记录了运算结果的最高有效位向更高位的进位值,或从更高位的借位值。加入一个无符号数据是8位的,也就是0-7个位,那么在做加法的时候就可能造成进位到第8位,这时并不是丢弃这个进位,而是记录在falg的CF位上。如:

  1. mov al,98h
  2. add al,al

执行后al=30h,CF=1。当两个数据做减法的时候有可能向更高位借位,如97h-98h借位后相当于197h-198h,CF也可以用来记录借位,如:

  1. mov al,97h
  2. sub al,98h

执行后(al)=FFH,CF=1记录了向更高位借位的信息。

OF标志

(Over Flag)在进行有符号运算的时候,如果结果超过了机器能表示的范围称为“溢出”。机器能表示的范围是指如8位寄存器存放或一个内存单元存放,表示范围就是-128~127,16位同理。如果超出了这个范围就叫做溢出,如:

  1. mov al,98
  2. add al,99
  3. mov al,0F0H
  4. add al,088H

第一段代码(al)=(al)+99=98+99=197超过了8位能表示的有符号数的范围,第二段代码结果(al)=(al)+(-120)=(-16)+(-12-)=-136也超过了8位有符号的范围,所以计算的结果是不可信的。如第一段代码计算之后(al)=0C5H,换成补码表示的是-59,98+99=-59很明显是不正确的结果。

flag的第11位是OF标志位,溢出标志位,一般情况下,OF记录有符号数运算结果是否溢出,如果溢出则OF=1,如果没有溢出,OF=0。所以CF是对无符号数的标志,OF是对有符号的标志。但对于一个运算指令,他们是同时生效的,只不过这个指令究竟是有符号还是无符号,是看实际的操作的。有符号CF无意义,无符号OF无意义。

adc指令

adc是带进位加法指令,利用了CF标志位上记录的进位值。格式:adc 操作对象1,操作对象2。功能:操作对象1=操作对象1+操作对象2+CF。如abc ax,bx实现的是(ax)=(ax)+(bx)+CF,如:

  1. mov ax,2
  2. mov bx,1
  3. sub bx,ax
  4. adc ax,1

注意这段代码,首先ax中的值是2,bx中的值是1,然后进行(bx)-(ax)的计算,结果是-1造成了无符号的借位,此时CF=1,在进行adc ax,1时,进行的是(ax)+1+CF=2+1+1=4。仔细分析一下就可以发现,如果把整个加法分开,低位先相加,然后高位相加再加上进位CF, 就是一个完整的加法运算,也就是说add ax,dx这个指令可以拆分为:

  1. add al,bl
  2. adc ah,bh

所以有了adc这个指令我们就可以完成一些更庞大的数据量的加法运算。如计算1EF000H+000H的值:

  1. mov ax,001eh
  2. mov bx,0f000h
  3. add bx,1000h
  4. adc ax,0020h

注:inc和loop指令不影响CF位。

sbb指令

sbb和adc类似,是带借位的减法,格式:sbb 操作对象1,操作对象2,执行的功能是操作对象1=操作对象1-操作对象2-CF,如:sbb ax,bx即(ax)=(ax)-(bx)-CF。sbb指令影响CF。

cmp指令

cmp是比较指令,cmp的功能相当于减法,只是不保存结果。

  1. cmp obj1,obj2 ; 实际上是执行的 obj1-obj2,结果放在ZF

cmp执行后影响标志寄存器,其他相关指令通过识别被影响的标志位来得知结果。格式:cmp 操作对象1,操作对象2,执行功能是计算对操作对象1-操作对象2但不保存结果,仅仅根据结果对标志位进行设置,如:cmp ax,ax结果为0,但并不保存在ax中,执行之后zf=1,pf=1,sf=0,cf=0,of=0。若执行cmp ax,bx通过标志位就可以判断结果:

  1. 若(ax)=(bx)则(ax)-(bx)=0,zf=1
  2. 若(ax)!=(bx)则(ax)-(bx)!=0,zf=0
  3. 若(ax)<(bx)则(ax)-(bx)产生借位,cf=1
  4. 若(ax)>=(bx)则(ax)-(bx)不产生借位,cf=0
  5. 若(ax)>(bx)则(ax)-(bx)既不产生借位,结果又不为0,cf=0zf=0
  6. 若(ax)<=(bx)则(ax)-(bx)既可能借位,结果可能为0,cf=1zf=1

但实际上往往会出现溢出,如34-(-96)=82H(82H是-126的补码),但应该等于130超出了补码表示的范围,所以sf=1。我们可以同时检验sf和of两个来验证cmp的结果:cmp ah,bh

  • 若sf=1,of=0说明没有溢出,那么sf的计算结果正确(ah)<(bh)
  • 若sf=1,of=1说明出现了溢出,那么sf结果相反(ah)>(bh)
  • 若sf=0,of=1说明有溢出,那么sf结果相反(ah)<(bh)
  • 若sf=0,of=0说明没有溢出,那么结果正确(ah)>=(bh)

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

    和高级语言的条件判断类似,这类指令会根据特定条件来执行跳转,执行不同的代码。如之前用过的jcxz,就是jump cx zero的缩写,如果cx是0的话就执行跳转。

下面几条指令和cmp一起使用,检测不同的标志位来达到不同的条件跳转效果:

指令 含义 检测的标志位
je 等于则转移 zf=1
jne 不等于转移 zf=0
jb 小于转移 cf=1
jnb 不小于转移 cf=0
ja 大于转移 cf=0且zf=0
jna 不大于转移 cf=1或zf=1

指令中的字母含义如下:

  • e:equa;
  • ne:not equal
  • b:below
  • nb:not below
  • a:above
  • na:not above

上面的检测都是在cmp进行无符号比较时的检测位,有符号数检测原理一样,只是检测的标志位不同而已。下面看一个例子,如果(ah)=(bh)则(ah)=(ah)+(ah),否则(ah)=(ah)+(bh)

  1. cmp ah,bh
  2. je s
  3. add ab,bh
  4. jmp short ok
  5. s:add ah,ah
  6. ok:···

这里注意的是,je检测的是zf位,而不管之前执行的是什么指令,只要zf=1就会发生转移,所以cmp的位置需要仔细的把控,当然是否和cmp配合使用也是取决于编程者,下面例子实现了统计data中数值为8的字节个数,然后用ax保存:

  1. ···
  2. data segment
  3. db 8,11,8,1,8,5,63,38
  4. data ends
  5. ···
  6. mov ax,data
  7. mov ds,ax
  8. mov bx,0
  9. mov ax,0
  10. mov cx,8
  11. s:cmp byte ptr [bx],8
  12. jne next
  13. inc ax
  14. next:inc bx
  15. loop s
  16. ···

DF标志位和串传送指令

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

  • df=0每次操作后si,di递增
  • df=1每次操作后si,di递减
    1. movsb ;相当于mov es:[di], byte ptr ds:[si]
    2. movsw ;相当于mov es:[di], word ptr ds:[si]
    串传送指令,movsb,这个指令相当于执行:
  1. ((es)16+(di))=((ds)16+(si))
  2. 如果df=0:(si)=(si)+1,(di)=(di)+1如果df=1:(si)=(si)-1,(di)=(di)-1

可以看出,movsb是将DS:SI指向的内存单元中的字节送入ES:DI中,然后根据DF的值对SI和DI增减1
同理mobsw就是将DS:SI指向的内存单元中的字送入ES:DI中,然后根据DF的值对SI和DI增减2
但一般来说,movsb和movsw都是和rep联合使用的,格式:rep movsb,这相当于:

  1. s:movsb
  2. loop s

所以rep的作用是根据cx的值重复执行后面的串传送指令,由于每次执行movsb之后si和di都会自行增减,所以使用rep可以完成(cx)个字节的传送。movsw也一样。

由于DF位决定着串传送的方向,所以这里有两条指令用来设置df的值:

  1. clddf=0
  2. stddf=1

例子:使用串传送指令将data段中第一个字符串复制到他后面的空间中:

  1. ···
  2. data segment
  3. db 'Welcome to masm!'
  4. db 16 dup (0)
  5. data ends
  6. mov ax,data
  7. mov ds,ax
  8. mov si,0
  9. mov es,ax
  10. mov di,16
  11. mov cx,16
  12. cld ;表示clear director,用于把DF变成0
  13. ;对应的有std(set director),用于把DF变成1
  14. rep movsb ;rep 命令表示重复movsb,重复的次数放在cx寄存器中
  15. ···

pushf和popf

pushf的功能是将标志寄存器的值入栈,popf是出栈标志寄存器。有了这两个命令,就可以直接访问标志寄存器了,如:

  1. mov ax,0
  2. push ax
  3. popf

标志寄存器在Debug中的表示

Debug中-r查看寄存器信息,最后有一段表示,下面列出我们已知的寄存器在Debug里的表示:

标志 值1的标记 值0的标记
of OV NV
sf NG PL
zf ZR NZ
pf PE PO
cf CY NC
df DN UP