标志位寄存器

⚠ 转载请注明出处:作者:ZobinHuang,更新日期:July 16 2021


知识共享许可协议

    本作品ZobinHuang 采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可,在进行使用或分享前请查看权限要求。若发现侵权行为,会采取法律手段维护作者正当合法权益,谢谢配合。


目录

有特定需要的内容直接跳转到相关章节查看即可。

    Section 1. 标志位寄存器:介绍了 8086 CPU 的标志位寄存器的作用和状态

    Section 2. adc 指令:介绍了用于传递进位的 adc 指令

    Section 3. sbb 指令:介绍了用于传递借位的 sbb 指令

    Section 4. cmp 指令:介绍了用于比较的 cmp 指令
        4.1 无符号数的比较:介绍了无符号数下 cmp 指令的输出结果
        4.2 有符号数的比较:介绍了有符号数下 cmp 指令的输出结果

    Section 5. 基于标志位寄存器的条件转移指令:介绍了基于标志位寄存器的条件转移指令 je, jb, jnb, ja, jna

    Section 6. 串传送指令:介绍了串传送指令以及 DF 寄存器的作用

    Section 7. pushf 和 popf:简单介绍了压栈/出栈标志位寄存器的 pushf 和 popf 指令

1. 标志位寄存器

bit 序号 F E D C B A 9 8 7 6 5 4 3 2 1 0
寄存器位 OF DF IF TF SF ZF AF PF CF

    在 8086 CPU 中,有一个特殊的 16-bits 寄存器:标志位寄存器。标志位寄存器中的某些位有特殊的含义,如上所示,它们主要是用于给出一些针对计算指令 (e.g. add, sub. div, mul) 的信号。我们列举如下所示:

标志位寄存器位 真值 假值 含义
CF
(Carry Flag 进位标志位)
CY
(Carry Yes)
NC
(Not Carry)
CF 把运算结果都当作 无符号数,它会记录运算结果的最高有效位向更高位的进位值 (add),或者从更高位的借位值 (sub)
ZF
(Zero Flag 零标志位)
ZR
(Zero)
NZ
(Not Zero)
ZF 用于记录相关运算指令执行之后,其结果是否为 0 的标志
PF
(Parity Flag 奇偶标志位)
PE
(Parity Even)
PO
(Parity Odd)
PF 用于记录相关指令执行后,运算结果中所有的 bit 位的 1 的奇偶性。若有偶数位为 1 则 PF 为真;若有奇数位为 1 则 PF 为假。
SF
(Sign Flag 符号标志位)
NG
(Negative)
PL
(Plus)
SF 把运算结果都当作 有符号数。它用于记录相关指令执行后,运算结果的正负情况。若计算结果为负,则 SF 为真;若计算结果为正,则 SF 为假。
OF
(Overflow Flag 溢出标志位)
NG
(Negative)
PL
(Plus)
OF 把运算结果都当作 有符号数。如果计算结果超过了用于存储数据的 8 位寄存器/内存单元的表示范围 (-128~127) 或者 16位寄存器/内存单元的表示范围 (-32768~32767),则说明运算溢出。如果发生了溢出,则 OF 为真;如果没有发生溢出,则 OF 为假。
DF
(Direction Flag 方向标志位)
UP
(Up)
DW
(Down)
在串传送指令中,控制 SI 寄存器和 DI 寄存器的增长方向,详见 串传送指令

    在上面这个表中我们发现,有些标志寄存器把运算结果当作有符号数,有些标志位寄存器则把运算结果当作无符号数,这显得有些困惑,这里我们解释一下。首先,如何看待运算结果,这是程序员自己决定的。我们完全可以把 10000001B 当作 129 来看待,也可以当作 -127(补码) 来看待。当我们把运算结果当作无符号数时,关心 CF 寄存器位就是有意义的;当我们把运算结果当作有符号数时,关心 SF,OF 寄存器就是有意义的。计算机二进制底层设计中最神奇的一点就是,不论我们怎么看待数据,不管我们认为一个数是有符号数还是无符号数,ALU (算数逻辑单元) 都能够计算出我们对有无符号设定下的正确的结果。

2. adc 指令

    adc (add carry) 指令与 add 指令的区别就在于它会自动地加上 CF 寄存器位的值,adc ax, bx 即 ax + bx + CF。CF 寄存器位在这里的含义是加法进位的意思,其值是由先前的一条加法指令决定的,也就是说 adc 指令可以用于延续先前的一条加法计算,也就是传递进位的功能。如下所示,我们使用 adc 命令实现了对 1EF0001000H + 2010001EF0H 的计算。

1
2
3
4
5
6
7
mov ax, 001EH
mov bx, F000H
mov cx, 1000H

add cx, 1EF0H
adc bx, 1000H
adc ax, 0020H

3. sbb 指令

    sbb 指令与 sub 指令的区别就在于它会自动减掉 CF 寄存器位的值,sbb ax, bx 即 ax - bx - 1。CF 寄存器位在这里的含义是减法借位的意思,其值也是由先前的一条减法指令决定的,也就是说 sbb 指令可以用于延续先前的一条减法计算,也就是传递借位的功能。如下所示,我们使用 sbb 命令实现了对 003E1000H - 00202000H 的计算。

1
2
3
4
5
mov bx, 1000H
mov ax, 003EH

sub bx, 2000H
sbb ax, 0020H

4. cmp 指令

    cmp 指令用于比较两个操作数的大小关系,并最终把结果呈现在标志位寄存器中。cmp 指令的运行过程实质上就是把两个操作数进行相减。

    下面我们分别分析有符号数和无符号数的情况下 cmp 输出的标志位的含义。再次强调,这里的有符号数和无符号数,是从程序员的角度出发的,当我们把操作数看作无符号数的时候,我们就应该去那些能够体现无符号数运算结果的标志位中提取信息,反之我们就应该去那些能够体现有符号数运算结果的标志位中提取信息。

4.1 无符号数的比较

    无符号数的比较较为简单,如下所示:

大小关系 cmp (减法) 关系 标志位寄存器输出
ax = bx ax - bx = 0 ZF = 1
ax `\ne` bx ax - bx `\ne` 0 ZF = 0
ax < bx ax - bx < 0 减法将产生借位,故 CF = 1
ax `\leq` bx ax - bx `\leq` 0 减法可能产生借位,又有可能减法为 0,故 ZF = 1 或 CF = 1
ax > bx ax - bx > 0 减法不可能有借位,结果也不可能为 0,故 ZF = 0 且 CF = 0
ax `\geq` bx ax - bx `\geq` 0 减法不必产生借位,故 CF = 0

4.2 有符号数的比较

    当我们把数据看作是有符号数时,我们必须明确,输入输出此时都得当作有符号数来看待,且输出的数有可能超出了当前有符号数的表示范围。有符号数的比较会稍微复杂一点,其原因我们通过一个例子来理解:

    考虑比较 22H(十进制:34) 和 A0H(十进制:-96)。对于实际 cmp 计算结果,我们得到:22H - A0H = 82H,82H 的十进制是 -126。但是我们把这个减法转化为十进制:34 - (-96) = 130,其结果已经超出了 8 位有符号数能够表示的范围:-128~127。因此我们得到了一个错误的结果。我们把 82H 叫做实际结果,130 叫做逻辑结果

    这样一来,当 82H 的结果映射到标志位寄存器时,我们能够做些什么来获取 cmp 的正确结果呢?基于 "cmp ax, bx",下面我们给出答案:

标志位寄存器输出 说明 大小关系
SF = 1, OF = 0 运算没有溢出,减法操作过后实际结果为负 ax < bx
SF = 1, OF = 1 运算发生溢出,减法操作过后实际结果为负。如果因为溢出导致了实际结果为负,那么逻辑结果必然为正 ax > bx
SF = 0, OF = 1 运算发生溢出,减法操作过后实际结果为正。如果因为溢出导致了实际结果为正,那么逻辑结果必然为负 ax < bx
SF = 0, OF = 0 运算没有溢出,减法操作过后实际结果为正 ax `\geq` bx

5. 基于标志位寄存器的条件转移指令

    我们在 转移指令 JMP, JCXZ, LOOP, RET 和 CALL 一文中介绍了一些基于寄存器的条件转移指令。在本节中我们将介绍基于标志位寄存器的条件转移指令。

    注意到本节给出的条件转移指令仅适用于无符号数的比较,原因很简单,因为这些导致这些转移指令触发转移的标志位是用于无符号数比较的寄存器位。这些命令的用法是 "je/jne/jb/jnb/ja/jna + [转移标号]"。

指令 含义 转移条件
无符号数比较
je 等于则转移 ZF = 1
jne 不等于则转移 ZF = 0
jb 低于则转移 CF = 1
jb 小于等于则转移 CF = 1 或 ZF = 1
jnb 不低于则转移 CF = 0
jnbe 不小于等于则转移 CF = 0 且 ZF = 0
ja 高于则转移 CF = 0 且 ZF = 0
jae 大于等于则转移 CF = 0
jna 不高于则转移 CF = 1 或 ZF = 1
jnae 不大于等于则转移 CF = 1
有符号数比较
jl 有符号数比较,小于则转移 SF `\ne` OF
jle 有符号数比较,小于等于则转移 ZF = 1 或 SF `\ne` OF
jnl 有符号数比较,不小于则转移 SF = OF
jnle 有符号数比较,不小于等于则转移 ZF = 0 且 SF = OF (i.e. 都为 0 或都为 1)
jng 有符号数比较,不大于则转移,等同于 "小于等于则转移" ZF = 1 或 SF `\ne` OF
jnge 有符号数比较,不大于等于则转移,等同于 "小于则转移" SF `\ne` OF
jg 有符号数比较,大于则转移 ZF = 0 且 SF = OF (i.e. 都为 0 或都为 1)
jge 有符号数比较,大于等于则转移 SF = OF (i.e. 都为 0 或都为 1)
其它
jpe, jp 校验为偶则转移 PF = 1
jpo, jnp 校验为奇则转移 PF = 0
jo 溢出则转移 OF = 1
jno 没有溢出则转移 OF = 0
jc 借位/进位则转移 CF = 1
jnc 没有借位/进位则转移 CF = 0
js 为负数则转移 SF = 1
jns 为正数则转移 SF = 0
jz 为零则转移 ZF = 1
jnz 不为零则转移 ZF = 0

    我们现在来看一个例子:将 data segment 中的字符串的小写字母转化为大写字母,并且在屏幕上显示出来。我们的程序如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
assume cs:code, ds:data, ss:stack

data segment
# 显示的字符串
# 0123456789ABCDEF
db 'Hello This Is Zobin Huang', 0

data ends

stack segment stack
db 128 dup (0)
stack ends


code segment
start: call init_reg
call init_data

call show_str

call up_letter
mov di, 160*11+20*2
call show_str

mov ax, 4C00H
int 21H

#================================================
init_data: mov si, 0
mov di, 160*10+20*2

ret

#================================================
up_letter:
push dx
push ds
push es
push si
mov si, 0

up_one: mov dl, ds:[si]
cmp dl, 0
je up_letter_ret
cmp dl, 'a'
jb next_letter
cmp dl, 'z'
ja next_letter
and byte ptr ds:[si], 11011111B # 把 ASCII 码转化为大写的与操作
next_letter: inc si
jmp up_one

up_letter_ret: pop si
pop es
pop ds
pop dx
ret

#================================================
show_str: push dx
push ds
push si
push di
push es

show_char: mov dl, ds:[si]
cmp dl, 0
je ret_show_str
mov es:[di], dl
add di, 2
inc si
jmp show_char

ret_show_str: pop es
pop di
pop si
pop ds
pop dx
ret

#================================================
init_reg: #设置数据段
mov ax, data
mov ds, ax

#设置显存区域位置
mov bx, 0B800H
mov es, bx
ret

code ends

end start

    我们在 Line 44, Line 46, Line 48 处使用了 "比较+跳转" 的命令。程序运行的结果如下:

6. 串传送指令

    串传送指令 movsb 的功能是把 DS:[SI] 处的值复制到 ES:[DI] 中,然后当 DF = 0 时令 SI = SI + 1 和 DI = DI + 1,当 DF = 1 时令 SI = SI - 1 和 DI = DI - 1。

    然后我们再介绍 rep 指令,其功能室循环执行其后面跟随的指令,循环的次数和 loop 指令一样由 CX 寄存器决定。这样一来 rep movsb 指令就可以被理解为将 DS:[SI] 处的若干数据,复制到 ES:[DI] 中去,复制的数据的方向由 DF 标志位寄存器来决定。当 DF 为 0 时,复制的数据方向是 SI, DI 自增的方向;当 DF 为 1 时,复制的数据方向是 SI, DI 自减的方向。

    控制 DF 标志位寄存器的指令是: cld 设置 DF 为 0,std 设置 DF 为 1。

    我们现在通过下面的例子来理解一下程序的执行过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
code segment
start: call init_reg

call cpy_data

mov ax, 4C00H
int 21H

#================================================
ex_code: mov ax, 1000H
mov ax, 1000H
mov ax, 1000H
mov ax, 1000H
ex_code_end: nop

#================================================
cpy_data: mov bx, cs
mov ds, bx
mov si, OFFSET ex_code

mov bx, 0
mov es, bx
mov di, 7E80H

mov cx, OFFSET ex_code_end-ex_code
cld
rep movsb

ret

#================================================
init_reg: #设置数据段
mov ax, data
mov ds, ax

#设置显存区域位置
mov bx, 0B800H
mov es, bx
ret

code ends

end start

    下面是我们观测到的程序运行过程中,SI 寄存器和 DI 寄存器的递增过程,以及循环寄存器 CX 的递减的过程。当 CX 寄存器为 0 时,循环就结束了。在这个过程中,我们看到 DF 寄存器的状态是 UP 状态,与 SI 和 DI 的递增方向一致。

    除了挨个字节地进行复制,8086 汇编也提供了复制一个字型数据的串传送指令:movsw。相应地,在执行 movsw 的时候,SI 和 DI 寄存器每次递增/递减的跨度就会变为 2 个字节,这里不再赘述。

7. pushf 和 popf

    pushf 用于将标志寄存器的 值压入栈中,popf 用于从栈中弹出数据并送入标志寄存器中。