汇编语言:标志寄存器
- flag 寄存器
- 标志位
- 零标志位 ZF
- 奇偶标志位 PF
- 符号标志位 SF
- 进位/借位标志 CF
- 溢出标志 OF
- x86 中的标志位
- 进位/借位的加/减法
- adc 指令
- sbb 指令
- flag 寄存器的访问
- 参考资料
flag 寄存器
在 8086CPU 中还有一类不同于ax、cs 这类整体存放数据的 flag 寄存器,这类寄存器按位起作用,每一位都记录特定的状态信息。它们存储的信息通常被称为程序状态字(PSW)。8086 CPU 的 flag 结构有 16 位,其中 0、2、4、6、7、8、9、10、11 这 9 位是具有实际含义的。

这 9 个标志位大致可以分为两类:
| 类型 | 标志位 | 作用 |
|---|---|---|
| 反映运算结果 | ZF、PF、SF、CF、OF | 记录结果是零、奇偶、正负、进位、溢出 |
| 控制 CPU 行为 | DF、IF、TF | 控制串操作方向、中断响应等 |
在 Debug 里,flag 不会显示成一串 0000010101010101B,而是按位拆成单个标记。在 Debug 中执行 r 命令,会看到类似这一行:
NV UP EI PL NZ NA PO NC
8 组字母的每一组对应一个标志位,前一个是值为 0 的标记,后一个是值为 1 的标记:
| 标志位 | 值为 1(真) | 值为 0(假) | 含义 |
|---|---|---|---|
| OF | OV | NV | 溢出 |
| DF | DN | UP | 方向(减/增) |
| IF | EI | DI | 中断允许 |
| SF | NG | PL | 符号(负/正) |
| ZF | ZR | NZ | 零 |
| AF | AC | NA | 辅助进位 |
| PF | PE | PO | 奇偶 |
| CF | CY | NC | 进位 |
标志位
零标志位 ZF
flag 的第 6 位为 ZF(Zero Flag,零标志位),记录用于指令执行结果是否为 0,若结果为 0 则 ZF = 1,若结果不为 0 则 ZF = 0。例如以下两条指令执行后 AX = 0,CPU 发现结果是 0 就把 ZF 置 1。
mov ax, 1
sub ax, 1

mov 等传送指令不影响标志位,只有,例如 add、sub、mul、div、and、or 等的运算类指令才会改变 ZF。
奇偶标志位 PF
flag 第 2 位是 PF(Parity Flag,奇偶标志位),用于记录结果的所有二进制位中“1 的个数”是否为偶数。若 1 的个数为偶数则 PF = 1,若 1 的个数为奇数则 PF = 0。
mov al, 1 ; AL = 00000001B,1 个 1 → PF = 0
add al, 1 ; AL = 00000010B,1 个 1 → PF = 0
add al, 1 ; AL = 00000011B,2 个 1 → PF = 1

PF 主要用于早期通信中的奇偶校验,现在更多是作为 CPU 行为的副产品存在。
符号标志位 SF
flag 第 7 位是 SF(Sign Flag,符号标志位),它用于记录结果的最高位(符号位)是否为 1。若结果为负(最高位为 1)则 SF = 1,若结果非负(最高位为 0)则 SF = 0。由于计算机使用补码表示有符号数据,因此同一串二进制既可以是正数,也可以是负数。例如如下指令执行后 AL = 10000010B(82H):
mov al, 10000001B ; 81H
add al, 1
如果当作无符号数,则 129(81H) + 1 = 130(82H),SF 的值没有意义;如果当作有符号数(补码),则是 -127 + 1 = -126,最高位是 1、SF = 1。

例如如下指令执行后 AL = 10000010B(81H):
mov al, 10000001B ; 81H
add al, 01111111B ; 7fH
执行后结果为 0、sf = 0,表示如果指令进行的是有符号运算,则结果为非负。

SF 是 CPU 对有符号数运算结果的指示,它总是会被设置,但只有在把数据当有符号数看时 SF 才有逻辑意义。
进位/借位标志 CF
CF(Carry Flag,进位/借位标志)位于 flag 的第 0 位,它记录的是无符号数运算中,最高有效位向更高位的进位或借位。8086 中的一个 8 位寄存器最多能表示 0~255,一个 16 位寄存器最多能表示 0~65535。做加法时如果结果超过了这个范围,CPU 会将这个进位值记录在 CF 标志上,而不是直接丢弃它。例如对于以下指令:
mov al, 98H ; AL = 98H = 152(无符号)
add al, al ; AL = 98H + 98H = 130H
从 8 位视角看 AL 里只剩下 30H,但 CPU 知道把那个溢出去的 1记在 CF 中,执行后得到 AL = 30H、CF = 1。

减法同理,此时 CF 的含义变成了借位:
mov al, 97H ; AL = 97H = 151(无符号)
sub al, 98H ; 151 - 152,不够减,向更高位借 1
CPU 实际算的是 197H - 98H,借位被记录在 CF 中,执行后得到 AL = FFH、CF = 1。

溢出标志 OF
OF(Overflow Flag,溢出标志)位于 flag 的第 11 位,它记录的是有符号数运算结果是否超出了机器能表示的范围。机器能表示的范围指的是补码的范围,8 位补码是 -128 ~ 127,16 位补码是 -32768 ~ 32767,一旦结果超出这些范围 OF 就会被置 1。补码是用固定位数表示正负的,位数不够时就会产生溢出,例如以下指令:
mov al, 63 ; AL = 99(有符号)
add al, 62 ; 98 + 99 = 197
计算结果 197 超出了 8 位有符号数的上限 127,因此 AL = 0C5H(-59 的补码)、OF = 1。

以下指令计算 -16 + (-120):
mov al, 0F0H ; AL = -16(有符号)
add al, 88H ; -16 + (-120) = -136
得数 -136 超出了 8 位有符号数的下限 -128,因此 AL = 78H(120 的补码),OF = 1

CF 与 OF 的功能有一定相似之处,但它们描述的是**同一个运算中两种不同的“正确性”,之间没有任何关系。
| 标志位 | 服务于 | 关心什么 | 不关心什么 |
|---|---|---|---|
| CF | 无符号数 | 有没有进位 / 借位 | 结果是正还是负 |
| OF | 有符号数 | 结果有没有超出补码范围 | 有没有进位 |
例如以下 3 组指令,有进位时,OF 可以 1 也可以 0;有溢出时,CF 可以 1 也可以 0,它们是独立的。
| 指令 | 无符号 | CF | 有符号 | OF |
|---|---|---|---|---|
mov al,98 / add al,99 |
152+99,无进位 | 0 | 98+99 溢出 | 1 |
mov al,0F0H / add al,88H |
240+136,有进位 | 1 | -16-120 溢出 | 1 |
mov al,0F0H / add al,78H |
240+120,有进位 | 1 | -16+120 不溢出 | 0 |
而 CPU 不知道你要把数据当有符号还是无符号,所以要同时记录 CF 和 OF。执行完运算指令后,CPU 通过 flag 寄存器就同时知道如下信息:
- 如果这是无符号加法,有没有进位?(CF)
- 如果这是有符号加法,有没有溢出?(OF)
- 结果是正还是负?(SF)
- 结果是不是 0?(ZF)
至于程序里到底用的是哪一种,那是编程时要考虑的事。CPU 只负责把可能的事实都记下来,放在 flag 里待汇编程序使用。
x86 中的标志位
在现代 x86 架构中,EFLAGS/RFLAGS 寄存器继承了 8086 flag 的所有位,如 CF、ZF、SF、OF 依然存在,在此基础上还进行了大幅扩展。编译器在生成代码时,会充分利用这些标志位进行分支优化,例如 C 语言的分支结构:
if (a == 0) { ... }
编译后通常会变成如下指令,其中的 je 就是依赖 ZF 标志位。
cmp eax, 0
je .L1
编译器在生成代码时会严格区分 CF 和 OF,在底层它们对应不同的条件跳转指令。例如以下这段代码编译器可能基于 CF 判断溢出:
unsigned int a, b, c;
c = a + b;
以下这段代码编译器可能基于 OF 判断溢出:
int a, b, c;
c = a + b;
进位/借位的加/减法
adc 指令
adc 的全称是 add with carry,它的功能是利用 CF 上记录的进位值实现带进位的加法。该指令的格式为 adc 操作对象1, 操作对象2,其计算结果为:操作对象1 + 操作对象2 + CF,多出来的 + CF 就是上一次运算留下的进位(或借位)。CF 不是 adc 自己算出来的,而是前面那条指令“留给它的”。例如考虑如下指令:
mov ax, 2
mov bx, 1
sub bx, ax ; cf = 1
adc ax, 1 ; ax = ax + 1 + CF
指令 sub bx, ax 发生了借位,将 CF 设置为 1,然后 adc ax, 1 把 CF 当成额外加数一起算进去,最终 ax = 2 + 1 + 1 = 4。

8086 的寄存器只有 16 位,一次最多处理 16 位数据。但现实中的数往往更大,比如 1EF000H + 201000H,已经超过 16 位了。此时 CPU 的解决方案类似于列竖式,第一步先加低位 98 + 83 = 11B,产生进位 1;第二步,接着再加高位 01 + 01 + 进位 1 = 03。
01 98
+ 01 83
-------03 1B
adc 的效果就类似该过程的第二步,使用该指令就可以实现大数加法。例如计算 32 位加法 1EF000H + 201000H,用 ax 保存结果的高 16 位,bx 保存低 16 位,指令如下:
mov ax, 001EH
mov bx, 0F000H
add bx, 1000H
adc ax, 0020H
指令首先使用 add 对低 16 位相加,此时产生进位设置 CF = 1。然后使用 adc 将高 16 位和进位相加,得到计算结果

48 位加法同理,例如计算 1EF0001000H + 2010001EF0H,令结果为 ax = 最高 16 位,bx = 次高 16 位,cx = 低 16 位,指令如下:
mov ax, 001EH
mov bx, 0F000H
mov cx, 1000H
add cx, 1EF0H ; 低16位相加
adc bx, 1000H ; 次高16位 + 进位
adc ax, 0020H ; 最高16位 + 进位
也就是最低 16 位用 add,之后每一层都用 adc 把上一层的进位加上。

同理,考虑一个 128 位加法子程序。因为第一次加法不需要任何进位,必须从 CF = 0 的状态开始,因此使用 sub ax, ax 把 CF 清零。使用 inc si 而不是 add si, 2 的原因在于 inc 指令不影响 CF,但是用 add si, 2 可能会修改 CF,从而破坏上一层 adc 留下的进位,这样程序的计算结果可能错误。loop 不影响 CF,所以可以令进位传递到下一轮。
; 功能:128 位加法
; 参数:
; ds:si 指向第一个数(8 个字,低地址到低字节)
; ds:di 指向第二个数
; 结果:存储在第一个数位置add128:push axpush cxpush sipush disub ax, ax ; 将 CF 清零mov cx, 8
s:mov ax, [si]adc ax, [di]mov [si], axinc siinc siinc diinc diloop spop dipop sipop cxpop axret
sbb 指令
sbb 是 subtract with borrow,和 adc 是对称的,它的格式和功能是:sbb 操作对象1, 操作对象2 = 操作对象1 − 操作对象2 − CF。例如计算 32 位减法 003E1000H - 00202000H,结果令 ax = 高 16 位,bx = 低 16 位,指令如下:
mov bx, 1000H
mov ax, 003EH
sub bx, 2000H
sbb ax, 0020H
sbb 的使用方式和 adc 一样,只是把加法换成减法,进位换成借位。指令中的 sub 令低 16 位相减并设置 CF = 1(借位),然后再用 sbb 指令实现高 16 位 - 借位。

现代 CPU 中 adc / sbb 依然存在,而且在大整数库(GMP、OpenSSL)中发挥了重要作用。现代 x86 还引入了 adcx / adox 这类指令,可以同时使用 CF 和 OF 两个标志位,用于高性能加密运算。
flag 寄存器的访问
adc、sbb 等指令都是在使用 flag 寄存器,但如果想知道 flag 里的具体数值,或者想人为设置一个 flag 状态,需要使用 8086 提供的两条专用的指令:
| 指令 | 功能 |
|---|---|
pushf |
把整个 flag 寄存器压入栈 |
popf |
从栈顶弹出一个字,写入 flag 寄存器 |
例如对于如下一段指令:
mov ax, 0
push ax
popf
mov ax, 0fff0h
add ax, 0010h
pushf
pop ax
and al, 11000101B
and ah, 00001000B
push ax 把 0000H 压栈,popf 把 0000H 写入 flag,此时 flag 所有位被清零。

然后执行加法 FFF0H + 0010H = 10000H,结果为 AX = 0000H。

由于结果为 0 所以 ZF = 1;产生了从最高位的进位,因此 CF = 1;低 8 位全为 0,1 的个数为 0,此时为偶数所以 PF = 1;最高位为 0 是非负数,所以 SF = 0;这个加法为 -16 + 16 = 0,没溢出产生溢出所以 OF = 0。此时 flag 中关键位的状态是:
| 位 | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 标志 | - | - | - | - | OF | DF | IF | TF | SF | ZF | - | AF | - | PF | - | CF |
| 值 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 |
此时 flag = 0000 0000 0101 0101B = 0055H,然后 pushf 把当前 flag (0055H) 压栈,pop ax 把 0055H 弹入 AX。最后使用 and 指令,对于低 8 位 AL: 01010101B AND 11000101B = 01000101B = 45H;对于高 8 位 AH:00H AND 08H = 00H;最终结果为 AX = 0045H。

参考资料
《汇编语言(第四版)》,王爽 编著,清华大学出版社