汇编语言个人学习笔记——第十一章 标志寄存器
引言:
8086CPU的标志寄存器有16位,其中存储的信息通常被称为程序状态字(PSW)。
我们已经使用过8086CPU的ax、bx、cx、dx、si、di、bp、sp、ip、cs、ss、ds、es等13个寄存器了。
标志寄存器(简称flag)是要学习的最后一个寄存器。
flag和其他寄存器不一样,其他寄存器是用来存放数据的,都是整个寄存器具有一个含义。
而flag寄存器是按位起作用的,也就是说,它的每一位都有专门的含义,记录特定的信息。
8086CPU的flag寄存器的结构:
flag的1、3、5、12、13、14、15位在8086CPU中没有使用,不具有任何含义。而0、2、4、6、7、8、9、10、11位都具有特殊的含义。
11.1ZF标志
flag的第6位是ZF,零标志位。
它记录相关指令执行后,
结果为0,ZF=1
结果不为0,ZF=0
例如:
mov ax,1
sub ax,1
指令执行后,结果为0,则ZF=1。
mov ax,2
mov ax,1
指令执行后,结果为1,则ZF=0。
对于ZF的值,我们可以这样来看,ZF标记相关指令的计算结果是否为0,如果为0,则在ZF要记录下“是0”这样的肯定信息。
指令:mov ax,1
and ax,0
执行后,结果为0,则ZF=1,表示“结果是0”。
指令:mov ax,1
or ax,0
执行后,结果不为0,则ZF=0,表示“结果非0”。
注意:
在8086CPU的指令集中,有的指令的执行是影响标志寄存器的,比如:add、sub、mul、div、inc、or、and等,它们大都是运算指令(进行逻辑或算术运算);
有的指令的执行对标志寄存器没有影响,比如:mov、push、pop等,它们大都是传送指令。
我们在使用一条指令的时候,要注意这条指令的全部功能,其中包括,执行结果对标志寄存器的哪些标志位造成影响。
11.2PF标志
flag的第2位是PF,奇偶标志位。
它记录指令执行后,结果的所有二进制位中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.3SF标志
flag的第7位是SF,符号标志位。
它记录指令执行后,
结果为负,SF=1;
结果为正,SF=0。
有符号数与补码
我们知道计算机中通常用补码来表示有符号数据。计算机中的一个数据可以看作是有符号数,也可以看作是无符号数。
比如:
00000001B,可以看作无符号数1,或有符号数+1;
10000001B,可以看作无符号数129,也可以看作有符号数-127。
这也就是说,对于同一个二进制数据,计算机可以将它当作无符号数据来运算,也可以当作有符号数据来运算。
示例
mov al,10000001B
add al,1
结果:(al)=10000010B
我们可以将add指令进行的运算当作无符号数的运算,那么add指令相当于计算129+1,结果为130
(10000010B);
也可以将add指令进行的运算当作有符号数的运算,那么add指令相当于计算-127+1,结果为-126
(10000010B)。
不管我们如何看待,CPU在执行add等指令的时候,就已经包含了两种含义,也将de'd得到用同一种信息来记录的lian两种结果。
关键在于我们的程序需要哪一种结果。
SF标志,就是CPU对有符号数运算结果的一种记录,它记录数据的正负。
在我们将数据当作有符号数来运算的时候,可以通过SF来得知结果的正负。
如果我们将数据当作无符号数来运算,SF的值则没有意义,虽然相关的指令影响了它的值。
这也就是说,CPU在执行add等指令时,是必然要影响到SF标志位的值的。
至于我们需不需要这种影响,就要看我们如何看待指令所进行的运算了。
示例1:
mov al,10000001B
add al,1
执行后,结果为10000010B,SF=1,
表示:如果指令进行的是有符号数运算,那么结果为负;
示例2:
mov al,10000001B
add al,01111111B
执行后结果为0,SF=0
表示:如果指令进行的是有符号数运算,那么结果为非负。
某些指令将影响标志寄存器中的多个标志位,这些被影响的标记位比较全面地记录了指令地执行结果,为相关的处理提供了所需的依据。
比如指令sub al,al执行后,ZF、PF、SF等标志位都要受到影响它们分别位1、1、0。
11.4CF标志
flag的第0位是CF,进位标志位。
一般情况下,在进行无符号数运算的时候,它记录了运算结果的最高有效位向更高位的进位值,或从更高位的借位值。
对于位数为N的无符号数来说,其对应的二进制信息的最高位,即第N-1位,就是它的最高有效位,而假想存在的第N位,就是相对于最高有效位的更高位。
、
当两个数据相加的时候,有可能产生从最高有效位向更高位的进位。
比如,两个8位数据:98H+98H,将产生进位。由于这个进位值在8位数中无法保存,前面的学习中,只是简单地说这个进位值丢失了。
其实CPU在运算的时候,并没有丢弃这个进位值,而是记录在一个特殊寄存器的某一位上。
8086CPU就用flag的CF位来记录这个进位值。
在Debug中,我们可以看到类似下面的信息:
OF(over flag)溢出寄存器 NV(not overflow未溢出) OV(overflow)
DF(direction flag)方向寄存器 UP(向上)DN(down向下)
SF(sign flag)符号寄存器 NG(negtive负数)PL(plus正数)
ZF(zero flag)零标志 ZR(zero零)NZ(not zero非零)
PF(parity flag)奇偶标志 PE(parity even偶数)PO(partity odd奇数)
CF(carry flag)进位标志 CY(carried进位)NC(not carried不进位)
比如下面的指令:
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位也可以用来记录这个借位值。
11.5OF标志
先说一下溢出的问题。
在进行有符号数运算的时候,如结果超过了机器所能表示的范围称为溢出。
比如:add al,3,那么对于8位的有符号数据,机器所能表示的范围就是-128~127。
如果运算结果超出了机器所能表示的范围,将产生溢出。
注意,这里所讲的溢出,只是对有符号数运算而言。(进位只是相对于无符号数而言!)
如果在进行有符号数运算时发生溢出,那么运算的结果将不正确。
举例:
mov al,98
add al,99
add指令运算的结果是(al)=0C5H,因为是按有符号数进行运算的,所以al中存储的是有符号数,而0C5H是有符号数-59的补码。
如果我们用add指令进行的是有符号数运算,则98+99=-59这样的结果显然错误。
造成这种结果的,是因为实际的结果197作为一个有符号数,在8位寄存器al中存放不下。
由于进行有符号数运算时,可能发生溢出而造成结果的错误。所以CPU需要对指令执行后是否产生溢出进行记录。因此有了OF。
注意,CF和OF的区别:
CF是对无符号数运算有意义的标志位;
OF是对有符号数运算有意义的标志位。
对于无符号数运算,CPU用CF位来记录是否产生了进位;
对于有符号数运算,CPU用OF位来记录是否产生了溢出,
当然,还要用SF位来记录结果的符号。
对于有无符号,计算机是分不清的,因此他必须两种都记载着,要怎么用,要看用户当它是什么。
例如:mov 98d
add al,99d
对于无符号数运算,98+99没有进位,CF=0;
对于有符号数运算,98+99发生溢出,OF=1。
总结:对于无符号数,OF标志是没有意义的,而CF是有意义的
对于有符号数,CF标志是没有意义的,而OF是有意义的
11.6adc指令
adc是带进位加法指令,它利用了CF位上记录的进位值。
格式:adc 目标1,目标2
功能:目标1=目标1+目标2+CF
比如:adc ax,bx实现的功能是:
(ax)=(ax)+(bx)+CF
示例一:
mov ax,2
mov bx,1
sub bx,ax
adc ax,1
adc执行时,相当于计算:(ax)+1+CF=2+1+1=4。
示例二:
mov ax,1
add ax,ax
adc ax,3
adc执行时,相当于计算:(ax)+3+CF=2+3+0=5。
示例三:
mov al,98H
add al,al
adc al,3
adc执行时,相当于计算:(ax)+3+CF=30H+3+1=34H。
在执行adc指令的时候加上的CF的值的含义,由adc指令前面的指令决定的,也就是说,关键在于所加上的CF值是被什么指令设置的。
显然,如果CF的值是被sub指令设置的,那么它的含义就是借位值;如果是被add指令设置的,那么它的含义就是进位值。
来看一下两个数据:0198H和0183H如何相加的:
可以看出,加法可以分两步来进行:
(1)低位相加
(2)高位相加再加上低位相加产生的进位值。
下面的指令和add ax,bx具有相同的结果:
add al,bl
adc ah,bh
CPU提供adc的目的,就是来进行加法的第二步运算的。
adc指令和add指令相配和就可以对更大的数据进行加法运算。
编程任务:
编程计算1EF000H+201000H,结果放在ax(高16位)和bx(低16位)中。
代码如下:
adc指令执行后,也可能产生进位值,所以也会对CF位进行设置。
这样的功能使我们可以对任意大的数据进行加法运算。
例如:编程计算
1EF0001000H+2010001EF0H,结果放在ax(高16位),bx(次高16位),cx(低16位)中。
实践:编写一个子程序,对两个128位数据进行相加。
名称:add128
功能:两个128位数据进行相加
参数:ds:si指向存储第一个数的内存空间,因数据为128位,所以需要8个字单元,由低地址单元到高地址单元依次存放128位数据由低到高的各个字。运算结果存储在第一个数的存储空间中。
ds:di指向存储第二个数的内存空间。
代码:
思考:inc和loop指令不影响CF位,上面的程序中,能不能将4个inc指令用:
add si,2
add di,2
取代?
不可以!!因为add指令会影响CF位的值!
11.7sbb指令
sbb是带错位减法指令,它利用了CF位上记录的借位值。
格式:sbb 目标1,目标2
功能:目标1=目标1-目标2-CF
比如:sbb ax,bx
实现功能:(ax)=(ax)-(bx)-CF
利用sbb指令我们可以对任意大的数据进行减法运算。
计算003E1000H-00202000H,结果存放在ax,bx中,程序如下:
mov bx,1000H
mov ax 003EH
sub bx,2000H
sbb ax,0020H
通过学习这两条指令,我们可以进一步领会一下标志寄存器CF位的作用和意义。
11.8cmp指令
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
下面的指令:
mov ax,8
mov bx,3
cmp ax,bx
执行后:(ax)=8
ZF=0
PF=1
SF=0
CF=0
OF=0
其实,我们通过cmp指令执行后,相关标志位的值就可以看出比较的结果。
例如:cmp ax,bx
现在我们可以看出比较指令的设计思路
即:通过做减法运算,影响标志寄存器,标志寄存器的相关位记录了比较的结果。
再回头看上面的例子:cmp ax,ax
同add、sub指令一样,CPU在执行cmp指令的时候,也包含两种含义:
进行无符号数运算和进行有符号数运算。
所以利用cmp指令可以对无符号数进行比较,也可以对有符号数进行比较。
来看一下如果用cmp来进行有符号数比较时,需要注意哪些标志位
我们以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,(bx)=0FFH:
则(ah)-(bh)=(-2)-(-1)=0FFH,因为结果为负,所以SF=1。
通过这两个例子,是否可以得出这样的结论:
cmp 目标1,目标2 指令执行后,SF=1,就说明,目标1<目标2?
并不是。
再看一个例子:
(ah)=22H,(bh)=0A0H:
则(ah)-(bh)=34-(-96)=130=82H,82H是-126的补码,所以SF=1。
这里虽然SF=1,但是并不能说明(ah)<(bh),因为显然34>-96。
单纯地考察SF的值不可能知道结果的正负。因为SF记录的只是可以在计算机中存放的相应位数的结果的正负。
比如add ah,al执行后,SF记录的是ah中的8位二进制信息所表示的数据的正负。
所得到的相应结果的正负,并不能说明,运算所应该得到的结果的正负。
这是因为在运算的过程中可能发生溢出。
所以,我们应该在考察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,OF=1,说明了(ah)>(bh)。
(3)如果SF=0,而OF=1
OF=1,说明有溢出,逻辑上真正结果的正负≠实际结果的正负;
简单分析一下,就可以看出,如果因为溢出导致了实际结果为正,那么逻辑上真正的结果必然为负。这样,SF=0,OF=1,说明了(ah)<(bh)。
(4)如果SF=0,而OF=0
OF=0,说明没有溢出,逻辑上真正结果的正负=实际结果的正负;
因SF=0,实际结果非负,所以逻辑上真正的结果必然非负。所以(ah)≥(bh)。
在学习中,要领会80886CPU这种工作机制的设计思想。实际上,这种设计思想对于各种处理机来说是普通的。
下面将学习一些根据cmp指令的比较结果(即cmp指令执行后,相关标志位的值)进行工作的指令。
它们检测的就是被cmp指令影响的哪些,表示比较结果的标志位。
这些条件转移指令通常都和cmp相配合使用,就像call和ret指令通常相配合使用一样。
因为cmp指令可以同时进行两种比较,无符号数比较和有符号数比较,所以根据cmp指令的比较结果进行转移的指令也分为两种,即:
1、根据无符号数的比较结果进行转移的条件转移指令,它们检测ZF、CF的值;
2、根据有符号数的比较结果进行转移的条件转移指令,它们检测SF、OF和ZF的值。
11.9检测比较结果的条件转移指令
下面是常用的根据无符号数的比较结果进行转移的条件转移指令
这些指令比较常用也好记忆,它们的第一个字母都是j,表示jump;后面的:
e:表示equal
ne:表示not equal
b:表示below
nb:表示not below
a:表示above
na:表示not above
注意观察一下它们所检测的标志位,都是cmp指令进行无符号数比较时,记录比较结果的标志位。
比如je,检测ZF位,当ZF=1的时候进行转移,如果在je前面使用了cmp指令,那么je对ZF的检测,实际上就是间接地检测cmp的比较结果是否为两数相等。
编程训练:
编程实现如下功能:
如果(ah)=(bh)则(ah)=(ah)+(ah),否则(ah)=(ah)+(bh)。
cmp ah,bh
je s
add ah,bh
jmp short ok
s:add ah,ah
ok:ret
虽然je的逻辑含义是“相等则转移”,但它进行的操作是,ZF=1时则转移。
“相等则转移”这种逻辑含义,是通过和cmp指令配合使用来体现的,因为是cmp指令为“ZF=1”赋予了“两数相等”的含义。
至于究竟在je之前使不使用cmp指令,在于我们的安排。
je检测的是ZF的位置,不管je前面是什么指令,只要CPU执行je指令时,ZF=1,那么就会发生转移。
比如:
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指令发生转移。
对于jne、jb、jnb、ja、jna等指令和cmp指令配合使用的思想和je相同,不再详述。
练习:
data段中的8个字节如下:
data segment
db 8,11,8,1,8,5,63,38
data ends
(1)编程:统计data段中数值为8的字节的个数,用ax保存统计结果。
个人代码:
这个程序效率并不算高,不如下面程序:
(2)编程:统计data段中数值大于8的字节的个数,用ax保存统计结果。
(3)编程:统计data段中数值小于8的字节的个数,用ax保存统计结果。
上面记录了根据无符号数的比较结果进行转移的条件转移指令。
根据有符号数的比较结果进行转移的条件转移指令的工作原理和无符号的相同,只是检测了不同的标志位。
11.10DF标志和串传送指令
flag的第10位是DF,方向标志位。
在串处理指令中,控制每次操作后si,di的增减。
DF=0:每次操作后si,di递增;
DF=1:每次操作后si,di递减。
格式1:movsb
功能:(以字节为单位传送)
(1)((es)x16+(di))=((ds)x16+(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递增或递减。
当然也可以传送一个字:movsw
格式2:movsw
功能:(以字为单位传送)
将ds:si指向的内存字单元中word送入es:di中,然后根据标志寄存器DF位的值,将si和di递增2或递减2。
movsb和movsw进行的是串传送操作中的一个步骤,一般来说,movsb和movsw都和rep配合使用,格式如下:rep movsb
rep的作用是根据cx的值,重复执行后面的串传送指令。
由于每执行一次movsb指令si和di都会递增或递减指向后一个单元或前一个单元,则rep movsb就可以循环实现(cx)个字符的传送。
由于flag的DF位决定着串传送指令执行后,si和di改变的方向,所以CPU应该提供相应的指令来对DF位进行设置,从而使程序员能够决定传送的方向。
8086CPU提供下面两条指令对DF位进行设置:
cld指令:将标志寄存器的DF位置为0
std指令:将标志寄存器的DF位置为1
来看两个程序
编程1:
用串传送指令,将data段中的第一个字符串复制到它后面的空间中。
data segment
db 'Welcome to masm!'
db 16 dup (0)
data ends
代码如下:
编程2:
用串传送指令,将F000H段中的最后16个字符复制到data段中。
data segment
db 16 dup (0)
data ends
要传送的字符串位于F000H段的最后16个单元中,那么它的最后一个字符的位置:F000:FFFF,是显而易见的。
我们可以将ds:si指向F000H段的最后一个单元,将es:di指向data段中的最后一个单元,然后逆向(即从高地址向低地址)传送16个字节即可。
代码如下:
11.11pushf和popf
pushf:将标志寄存器的值压栈;
popf:从栈中弹出数据,送入标志寄存器中。
pushf和popf,为直接访问标志寄存器提供了一种方法。
具体用法在检测点中总结。
到这里,如果只想学**,后面的就不需要了,后面的内容是真正的8086汇编内容,与**无关了。
本人觉得,还是继续学习下去比较好,据说如果能独立完成课程设计2,就可以写出一个计算机病毒了。