8086CPU的标志寄存器有16位,其中存储的信息通常被成为程序状态字(PSW)。标志寄存器和其他寄存器是不一样的,其他寄存器是用来存放数据的,而flag寄存器是按位起作用的,它每一位都有专门的含义,记录特定的信息,其结构如下图所示。
从上述图中可以看到标志寄存器中的0,2,4,6,7,8,9,10,11都是有特定含义的,下面就学习一下寄存器中的标志位和其相关的典型指令。
11.1 零标志位ZF
标志寄存器的第6位是零标志位ZF,表示相关指令执行后的结果是否为0,如果结果为0,则ZF=1;如果执行结果不为0,则ZF=0。
比如,指令
mov ax,1
sub ax,1
执行后,结果为0,则ZF=1
注意:在8086CPU的指令集中,有些指令的执行是会影响标志寄存器的,比如add
、sub
、mul
、inc
、or
、and
等,这些指令大多都是运算指令;而像mov
、push
、pop
等这样的传送指令的执行对标志寄存器没有影响。
11.2 奇偶标志位PF
标志寄存器的第2位是奇偶标志位PF,记录相关指令执行后,其结果的所有位中1的个数是否为偶数。如果1的个数为偶数,则PF=1;如果为奇数,那么PF=0。
比如,指令
mov al,1
add al,10
执行后的结果为00001011B,其中有3(奇数)个1,则PF=0
mov al,1
or al,2
执行后,结果为00000011B,其中有2(偶数)个1,则PF=1
11.3 符号标志位SF
标志寄存器的第7位是符号标志位SF,它记录相关指令执行后的结果是否为负。如果结果为负,则SF=1;如果非负,则SF=0。
计算机通常用补码来表示有符号数据,计算机中的一个数据可以被看成是有符号数或者无符号数,例如
00000001B ; 有符号数+1 或 无符号数1
10000001B ; 有符号数-127 或 无符号数129
同样,计算机可以将一个二进制数据当作无符号数据计算,也可以当作有符号数据计算。例如
mov al,10000001B
add al,1
结果为:(al) = 10000010B
可以将add
指令进行的运算当作无符号数的运算,那么add
指令相当于计算,结果为130(10000010B);也可以当作有符号数的运算,那么add
指令相当于计算,结果为(1000010B).
上述的CPU的运算默认是有两层含义的
- 有符号运算:使用符号标志位SF来记录数据的政府
- 无符号运算:符号标志为SF没有用,但是计算结果还是会影响它的值
某些指令会影响标志寄存器中的多个标志位,这些标志为能够较为全面地记录指令的执行结果,为相关的处理提供了所需的依据,例如
sub al,al
; 执行后:ZF=1,PF=1,SF=0(虽然没什么用)
11.4 进/借位标志位CF
标志寄存器的第0位是进位标志位CF。一般情况下,在进行无符号数运算的时候,它记录了运算结果的最高有效位向更高位的进位值,或从更高位的借位值。
对于位数为N的无符号数来说,其对应的二进制信息的最高位,即第位,就是它的最高有效位,而假想存在的第N位,就是相对于最高有效位的更高位,如图11.2所示。
之前我们进行相加运算的时候,有可能产生从最高有效位向更高位的进位。比如,两个8位数据相加 就会产生进位。由于这个进位值在8位数中无法保存,我们在前面的课程中,就只是简单地说这个进位值丢失了。其实CPU在运算的时候,将这个值记录在了标志寄存器的进位标志位CF上。
mov al,98H
add al,al ; 执行后: (al)=30H,CF=1,CF记录了从最高有效位向更高位的进位值
add al,al ; 执行后:(al)=60H,CF=0,CF记录了从最高有效位向更高位的进位值
当两个数据做减法的时候,有可能向更高位借位。比如,连个数据相减 ,将产生借位。借位后,相当于计算 ,而标志寄存器的也可用来记录这个借位值
mov al,97H
sub al,98H ; 执行后,(al)=FFH,CF=1,由于借位1所以记录1
sub al,al ; 执行后,(al)=0,CF=0,由于没有借位所以记录0
11.5 溢出标志位OF
当进行有符号数运算的时候,如果超过了机器所能表示的范围成为溢出,机器所能表示的范围是什么呢?
- 当我们使用8位寄存器存放指令运算的结果的时候,对于8位有符号数据,机器所能表示的范围就是-128~127
- 当我们使用16位寄存器存放指令运算的结果的时候,对于16位有符号数据,机器所能表示的范围就是-32768~32767
下面看关于溢出的例子
; 例子1:正数溢出
mov al,98
add al,99
; 执行后将会造成溢出
; 99+98=197,而197超过了8位有符号数的表示范围:-128~127
; 例子2:负数溢出
mov al,0F0H ; F0H为有符号数-16的补码
add al,088H ; 88H为有符号数-120的补码
; 执行后,将产生溢出
; -16-120=-136,而-136超出了机器所能表示的范围:-128~127
我们使用add
指令计算出的结果为(al)=0C5H
,而这样的运算结果所能表示的可能有两种
- 正数:197
- 负数:因为我们使用补码表示的,所以实际负数为-59,但是这个结果显然不可能是真实的,所以这个是错的
还记得上面的进位标志位SF,上面是记录是否进位的,而进位是不是就相当于变相的溢出呢?对于无符号运算,其范围是0~256;而对于有符号运算,其范围是-127~128。
mov al,98
mov al,99
; 无符号数:add指令后98+99=197 < 256,所以不需要进位
; 有符号数:add指令后98+99=197 > 128,所以需要溢出
11.6 带位加法指令adc
adc
是带进位加法指令,它利用了进/借位上记录的进位值。
指令格式:adc 操作对象1, 操作对象2
功能:操作对象1=操作对象1+操作对象2+进位对象CF
,比如adc ax,bx
实现的功能是:(ax)=(ax)+(bx)+CF
例如
; 例子1
mov ax,2
mov bx,1
sub bx,ax ; bx-ax需要借位,所以CF=1
adc ax,1
; 执行后,(ax)=4,相当于计算(ax)+1+CF = 2+1+1 = 4
; 例子2
mov ax,1
add ax,ax
adc ax,3
; 执行后,(ax)=5,相当于计算(ax)+3+CF = 2+3+0 = 5
; 例子3
mov al,98H
add al,al ; al+al产生借位,所以CF=1
adc al,3
; 执行后,(al)=34H,相当于计算(al)+3+CF = 30H+3+1 = 34H
相比于add
指令,adc
多加了一个CF位的值,为什么要加上CF的值呢?CPU为什么要提供这样一条指令呢?
先来看一下CF的值的含义,在执行adc
指令的时候加上的CF的值的含义是由adc
指令前面的指令决定的。
- 如果CF的值是被
sub
指令设置的,那么它的含义就是借位值 - 如果CF的值是被
add
指令设置的,那么它的含义就是进位值
现在我们来看一下数据0198H和0183H是如何相加的?
可以看出,加法分为两步进行的
- 低位相加
- 高位相加再加上低位相加产生的进位值
而上面的步骤add ax,bx
可以用下面的指令表示
add al,bl
adc ah,bh
现在来看CPU提供adc
指令的目的就是来进行加法的第二步运算的,**adc**
指令和**add**
指令相配合就可以对更大的数据进行加法运算。我们现在看一个例子:
编程,计算1EF00H+201000H,结果放在ax(高16)位和bx(低16位)中。
因为两个数据的位数都大于16,用add
指令无法进行计算。我们将计算分为两步进行
- 先将低16位相加
- 将高16位和进位值相加
如下所示
mov ax,001EH
mov bx,0F000H
add bx,1000H
adc ax,0020H
adc
指令执行后,也可能产生进位值,所以也会对CF位进行设置。由于有这样的功能,我们就可以对任意大的数据进行加法运算,看下面这个例子:
编程,计算1EF0001000H+2010001EF0H,结果放在ax(最高16位)和bx(次高16位)和cx(低16位)中。
计算分为三步进行:
- 先将低16位相加,完成后,CF中记录本次相加的进位值
- 再将次高16位和CF(来自低16位的进位值)相加,完成后,CF中记录本次相加的进位值
- 最后高16位和CF(来自次高16位的进位值)相加,完成后,CF中记录本次相加的进位值
程序如下
mov ax,001EH
mov bx,0F000H
mov cx,1000H
add cx,1EF0H
adc bx,1000H
adc ax,0020H
下面编写一个子程序,对两个128位数据进行相加
- 名称:
add128
- 功能:两个128位数据进行相加
- 参数:
ds:si
指向存储第一个数的内存空间,因数据为128位,所以需要8个子单元,由低地质单元到高地质单元依次存放128位数据由低到高的各个字。运算结果存储在第1个数的存储空间中。ds:di
指向存储第二个数的内存空间。
程序如下:
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
指令,用
add si,2
add di,2
来取代?
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中,程序如下
mov bx,1000H
mov ax,003EH
sub bx,2000H ; 借位标识符CF=1
sbb ax,0020H ; 003E - 0020 - 1
sbb
和adc
非常类似,所以不用过多关注。
11.8 比较指令cmp
cmp
是比较指令,cmp
的功能相当于减法指令,但是不保存结果。cmp
指令执行后,将对标志寄存器产生影响。
cmp
指令格式:cmp 操作对象1, 操作对象2
- 功能:计算
操作对象1 - 操作对象2
但并不保存结果,仅仅根据计算结果对标志寄存器进行设置。 - 比如:指令
cmp ax, ax
相当于进行(ax) - (ax)
的运算,结果为0,但并不在ax中保存,仅仅影响标志寄存器中的标志位。执行后,ZF=1, PF=1, SF=0, CF=0, OF=0
下面的指令
mov ax,8
mov bx,3
cmp ax,ax
指令执行后,(ax)=8, zf=0, pf=1, sf=0, cf=0, of=0
通过cmp
指令执行后的相关标志位的值就可以知道两个数的关系,例如(ax)和(bx)
cmp ax,bx
- 如果%3D(bx)#card=math&code=%28ax%29%3D%28bx%29&id=wkDLI),则-(bx)%3D0#card=math&code=%28ax%29-%28bx%29%3D0&id=nhpaK),所以:ZF=1
- 如果%5Cne(bx)#card=math&code=%28ax%29%5Cne%28bx%29&id=WuzP2),则-(bx)%5Cne0#card=math&code=%28ax%29-%28bx%29%5Cne0&id=gfUtz),所以:ZF=0
- 如果%3C(bx)#card=math&code=%28ax%29%3C%28bx%29&id=jUkyX),则-(bx)#card=math&code=%28ax%29-%28bx%29&id=l3xpP)将产生借位,所以:CF=1
- 如果%5Cgeqslant(bx)#card=math&code=%28ax%29%5Cgeqslant%28bx%29&id=Jdzi9),则-(bx)#card=math&code=%28ax%29-%28bx%29&id=eshAF)不必借位,所以:CF=0
- 如果%3E(bx)#card=math&code=%28ax%29%3E%28bx%29&id=nLsnD),则-(bx)#card=math&code=%28ax%29-%28bx%29&id=ieHam)既不必借位,结果也不为0,所以:CF=0 并且 ZF=0
- 如果%5Cgeqslant(bx)#card=math&code=%28ax%29%5Cgeqslant%28bx%29&id=yKMmb),则-(bx)#card=math&code=%28ax%29-%28bx%29&id=YsRR4)既可能借位,结果可能为0,所以:CF=1 或 ZF=1
指令cmp ax,bx
的逻辑含义是比较ax和bx中的值,如果执行后:
- ZF=1,说明%3D(bx)#card=math&code=%28ax%29%3D%28bx%29&id=Piy9g)
- ZF=0,说明%5Cne(bx)#card=math&code=%28ax%29%5Cne%28bx%29&id=QSODC)
- CF=1,说明%3C(bx)#card=math&code=%28ax%29%3C%28bx%29&id=z8wbG)
- CF=0,说明%5Cgeqslant(bx)#card=math&code=%28ax%29%5Cgeqslant%28bx%29&id=UzWR6)
- CF=0 并且 ZF=1,说明%3E(bx)#card=math&code=%28ax%29%3E%28bx%29&id=nnDdS)
- CF=1 或 ZF=1,说明%5Cleqslant(bx)#card=math&code=%28ax%29%5Cleqslant%28bx%29&id=BSStC)
和add
、sub
指令一样,CPU在执行cmp
指令的时候,包含两种含义
- 进行无符号运算:对无符号数进行比较
- 进行有符号运算:对有符号数进行比较
接下来看的是如何对有符号数进行比较,并且CPU使用哪些标志位对结果进行记录,以cmp ah,bh
为例进行说明
cmp ah,bh
有两种结果
- 如果%3D(bh)#card=math&code=%28ah%29%3D%28bh%29&id=jv83E),则-(bh)%3D0#card=math&code=%28ah%29-%28bh%29%3D0&id=zvTai),所以ZF=1;
- 如果%5Cne%20(bh)#card=math&code=%28ah%29%5Cne%20%28bh%29&id=o28Vm),则-(bh)%5Cne0#card=math&code=%28ah%29-%28bh%29%5Cne0&id=FwEOX),所以ZF=0.所以,根据
cmp
指令执行后ZF的值就可以知道两个数是否相等
如果%3C(bh)#card=math&code=%28ah%29%3C%28bh%29&id=Ejcyp)可能会发生什么呢?
对于有符号数运算,在%3C(bh)#card=math&code=%28ah%29%3C%28bh%29&id=sIGiM)情况下,-(bh)#card=math&code=%28ah%29-%28bh%29&id=l7fFq)显然可能引起SF=1,即结果为负。那么如果SF=1,是否就能说明%3C(bh)#card=math&code=%28ah%29%3C%28bh%29&id=VBjvm)呢?
当然不是,我们再看两个例子
(ah) = 22H
(bh) = 0A0H
则(ah)-(bh) = 34-(-96) = 82H,82H是-126的补码,所以SF=1。**这里虽然SF=1,但是并不能说明(ah)<(bh),因为显然34>-96。
两个有符号数单纯利用ZF来判断就不可以了。
11.9 检测比较结果的条件转移指令
转移指的是它能够修改IP,而条件指的是它可以根据某种条件决定是否修改IP,下面是常用的根据无符号数的比较结果进行转移的条件转移指令
另外,像jcxz
就是一个条件转移指令,它可以检测cx中的数值,如果(cx)=0,就修改IP,否则什么也不做。所有条件转移指令的转移位移都是[-128, 127]。除了jcxz
指令外,CPU提供的其他条件转移指令大多与cmp
指令相互配合使用,就像是上面的jn
,jne
等。
例子,编程实现如下功能:
如果%20%3D%20(bh)#card=math&code=%28ah%29%20%3D%20%28bh%29&id=FmMKc)则%3D(ah)%2Bah#card=math&code=%28ah%29%3D%28ah%29%2Bah&id=sFPee),否则%3D(ah)%2B(bh)#card=math&code=%28ah%29%3D%28ah%29%2B%28bh%29&id=qQCtc)
cmp ah,bh ; 如果(ah)=(bh),则ZF=1
je s ; 检测ZF是否为1,如果为1跳转到s标号处执行程序,下面两行将被忽略
add ah,bh
jmp short ok
s:add ah, ah
ok:...
之前,我们也学习过影响零标志位ZF的指令有很多,像add
指令,如下所示
mov ax,0
add ax,0
je s
inc ax
s:inc ax
所以一定要主意好这个因素,别让其它指令干扰了你想要的效果!
下面看一下条件转移指令在编程中的用法。我们看下面的一组程序,data段中的8个字节如下所示
data segment
db 8,11,8,1,8,5,63,38
data ends
问题一:编程,统计data段中数值为8的字节的个数,用ax保存统计结果。
编程思路:初始设置%3D0#card=math&code=%28ax%29%3D0&id=xLBQh),然后用循环依次比较每个字节的值,找到一个和8相等的数就将ax的值加1,程序如下
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
问题二:编程,统计data段中数值大于8的字节的个数,用ax保存统计结果
编程思路:初始设置%3D0#card=math&code=%28ax%29%3D0&id=NU2Ok),然后用循环一次比较每个字节的值,找到一个大于8的就将ax的值加1,程序如下。
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
程序执行后:%3D3#card=math&code=%28ax%29%3D3&id=oQ3tL)
问题三:编程,统计data段中数值小于8的字节的个数,用ax保存统计结果
编程思路:初始设置%3D0#card=math&code=%28ax%29%3D0&id=aBj7i),然后用循环一次比较每个字节的值,找到一个小于8的就将ax的值加1,程序如下。
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
程序执行后:%3D2#card=math&code=%28ax%29%3D2&id=yOWix)
11.10 方向标志位DF和串传送指令
标志寄存器的第10位是方向标志位DF,在串处理指令中,控制每次操作后si和di的增减。
- :每次操作后si和di递增
- :每次操作后si和di递减
我们来看下面的一个串传送指令
串传送指令movsb
格式:movsb
功能:执行movsb
指令相当于进行下面几步操作
- 16%20%2B%20(di))%20%3D%20((ds)16%2B(si))#card=math&code=%28%28es%29%2A16%20%2B%20%28di%29%29%20%3D%20%28%28ds%29%2A16%2B%28si%29%29&id=cnb62)
- 如果,则%3D(si)%2B1#card=math&code=%28si%29%3D%28si%29%2B1&id=nAhrr),%3D(di)%2B1#card=math&code=%28di%29%3D%28di%29%2B1&id=ewM0B);否则递减
用汇编语法描述movsb
的功能如下
mov es:[di],byte ptr ds:[si] ; 8086并不支持这个指令,这里只是一个描述
; 如果df=0
inc si
inc di
; 如果df=1
dec si
dec si
可以看出,**movsb**
的功能是将 ds:si 指向的内存单元中的字节送入 es:di 中,然后根据标志寄存器DF的值,将si和di递增或递减。
串传送指令movsw
**movsb**
是将字节进行传送,**movsw**
则是将字进行传送。movsb
的功能是将 ds:si 指向的内存单元中的字送入 es:di 中,然后根据标志寄存器DF的值,将si和di递增2或递减2。
用汇编语法描述movsw
的功能如下
mov es:[di],word ptr ds:[si] ; 8086并不支持这个指令,这里只是一个描述
; 如果df=0
add si,2
add di,2
; 如果df=1
sub si,2
sub si,2
movsb
和movsw
进行的是串传送操作中的一个步骤,一般来说,movsb
和movsw
都和rep
配合使用,格式如下:
rep movsb
用汇编语法描述rep movsb
的功能就是:
s:movsb
loop s
可见,**rep**
的作用是根据cx的值,重复执行后面的串传送指令。由于每执行一次movsb
指令si和di都会递增或递减指向后一个单元或前一个单元,则rep movsb
就可以循环实现(cx)个字节的传送。
改变方向标志位DF
由于标志寄存器的方向标志位DF决定着串传送指令执行后,si和di改变的方向,所以CPU应该提供相应的指令对DF来进行设置,从而能够让程序员决定传送的方向。
8086CPU提供下面两条指令对DF位进行设置
cld
指令:将标志寄存器的方向标志位DF置0std
指令:将标志寄存器的方向标志位DF置1
下面看两个程序
问题一:编程,使用串传送指令,将data段中的第一个字符串复制到它后面的空间中
data segment
db 'Welcom to masm'
db 16 dup (0)
data ends
我们分析一下,使用串传送指令进行数据的传送,需要给它提供一些必要的信息,他们是
- 传送的原始位置:ds:si
- 传送的目的位置:es:di
- 传送的长度:cx
- 传送的方向:df
在这个问题中,这些信息如下
- 传送的原始位置:data:0
- 传送的目的位置:data:0010
- 传送的长度:16
- 传送的方向:因为正向传送比较方便,所以设置DF=0
所以程序如下所示
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
问题二:编程,用串传送指令,将F0000H段中的最后16个字符复制到data段中
data segment
db 16 dup (0)
data ends
要传送的字符串位于F000H段的最后16个单元中,那么它的最后一个字符的位置为:F000:FFFF。可以将ds:si指向F000H段的最后一个单元,将es:di指向data段的最后一个单元,然后逆向传送16个字节即可
- 传送的原始位置:F000:FFFF
- 传送的目的位置:data:000F
- 传送的长度:16
- 传送的方向:因为逆向传送比较方便,所以设置DF=1
所以程序如下所示
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.12 标志寄存器在Debug中的表示
在Debug中,标志寄存器是按照有意义的各个标志位单独表示的。在Debug中,我们可以看到如下信息。
而其中的标志寄存器就如图中部分所示,他们的值表示为