assembly 在x86中设置和清除零标志

nfzehxib  于 2022-11-13  发布在  其他
关注(0)|答案(3)|浏览(215)

在x86-64中,设置和清除零标志(ZF)的最有效方法是什么?
不需要具有已知值的寄存器或根本不需要任何空闲寄存器的方法是优选的,但是如果当那些或其它假设为真时有更好的方法可用,则也值得一提。

wmvff8tz

wmvff8tz1#

ZF=0
这是比较困难的。cmp在任何两个已知不相等的寄存器之间。或者cmp reg,imm与一些寄存器不可能具有的任何值。例如,cmp reg,1与任何已知为零的寄存器。

一般来说,test reg,reg适用于任何已知的非0寄存器值,例如指针
**test rsp, rsp可能是一个不错的选择,**甚至test esp, esp都可以保存一个字节,除非您的堆栈位于跨越4G边界的不寻常位置。

我看不出有什么方法可以在一条指令中创建ZF=0,而不对某个输入寄存器产生假依赖。如果您不介意破坏一个寄存器,打破假依赖,xor eax,eax/inc eaxdec将在2个微操作中完成此操作。(not不会设置FLAGS,neg只会执行0-0 = 0。)

or eax, -1不需要寄存器值的任何前提条件。(伪相关性,但不是真正的相关性,因此您可以选择任何寄存器,即使它可能为零。)它不一定是-1,它不会为您带来任何好处,因此如果您能使它变得有用,那就更好了。

  • FLAG结果 * or eax,-1:ZF=0 PF=1 SF=1 CF=0 OF=0(AF=未定义)。**

如果需要在循环中执行此操作,显然可以在循环 * 外部 * 对其进行设置,前提是可以指定一个寄存器为非零值,以便与test一起使用。
ZF=1
破坏性最小:cmp eax,eax-但是有一个伪依赖关系(我假设),需要一个后端uop:不是一个清零的习惯用法。RSP通常不会改变太多,所以cmp esp, esp可能是一个很好的选择。(除非它强制堆栈同步微操作)。
效率最高:**xor-zeroing(如使用任何空闲寄存器的xor eax,eax)无疑是SnB系列上最有效的方法(与2字节nop**成本相同,如果需要雷克斯,则为3字节,因为您希望将r8d..r15d中的一个清零):1个前端微操作,0个后端微操作(在SnB系列上),并且FLAGS结果在其发出的同一周期内就绪。(仅在前端暂停的情况下相关,或者在同一周期内发出依赖于它的微操作,并且RS中没有任何具有就绪输入的较旧微操作,否则此类微操作对于任何执行端口都具有优先级。)

标记结果:ZF=1 PF=1 SF=0 CF=0 OF=0(AF=undefined).(或者使用sub eax,eax得到定义明确的AF=0。实际上,现代CPU也会选择AF=0进行异或归零,因此它们可以用相同的方式对两个归零惯用法进行解码。Silvermont只将32位操作数大小的异或识别为归零惯用法,而不是sub。)

当然,异或零在所有其它uarch上也是非常便宜的:没有输入依赖性,并且不需要任何预先存在的寄存器值。(因此不会导致P6系列寄存器读取暂停)。因此,在最坏的情况下,它将与您可以在任何其他uarch上执行的任何其他操作绑定在一起(在那里它确实需要执行单元)。
(On早期P6系列,Pentium M之前,xor-归零 * 不 * 中断依赖性;它只触发特殊的al=eax状态,以避免部分寄存器填充。但是这些CPU都不是x86-64,都是32位的。)
无论如何,需要一个置零寄存器是很常见的,例如,作为0 - xsub目的地,以便进行复制和求反,因此,通过将异或置零放在需要它的地方来利用它,也可以创建一个有用的FLAG条件。
有趣但可能没有用:test al, 0的长度为2个字节,但cmp esp,esp的长度也是2个字节。

正如@prl所建议的,cmp same,same与任何寄存器都可以工作,而不会干扰值。我怀疑这 * 不是 * 特殊情况,因为依赖关系破坏了sub same,same在某些CPU上的方式,因此选择一个“冷”寄存器。同样是2或3个字节,1个微操作。它可以与JCC微融合,但这是愚蠢的(除非JCC也是来自其他条件的分支目标?)

标记结果:与异或归零相同。
缺点:

  • (可能)假依赖关系
  • 在P6系列上,可能会导致寄存器读取暂停,因此请选择您已经在附近指令中阅读的冷寄存器。
  • 需要SnB系列上的后端执行单元

为了好玩,其他廉价的替代方法包括test al, 0。2个字节用于AL,3或4个字节用于任何其他8位寄存器。(雷克斯)+ opcode + modrm + imm 8。原始寄存器值无关紧要,因为imm8为零保证了reg & 0 = 0
如果你碰巧在一个寄存器中有一个1-1,你可以销毁它,32位模式incdec只需要1个字节就可以设置ZF。但是在x86-64中,这至少需要2个字节。对于64位模式中的1个字节的指令,没有什么是真正有效的,并且可以设置FLAGS。

ZF=!CF

sbb same,same可以设置ZF=!CF(保持CF不变),并将reg设置为0(CF=0)或-1(CF=1)。在AMD上,自Bulldozer(BD系列和Zen-family)以来,这不依赖于GP寄存器,仅依赖于CF。但在其他uarch上,这不是特殊情况,对reg存在错误依赖。在Broadwell之前的英特尔上,这是2个uops。

ZF=!bool(整数寄存器)

要设置ZF=!integer_reg,很明显,普通的test reg,reg是最好的选择(比and reg,regor reg,reg好,除非你有意重写寄存器以避免P6寄存器读取暂停)。

如果寄存器值为零,则ZF=1,因此它类似于C的逻辑反运算符。

ZF=!ZF

可能是setz al/test al, al。没有单一指令:我不认为任何读ZF和写FLAGS。setz在寄存器中实现ZF,那么test就是ZF = !reg

其他FLAGS条件:

CF具有clc/stc/cmc指令。(clc与SnB系列上的异或归零一样高效。)

6ojccjat

6ojccjat2#

操作Fags低8位中任何一位的最简单方法是使用经典的LAHF/SAHF指令,该指令将Fags带入AH或从AH取出Fags,可对其应用任何位操作。

关闭ZF

LAHF                      ; Load lower 8 bit from Flags into AH
       AND      AH, 10111111b    ; Clear bit for ZF
       SAHF                      ; Store AH back to Flags

开启ZF

LAHF                      ; Load AH from FLAGS
       OR       AH, 01000000b    ; Set bit for ZF
       SAHF                      ; Store AH back to Flags

当然,任何CMP (E)AX,(E)AX都可以更快地设置ZF,并且使用更少的代码;这样做目的是不修改其他FLAGS,如 * How to read and write x86 flags registers directly? * 和 * how to change flags manually (in assembly code) for 8086? *

CAVEAT对于早期的AMD 64- LAHF在长模式下是一个扩展

一些非常早期的x86-64 CPU,最值得注意的是所有

*AMD速龙64、皓龙和Turion 64修订版D之前(2005年3月)和
奔腾4步进G1之前的英特尔(2005年12月)

由于该指令最初被从AMD 64指令子集中删除,但后来又被重新引入。幸运的是,这发生在x86-64成为普遍景象之前,因此只有少数早期的高端CPU受到影响,甚至更少生存下来。更重要的是,这些CPU无法运行Win10或任何64位Windows之前的Win10(参见SuperUser.SE上的this answer)。
如果您真的认为有人可能会尝试在使用了17年以上的高端CPU上运行该软件,则可以通过执行CPUID(EAX= 80000001 h)并测试2^0=1来检查。

lh80um4z

lh80um4z3#

假设您不需要保留其他标志的值,

cmp eax, eax

相关问题