由于第一个笔记写到后面编辑界面和查看界面都出奇的卡,翻页也难受,所以新开了一个
7 标志寄存器
标志寄存器用来存储计算的某些结果,为 cpu 的执行提供依据或控制其行为
与其它寄存器不同,它是每个二进制位代表一个意义(其它都是整个寄存器代表一个意义)
每一位的意义如下,空白说明在 8086cpu 中这一位无意义
7.1 ZF
第 6 位,零标志位
如果上一条指令执行的结果为 \(0\),则 \(ZF=1\),否则 \(ZF=0\)
关于“上一步的结果”:8086cpu 中有一些指令会产生结果,比如 add,sub,mul,div,inc,dec,or,and(其中 inc 和 dec 只有可能影响 AF,OF,PF,SF,ZF 标志位,但不影响 CF 标志位);还有一些比如 mov,push,pop 不影响标志位
这一点在后面也会用上
7.2 PF
第 2 位,奇偶标志位
如果上一条指令执行的结果的二进制中有偶数个 \(1\),则 \(PF=1\),否则 \(PF=0\)
7.3 SF
第 7 位,符号标志位
如果上一条指令执行结果为负,\(SF=1\),如果非负,\(SF=0\)
和符号有关,就是要用到补码了,先去学补码再看下面内容会更名白一些
一个数据以二进制保存在计算机中,它既可以代表直接转换成十进制的数(无符号),也可以用补码来转换(有符号)
也就是说,cpu 执行一条指令的时候,已经有了两种含义(当成有符号执行和当成无符号执行),结果也有两种含义(有符号和无符号),虽然它们在计算机中的表达是一样的,把它当成有符号还是无符号是我们的“看待”
所以说,cpu 在执行一条有结果的指令时,必然影响到 SF 的值(当然是当作有符号运算来进行影响),而我们需不需要这个影响就另说了:比如我们对这个运算的“看待”就是无符号运算,那么 SF 受到的影响就是无用的,但 cpu 对 SF 的影响还是会有,只是我们此时不需要罢了
7.4 CF
第 0 位,进位标志位
两个 N 位数字运算时,有可能发生溢出,CF 记录的就是溢出的这一位(第 N 位)
当减法出现借位时,CF 也会记录借位值。比如一个八位减法 \(97H-98H\),发生借位,变成 \(197H-98H\),然后 \(CF=1\)
其实可以发现,一般来说这个 CF 也是对于无符号数的,但是如果我们把一个运算看作有符号的运算,cpu 执行指令对 CF 的影响仍然是存在的
7.5 OF
第 11 位,溢出标志位
溢出一般是对于有符号数来说的,就是如果运算过程中结果超过了机器所能表示的范围称为溢出
比如对于两个 8 位数的运算,\(98+99=197\),这个 \(197\) 就超过了 8 位数的表示范围 \([-128,127]\),发生了溢出
这样结果变成十六进制就是 \(0C5H\),又因为是有符号运算,所以它应该被按照补码的规则看作 \(-59\),发生了错误
这时就要用到 OF 了,如果上一个指令的结果发生了溢出,\(OF=1\),否则为零
注意:OF 是对有符号数有意义的标志位,而 CF 是对无符号运算有意义的
但即使一个标志位对当前的运算无意义,它也会被影响(cpu 不知道当前是有符号还是无符号)
7.6 在 debug 中查看标志寄存器
用 r
命令查看寄存器值时右下角会有一些字符:
7.7 adc 与 sbb
adc X,Y
就是 \(X=X+Y+CF\)
比如 adc ax,bx
,意义是 \((ax)=(ax)+(bx)+CF\)
那么这样一种指令的意义何在?比如当我们执行 add al,bl
后,\((al)=(al)+(bl)\),但这样以后 al 可能发生进位,那么会对应的记录到 CF 中,此时再调用 adc ah,bh
,就会在把 bh 的值加到 ah 上的同时,把 CF 也加到 ah 上
那么如果之前 al 进位,也就是 \(CF=1\),多了一个 \(100H\),加到 ah 上就是加一,也就是加 CF 的值(当然没进位 \(CF=0\) 也不会有问题)
所以 adc
的意义其实是使得更大数据的加法可以被支持,通过把 CF 的值加到高位上来解决低位出现进位的问题
比如下面这个程序,我们计算了 \(1EF000H+201000H\),并将结果存进了 ax:bx
;calc 1EF000H+201000H,result in ax:bx
assume cs:code
code segment
start:
mov ax,001EH
mov bx,0F00H
add bx,1000H
adc ax,0020H
mov ax,4c00h
int 21h
code ends
end start
同样,也可以实现下面这样的一个函数,来利用 adc
进行两个 128 位数据的相加
;两个 128 位数字相加,ds:si 指向第一个数,8 个字
;ds:di 指向第二个数,结果存在第一个数的位置
add128:
push ax
push cx
push si
push di
sub ax,ax;将 CF 设零
mov cx,8
S:
mov ax,[si]
adc ax,[di]
inc si
inc si
inc di
inc di;用 inc 而不是 add 来防止改变 CF
loop S
pop di
pop si
pop cx
pop ax
ret
再来说 abb
,abb X,Y
就是 \(X=X-Y-CF\)
比如 sbb ax,bx
,意义是 \((ax)=(ax)-(bx)-CF\)
现在类比上一个看这个指令,意义也很明确了,实现带借位的减法
这两个指令也体现出了 CF 存在的意义
7.8 cmp
比较指令,对标志寄存器的影响相当于减法指令,但是它不会改变参与减法运算的两个寄存器或内存单元的值(就是说只改变标志寄存器,不保存结果)
比如执行指令 cmp ax,ax
,执行后标志寄存器:\(ZF=1,PF=1,SF=0,CF=0,OF=0\),但 ax 以前是多少还是多少
如果执行 cmp ax,bx
,则:
如果 cmp 是对无符号数进行比较,那么上面的几条也可以倒推
但如果是有符号数,就稍微复杂一些了
首先前两条相等和不相等,当然还是一样
如果 \((ax)<(bx)\),则会引起 \(SF=1\),但是 \(SF=1\) 却不一定可以说明 \((ax)<(bx)\)
比如有符号 8 位减法:\(22H-0A0H=34-(-96)=82H=-126\text{(补码转换为原码)},CF=1\),但是 \(34>-96\)
什么情况下会出现这种问题?\(SF=1\) 并不完全等价于结果为负数(结果为负数我们一定能说明那个小于关系),因为就像上面那个例子,运算中发生了溢出,因此出现了这种情况,所以再经过一些简单分析,就可以得到:
- \(SF=1,OF=0\),没有溢出,此时 \(SF=1\) 就等价于结果为负,所以 \((ax)<(bx)\)
- \(SF=1,OF=1\),发生溢出,溢出导致了 \(SF=1\),也就是 cpu“以为”结果为负,那么实际上应该是结果为正,那么 \((ax)>(bx)\)
- \(SF=0,OF=0\),没溢出,\((ax)\ge (bx)\),注意由于 \(SF=0\) 这里是大于等于
- \(SF=0,OF=1\),溢出了,\((ax)<(bx)\),这后面两个都是同理
这里感觉比较容易迷惑,主要就是关注有符号数溢出,在原码上的表示超出范围,对应到补码上就是改变了符号
7.9 基于标志寄存器的跳转指令
其实就是通过上面讲述的 cmp
结果,对于无符号数,有这几种:
其中各个字母缩写的含义:not,equal,below,above,可能会帮助记忆
其实这个图稍微有一些歧义,要知道中间那一竖栏只是一个辅助的描述,比如如果你只执行一个 je
并不会直接起到中间竖栏的作用,而只是通过 ZF 的值来进行转移,只有当在 je
之前执行一个 cmp
,它才会起到“等于则跳转”的效果
也就是,这些指令都可以单独使用,根据标志寄存器跳转,但一般都是通过和 cmp
搭配使用来起到根据两数大小来跳转的作用
就好像 call
和 ret
一般搭配使用,但也可以单独拿出一个来用
然后,对于有符号数,原理上是一样的,只是检测的标志位不同,整理出了下面这一个和无符号数跳转指令的对应关系(以下同一个指令两种助记符用斜杠隔开,其实可以发现它们是有规律的)
无符号 | 有符号 | 何时跳转 |
---|---|---|
je / jz | je / jz | 等于 |
jne / jnz | jne / jnz | 不等于 |
jb / jnae | jl / jnge | 低于 |
jnb / jae | jnl / jge | 不低于(大于等于) |
ja / jnbe | jg / jnle | 高于 |
jna / jbe | jng / jle | 不高于(小于等于) |
例子
可以用如下程序检测 ds:si 开始的一些数据中有几个 8
assume cs:code,ds:datasg;看 datasg 里有多少数等于 8,结果存 ax
datasg segment
db 8,11,8,1,8,5,63,38
datasg ends
code segment
start:
mov ax,datasg
mov ds,ax
xor bx,bx
xor ax,ax
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
同理,也可以统计有多少个大于,小于,不等于 8
7.10 DF 和串传送指令
第 10 位,方向表示位,串处理指令中,控制每次 di 和 si 的加减,\(DF=0\),就加,否则就减
串传送指令:movsb
或 movsw
相当于每次把 es:si 处的数据送入 ds:di 中,每次的长度分别是 byte 和 word
然后每次传送完以后变更 si 和 di,就依靠 DF 的值
它可以和 rep
配合使用,rep movsb
就相当于:
S: movsb
loop S
所以要提前设置 cx
用 cld
和 std
分别将 DF 置为 \(0\) 和 \(1\)
下面就是一个例子
assume cs:code,ds:datasg;把 datasg 第一个 16 字节传送到第二个 16 字节
datasg segment
db 'Welcome to masm!'
db 16 DUP(0)
datasg ends
code segment
start:
mov ax,datasg
mov ds,ax
mov es,ax
xor si,si
mov di,16
mov cx,8
cld
rep movsw
mov ax,4c00h
int 21h
code ends
end start
7.11 pushf 和 popf
分别是把标志寄存器的值入栈、出栈。这也是一种可以直接访问标志寄存器的方法
7.12 实例
写一个大小写转换的子程序,小写转大写,但是转换的字符串不一定都是字母,要提前判断
assume cs:code,ds:datasg
datasg segment
db "Beginner's All-purpose Symbolic Instruction Code.",0
datasg ends
code segment
start:
mov ax,datasg
mov ds,ax
xor si,si
call letterc
mov ax,4c00h
int 21h
letterc:;ds:si 指向的以 0 结尾的字符串中小写字母转成大写
push ax
push cx
push si
do:
mov al,[si]
cmp al,'a'
jb no
cmp al,'z'
ja no
and al,11011111B
mov [si],al
no:
inc si
mov cx,[si]
inc cx
loop do
pop si
pop cx
pop ax
ret
code ends
end start