【专业课学习】FLAGS register知识点整理

1,196 阅读10分钟

108001170_p0.jpg

2333.png

常见的标志位

CF标志位

Case1: 无符号数加法产生进位

unsigned int a = 0xff'ff'ff'ff;
00007FF7C56F182B  mov         dword ptr [a],0FFFFFFFFh  
unsigned int b = 1;
00007FF7C56F1832  mov         dword ptr [b],1  
unsigned int c = a + b;
00007FF7C56F1839  mov         eax,dword ptr [b]  
00007FF7C56F183C  mov         ecx,dword ptr [a]  
00007FF7C56F183F  add         ecx,eax  
00007FF7C56F1841  mov         eax,ecx  
00007FF7C56F1843  mov         dword ptr [c],eax

在执行add ecx,eax这条指令后,由于产生了进位,CF标志位(Visual Studio中为CY)被强制置为1.

Case2: 无符号数减法产生借位

首先来看一下直观的理解:

在处理两个无符号数的减法运算时,CF标志位作为借位标志(Borrow Flag)来使用。CF标志位用于指示在无符号算术运算中是否发生了借位或者说是否需要借位。

具体到减法操作:当执行减法操作时,如果被减数小于减数,即无法直接完成减法而需要借位时,CF会被置为1。这表示发生了借位,或者说结果需要从更高位借位才能表示。反之,CF会被置为0,这表示操作可以顺利完成,无需任何借位。

例子如下:

unsigned int a = 1;
00007FF7363318BB  mov         dword ptr [a],64h  
unsigned int b = 2;
00007FF7363318C2  mov         dword ptr [b],0C8h  
unsigned int c = a - b;
00007FF7363318C9  mov         eax,dword ptr [b]  
00007FF7363318CC  mov         ecx,dword ptr [a]  
00007FF7363318CF  sub         ecx,eax  
00007FF7363318D1  mov         eax,ecx  
00007FF7363318D3  mov         dword ptr [c],eax

在这个例子中,虽然站在有符号数的角度来看1 - 2这个运算并没有任何问题,我们将会得到-1的补码。但从无符号数的角度来看,由于被减数1小于减数2,在形式上我们并不能直接完成减法操作,需要从更高的位"借位"。在这种情况下,CF标志位会被置为1,表示发生了向高位的借位。

下面我们再站在ALU的角度来思考一下这个问题。

首先,我们知道计算机执行a - b运算的本质是计算a + (~b + 1)

下面,我们还是通过具体的例子来说明这个过程中发生了什么(为了方便说明问题,假设通用寄存器宽度为4bit):

  • a < b情形:假设a=1, b=2,则a - b = 0b0001 + 0b1101 + 1 = 0b1111 = -1。在这个运算过程中并没有发生二进制数向高位的进位,CF被置为1。
  • a ≥ b情形:假设a=2, b=1,则a - b = 0b0010 + 0b1110 + 1 = 0b0001 = 1。在这个运算过程中发生了二进制数向高位的进位,CF被置为0。

可见,如果我们从ALU的视角来思考问题的话,会发现在进行无符号数减法时,CF的取值与ALU底层执行的运算是否产生进位之间的关系,正好与无符号数加法相反。

当然必须说明的是,笔者在此介绍这种理解方式,只是因为曾在网课资料上看到有人如此讲授。实事求是地讲,这种理解方式显然没有前面提到的"向高位借位"的理解方式那般直观和方便记忆。

ZF标志位

ZF标志位(Visual Studio中为ZR)用于标识最近的操作得出的结果是否为0

例如,还是在这个无符号数加法进位的例子中,由于执行add ecx,eax发生了进位,用于储存计算结果的ecx寄存器中的值变为0x00000000,ZF标志位也随之被强制置为1.

unsigned int a = 0xff'ff'ff'ff;
00007FF7C56F182B  mov         dword ptr [a],0FFFFFFFFh  
unsigned int b = 1;
00007FF7C56F1832  mov         dword ptr [b],1  
unsigned int c = a + b;
00007FF7C56F1839  mov         eax,dword ptr [b]  
00007FF7C56F183C  mov         ecx,dword ptr [a]  
00007FF7C56F183F  add         ecx,eax  
00007FF7C56F1841  mov         eax,ecx  
00007FF7C56F1843  mov         dword ptr [c],eax

还有一些其他更加简单的情况,比如说一个有符号的-11相加,那么ZF标志位也会被置为1.

OF标志位

Case1: 正溢出(Positive Overflow)

当两个正数运算(不一定是加法)的结果超出了该数据类型可表示的最大正数范围时,发生正溢出。

例子如下:

    int a = 0x7f'ff'ff'ff;
00007FF7AEA3182B  mov         dword ptr [a],7FFFFFFFh  
    int b = 1;
00007FF7AEA31832  mov         dword ptr [b],1  
    int c = a + b;
00007FF7AEA31839  mov         eax,dword ptr [b]  
00007FF7AEA3183C  mov         ecx,dword ptr [a]  
00007FF7AEA3183F  add         ecx,eax  
00007FF7AEA31841  mov         eax,ecx  
00007FF7AEA31843  mov         dword ptr [c],eax

在这个例子中,变量a保存的已是int32数据类型所能表示的最大正数值0x7fffffff,因此再将其加1就会发生正溢出。具体地,add得到结果为0x80000000,即int32数据类型所能表示的最小负数值的补码形式,同时OF标志位(Visual Studio中为OV)将被强制置为1.

Case2: 负溢出(Negative Overflow)

当两个负数运算的结果超出了该数据类型可表示的最小负数范围时,发生负溢出。

例子如下:

    int a = 0xff'ff'ff'ff;  // 补码形式的-1
00007FF62F911F9B  mov         dword ptr [a],0FFFFFFFFh  
    int b = 0x80'00'00'00;  // 补码表示为-2147483648
00007FF62F911FA2  mov         dword ptr [b],80000000h
    int c = a + b;
00007FF62F911FA9  mov         eax,dword ptr [b]  
00007FF62F911FAC  mov         ecx,dword ptr [a]  
00007FF62F911FAF  add         ecx,eax  
00007FF62F911FB1  mov         eax,ecx  
00007FF62F911FB3  mov         dword ptr [c],eax

我们期望ALU计算0xffffffff0x80000000相加得到的的结果应为0x17fffffff(即十进制的-2147483649),但这就超出了int32所能表示的范围,故ecx是即得到的结果为0x7fffffff(即十进制的2147483647),这就发生了负溢出。因此在执行完add指令后,OF标志位被强制置为1.

有符号数溢出检测原理

结论:

设最高有效数据位(次高位)产生的进位信号为Cn1C_{n-1},符号位(最高位)产生的进位信号为CnC_n,则溢出检测逻辑表达式为OF=Cn1CnOF = C_{n-1} ⊕ C_n.

即当运算过程中最高数据位的进位与符号位的进位不一致时,运算结果发生溢出。

证明:

Part1. 当参加运算的两数均为正数时,无论运算是否发生溢出,CnC_{n}一定为00. 此时若Cn1=1C_{n-1}=1,则运算结果的符号位为11,即发生正溢出(正数相加得到负数).

例:(0110)2+(0111)2=(1101)2(0110)_2+(0111)_2=(1101)_2,此时有Cn1=1C_{n-1}=1,且Cn=0C_{n}=0.

Part2. 当参加运算的两数均为负数时,无论是否发生溢出,两数符号位之和为00,同时CnC_n一定为11. 此时若Cn1=0C_{n-1}=0,则运算结果的符号位仍为00,即发生正溢出(负数相加得到正数).

例:(1000)2+(1001)2=(0001)2(1000)_2+(1001)_2=(0001)_2,此时有Cn1=0C_{n-1}=0,且Cn=1C_{n}=1.

Part3. 当参加运算的两数均为一正一负时,始终不会发生溢出。这是因为对于一正一负的在整型数据表示范围内的两数,其结果的绝对值一定小于等于这两个数的绝对值。这就意味着既然这两个数本身就在能表示的数据范围之内,那它们的加法结果也就一定不会溢出啦。

此时符号位和为11,至于CnC_n的值以及运算结果的符号位值,则完全取决于Cn1C_{n-1}:若Cn1C_{n-1}为0,则CnC_n为0,运算结果符号位为11;若Cn1C_{n-1}为1,则CnC_n11,运算结果的符号位为00. 可见,在一正一负两数进行绝对不会发生溢出的运算时,CnC_nCn1C_{n-1}始终是相同的.

综上所述, CnC_nCn1C_{n-1}不同时,意味着发生溢出;CnC_nCn1C_{n-1}相同时,意味着没有发生溢出.

利用标志位设计指令

Jump If Less

在x86_64汇编中,如果要实现的效果如果signed A<signed B,则跳转,可以使用jl指令。如下是Intel风格的汇编代码示例:

cmp rsi, rdi  // ALU计算rsi-rdi,只根据结果设置标志位寄存器,而不储存运算结果
jl somewhere  // 如果rsi<rdi,则执行跳转

「CSAPP」中指出,jl指令决定是否跳转的本质,是根据位运算表达式SFOFSF⊕OF的取值来决定的。这是为什么呢?下面给出简单的证明:

  • 若A、B均为有符号正数:显然A-B的结果比A、B的绝对值都要小,不可能发生溢出,即OF=0OF = 0. 若A<B,则计算结果应为一负数,即SF=1SF = 1. 因此当A<B时,我们有SFOF=1SF⊕OF=1.
  • 若A、B均为有符号负数:此时A-B相当于A+(-B),两正数做加法,其结果可能溢出。
    • 若没有发生溢出,OF=0OF = 0. 此时若有A<B,则结果仍为一负数,SF=1SF = 1. 于是我们有SFOF=1SF⊕OF=1.
    • 若发生溢出,OF=1OF = 1. 此时若有A<B,虽然A-B的结果在数学上仍然为一负数,但由于溢出的发生,实际上ALU会得到一个正数结果,SF=0SF = 0. 于是我们有SFOF=1SF⊕OF=1.
  • 若A为有符号负数,B为有符号正数,此时必有A<B,我们来看看这种情况下是否必有SFOF=1SF⊕OF=1. 由于A-B相当于A+(-B),即两负数做加法,其结果可能溢出,
    • 若没有发生溢出(OF=0OF = 0),ALU得到的结果为一负数(SF=1SF = 1),于是我们有SFOF=1SF⊕OF=1.
    • 若发生溢出(OF=1OF = 1),ALU得到的结果为一正数(SF=0SF = 0),于是我们有SFOF=1SF⊕OF=1.
  • 若A为有符号正数,B为有符号负数,此时必有A≥B,我们来看看这种情况下是否必有SFOF=0SF⊕OF=0. 由于A-B相当于A+(-B),即两正数做加法,其结果可能溢出。
    • 若没有发生溢出(OF=0OF = 0),ALU得到的结果为一正数(SF=0SF = 0),于是我们有SFOF=0SF⊕OF=0.
    • 若发生溢出(OF=1OF = 1),ALU得到的结果为一负数(SF=1SF = 1),于是我们有SFOF=0SF⊕OF=0.

综上所述,我们可以确定,对于jl指令,只需令其在SFOF=1SF⊕OF=1执行跳转,否则不跳转即可。