CF 和 OF 标志位

看汇编语言时看到,标志寄存器中 CF 标志位表示无符号数运算时是否向最高有效位外的更高位产生进位或借位,而 OF 标志位表示有符号数运算时是否产生溢出。这里存在两个疑问:

  1. 对于 CPU 来说,它并不区分处理的是无符号数还是有符号数,那什么时候设置 CF,什么时候设置 OF
  2. CF 表示进位时也是一种溢出,能否和 OF 共用一个

CF

首先来看 CF 进位的例子,这里我们以8位无符号数为例,其最大值为255,那么计算 255 + 1 则会产生进位。可以通过一段简单的汇编代码进行验证:

1
2
3
4
5
6
7
8
.section .text
.globl _start
_start:
mov $255, %al
add $1, %al
movl $1, %eax
movl $0, %ebx
int $0x80

在上述代码中,al 是一个8位寄存器,是 eax 寄存器的低8位,这里首先将255放到 al 寄存器内,然后对 al 寄存器中的值加1并放回到 al 寄存器中,即实现 255 +1 的运算。

最后的 int $0x80 中的 int 表示 interrupt,即中断,当发生一个中断时会有一个与之对应的中断处理程序来处理,这里的 $0x80 就是声明由哪个中断处理程序处理,在 Linux 中,$0x80 对应的是操作系统内核,用于发起一个系统调用,而具体发起哪个系统调用则由 eax 中的值决定,这就是 movl $1, %eax 的作用,1对应的系统调用是 exit,用于退出程序,而程序退出时会伴有一个状态码,这个状态码的值来自于 ebx,也就是 movl $0, %ebx 的作用,这里使用0来表示程序正常退出。

接下来我们借助 gdb 来观察程序运行时 CF 的值的变化。首先将上述代码保存为 demo.s 后进行编译:

1
as demo.s -o demo.o -gstabs+

这里的 -gstabs+ 表示生成机器码时同时生成调试信息,如果没有这个选项后续 gdb 加载时会提示 (No debugging symbols found in ./demo)

然后进行链接:

1
ld demo.o -o demo

这个时候就可以通过 gdb 加载生成的可执行文件:

1
gdb ./demo

alt

然后输入 break 4 在代码第四行设置一个断点,即 mov $255, %al 处,最后输入 run 开始调试执行:

alt

此时可输入 layout reg 来观察各寄存器内的值,我们需要关注的是 eflags 寄存器,它展示了哪些标志位生效了:

alt

或者通过执行 info registers eflags 来查看 eflags 的值:

1
2
(gdb) info registers eflags
eflags 0x202 [ IF ]

目前只有一个 IF 标志位,它用于表示是否响应中断。

接着,输入 next 来执行当前断点所在处的指令,可以看到,执行后 rax 寄存器内的值变成了255(rax 是64位 CPU 下的一个通用寄存器,32位 CPU 下对应为 eax):

alt

再输入一次 next 来执行加法运算,此时 rax 中的值变为了0(实际的二进制结果应该是100000000,因为 al 寄存器最多只能表示8位,所以最高位的1无法表示,最终结果为0),eflags 中出现了 CF 标志位,说明发生了进位:

alt

rax 中的值为0也说明了加法运算后产生的进位并不会体现在比参与运算的寄存器位数更多的寄存器中,否则 rax 中的值应该是256。

再来看借位,将程序稍加修改执行一个 1 - 2 的运算:

1
2
3
4
5
6
7
8
.section .text
.globl _start
_start:
mov $1, %al
sub $2, %al
movl $1, %eax
movl $0, %ebx
int $0x80

最后 rax 中的值为255(存在高位借位的情况下最后的二进制结果为11111111,解释为无符号数为255),eflags 中同样出现了 CF 标志位。

alt

所以,CF 的标记取决于两个二进制数的运算是否产生进位或借位。

OF

有符号数的溢出分两种情况,一种是运算结果应该是正数却返回负数,另一种是运算结果应该是负数却返回正数。

首先来看两个正数运算得到负数的例子,同样对代码稍加修改实现 127 + 1 的运算:

1
2
3
4
5
6
7
8
.section .text
.globl _start
_start:
mov $127, %al
add $1, %al
movl $1, %eax
movl $0, %ebx
int $0x80

最后 rax 中的值为128(对应二进制表示为10000000,以有符号数的角度来看,其值为-128,即两个正数相加得到一个负数),eflags 中出现了 OF 标志位,说明发生了溢出:

alt

从有符号数的角度来看,参与运算的两个数的符号位都是0,相加后符号位却是1,所以 OF 设置为1。

再来看两个负数运算得到正数的例子,再次对代码稍加修改实现 -128 - 1 的运算,-128的二进制补码表示为10000000,即无符号数角度下的128,-1的二进制补码表示为11111111,即无符号数角度下的255:

1
2
3
4
5
6
7
8
.section .text
.globl _start
_start:
mov $128, %al
add $255, %al
movl $1, %eax
movl $0, %ebx
int $0x80

最后 rax 中的值为127(对应二进制表示为01111111,以有符号数的角度来看,其值为127,即两个负数相加得到一个正数),eflags 中出现了 OF 标志位,说明发生了溢出:

alt

从有符号数的角度来看,参与运算的两个数的符号位都是1,相加后符号位却是0,所以 OF 设置为1。

所以,OF 的标记取决于运算结果的符号位是否发生变化,这里的变化指的是两个相同符号位的数的运算结果是一个不同符号位的数。

比较

注意到前面有符号数 -128 - 1 运算的例子中,最后 CFOF 都被设置为了1,说明 CFOF 并不是互斥的关系,在这个例子中即发生了进位又发生了符号位的变更,也就是说如果满足了设置 CF 的条件,那么 CF 就是1,如果满足了设置 OF 的条件,那么 OF 就是1。因此,回到文章开头的问题,CPU 并不是去判断该设置 CF 还是 OF,而是只要条件满足就会设置对应的标志位,而具体应该关注哪个标志位,则交由编译器去判断,因为对 CPU 而言它处理的只是比特运算,只有编译器知道当前的运算数是无符号数还是有符号数。

另外,CFOF 也不能合二为一,无法相互替代,例如两个无符号数相加 CF 有可能是0,但是 OF 却是1,如 127 + 1;两个有符号数相加 OF 有可能是0,但是 CF 却是1,如 -1 - 1。也有可能 CFOF 都是1,如有符号数运算 -128 - 1

参考