如何在x86汇编中设置或清除溢出标志?

7

我想编写一个简单的代码(或算法)来设置/清除溢出标志。对于设置OF,我知道可以使用有符号值。但是如何清除它呢?


执行一个永远不会导致溢出的操作?例如:mov ax, 1; inc ax - Marc B
3个回答

3

有很多可能的解决方案。

例如,test al,al 可以清除 OF 标志而不影响寄存器内容。


或者,如果你不想影响其他标志,你可以直接修改 *FLAGS 寄存器。例如,在32位系统中,这样做看起来像:

pushfd                   ; Push EFLAGS onto the stack
and dword [esp], ~0x800  ; Clear bit 11 (OF)
popfd                    ; Pop the modified result back into EFLAGS

编辑:根据Peter Cordes的建议,将or al, al更改为test al, al。(效果相同但后者在性能方面更优)


5
test al,al是根据寄存器内容设置标志位的更好习惯用语。它只写入标志位,因此不会增加涉及al的依赖链长度。但是pushf/popf方法也有其优点(因为sahf/lahf不包括OF)。 - Peter Cordes
关于 ortest 的观点很好;我相应地更新了我的答案。谢谢! - user1354557

3

提供的条件:

  • 您有一个寄存器,您不关心其内容,
  • 你必须保留 CF 标志

清除OF(sar)的最佳解决方案:

假设寄存器是 al。(只能使用字节寄存器 r/8 的 setc

; clear OF-Flag, preserve CF
setc al
sar al, 1

注意:这是好的,因为它没有部分标志位更新,这可能会导致停顿。(`sar xx, 1`会写入所有标志位,不像`inc`/`dec`会有未修改的标志位) 参见英特尔优化指南,3.5.2.6:部分标志寄存器停顿,但请注意现代英特尔CPU根本没有部分标志位停顿或标志合并:读取FLAGS寄存器的指令只是将CF或SPAZO组中的一个或两个作为2个单独的输入读取。(这就是为什么在Broadwell及以后,`cmovbe`仍然需要2个微操作:它需要CF和ZF。 https://uops.info/)

来源:英特尔文档SAR p.1234

常规解决方案(inc/dec):

假设寄存器是al。(适用于r/8、r/16、r/32、r/64)

; set OF-Flag, preserve CF
mov al, 0x7F
inc al

; clear OF-Flag, preserve CF
mov al, 0x0
inc al

来源:英特尔文档INC p.551

另一种方法(adox):

不同的方法,如果您可以假设:

  • 一个 adx 启用的处理器(您可以使用 grep adx /proc/cpuinfo 检查CPU标志)

假设寄存器是 eax。(需要r64/r32)

; clear OF-Flag, preserve CF
mov eax, 0x0
adox eax, eax

; set OF-Flag, preserve CF
mov eax, 0xFFFFFFFF
adox eax, eax 

注意:不要尝试用xor(或类似的指令)代替mov,因为这将清除CF
来源:英特尔文档 ADOX 第150页

在此之前,您需要确保寄存器不是“-1”。 0xFFFF ... + 1会进行包装,产生进位(ADOX将其输出为OF=1)。但是如果您事先设置了不同的值,那么这是一个有趣的方法来仅设置/清除OF而不触及其他标志。 - Peter Cordes
好的观点。另外,adox无法处理立即值。我已经编辑了代码。 - Joel
2
现代英特尔CPU没有部分标志延迟或部分标志合并uops;请参见@Bee在什么是部分标志停顿?上的答案。我编辑了您的答案以包括这一点,因为您正在讨论优化。 - Peter Cordes

2

popf在Skylake上的速度相当慢(大约每20个周期执行一次);如果您需要清除或设置OF,则最好将其作为ALU指令的副作用完成,特别是您已经打算使用它进行有用计算时,您知道不会或会溢出。 (通常很难找到一个会溢出的,与CF不同,您可以始终使用几乎包围所有输入的常量进行sub而不是add,除了非常小的范围之外)。

如果您需要仅设置/清除OF而不影响其他条件码,那么是的,pushf/popf是正确的方法。lahf / sahf无法获取OF,因为OF位于EFLAGS中的第11位,超出了低8位。


test al,al (或任何相同,相同寄存器)清除OF和CF,就像比较/减零一样。其他标志根据值进行有用的设置。 xor eax,eax清除EAX,并清除OF / SF / CF,设置ZF / PF。你经常需要一个清零的寄存器,所以如果你需要清除OF(例如,对于adox扩展精度链的开始), 那么你可以通过安排代码使最后设置标志的指令是异或清零指令来一举两得。
在x86-64中,您还可以相信对指针+长度使用add不会越过无符号虚拟地址空间的中心,因此清除了OF。但是这种假设可能会在未来具有完全64位虚拟地址的CPU上被打破,因为那时在签名环绕边界周围将没有虚拟地址空间中的空隙,因此单个连续数组可以跨越它。而且这已经可以在32位代码中发生,在64位内核下运行或者在不使用2G:2G内核:用户分割的虚拟地址空间的32位内核下运行。
`

xor eax, eax / cmp al, -128设置OF标志,只需要4个字节的代码。这可能是最便宜的方法,而且不像sub或其他方法那样,它不会写入任何部分寄存器(或任何完整的寄存器)。它仍然将EAX清零。

`

0 - -128 wraps to -128, i.e. signed OF

8位的二进制补码整数只能表示从-128..+127的值。最小的负数是一个特例,没有逆元。它的绝对值/负数是它自己,或者更准确地说,这些函数会溢出。(或者您可以将绝对值操作视为具有带符号输入和无符号输出,因此结果为+128,即0x80。x86没有整数绝对值指令(请准备一个-x,然后测试/cmov),但使用SSSE3它确实有向量整数pabsb)

对于 AL 中的任何已知值,除了 -1,都有一个 cmp al, imm8 会设置 OF。对于从 0..127 的任何值,cmp al, -128 会环绕。对于从 -2..-128 的任何值,cmp al, +127 会环绕并因此设置 OF。对于 -1,减去 127 只会将其带到 -128。减去 -128 会将其提升到 +127。不幸的是,我认为没有一种单指令的方法可以在没有寄存器中的已知值的情况下设置 OF。
它不一定是 al,但是有一个 cmp al,imm8 的特殊编码方式,长度为 2 字节。其他 8 或 32 位寄存器可以使用正常的 3 字节编码。
不使用任何寄存器,也没有已知的常量,这个代码只有6个字节:
push   rax
xor    eax,eax
cmp    al, -128
pop    rax

这确实会覆盖其他条件码,但比pushf/popf更快。通常情况下,您可以覆盖某些内容,否则无法覆盖堆栈。

切换开/关

setno al              # OF=0 -> AL=1           OF=1 -> AL=0
cmp   al, -127        # 1 - -127 = 128 = -128     0 - -127 = +127

lahf和sahf的意义是什么,既然它们并不影响所有标志位?为什么会有这些指令存在? - phuclv
2
@LưuVĩnhPhúc:我不理解8086设计决策将OF放在受LAHF/SAHF影响的标志位之外,特别是在FLAGS的低8位中仍有保留位。我认为这里可能有一些意图,即使是将8080汇编源代码移植到8086也很容易/机械化,因此可能会解释这一点。(你或我应该在复古计算机上询问这个设计决策)LAHF的一个主要用途是从x87 FP状态字的高字节设置条件码,例如fstsw [bp-2] / pop ax / lahf。或者在286及更高版本fstsw ax/lahf - Peter Cordes
1
@LưuVĩnhPhúc:哎呀,应该是fstsw ax / SAHF(将AH存储到FLAGS)/ jajbjp。(LAHF / SAHF助记符对我来说似乎是反过来的;按照这个顺序编写,我的大脑认为将AH加载到FLAGS中,而不是从FLAGS中加载AH)。无论如何,这就是为什么更近期的fucomi和SSE ucomiss / ucomisd指令会像无符号比较一样设置标志位,当出现无序(NaN)时设置PF,以便与遗留方法中x87状态字中的C3 / C2 / C0位对齐 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接