gpt4 book ai didi

x86 - 获取比较指令的值

转载 作者:行者123 更新时间:2023-12-03 05:59:42 29 4
gpt4 key购买 nike

据我了解,cmp指令将设置标志寄存器中的某些位。然后,您可以使用诸如jle,jnp等指令来基于这些指令进行分支。

我想知道的是如何从比较中恢复整数值。

示例:以下是有效的c语法

y = x[a >= 13];


因此,将a与13进行比较,得出true或false分别解释为1或0。但是,必须将1或0作为整数输入到数组访问中。编译器会做什么?

我能想到的一些事情是:

进行比较,然后跳转到x [0]或x [1]

进行比较,然后分支以执行tmp = 0或tmp = 1然后执行x [tmp]

也许在标志上做一些花哨的逻辑(不确定是否有直接访问标志的指令)

我已尝试查看此代码示例中gcc吐出的内容,但是不可能从它抛出的所有多余垃圾中挑选出逻辑。

我正在开发一个编译器,所以任何建议将不胜感激。

最佳答案

基本上可以通过三种方式完成此操作。我一次要遍历他们。

一种解决方法基本上就是您在问题中描述的内容:进行比较,然后跳转到分别实现这两种可能性的代码。例如:

    cmp  [a], 13                     ; compare 'a' to 13, setting flags like subtraction
jge GreaterThanOrEqual ; jump if 'a' >= 13, otherwise fall through

mov eax, [x * 0 * sizeof(x[0])] ; EAX = x[0]
jmp Next ; EAX now loaded with value, so do unconditional jump

GreaterThanOrEqual:
mov eax, [x * 1 * sizeof(x[0])] ; EAX = x[1]
; EAX now loaded with value; fall through

Next:
mov [y], eax ; store value of EAX in 'y'


通常,编译器会尝试在寄存器中保留更多的值,但这应该使您对基本逻辑有所了解。它进行比较,然后跳转到读取/加载 x[1]的指令,或者跳转到读取/加载 x[0]的指令。然后,它移至将值存储到 y的指令。

您应该能够看到,由于需要所有分支,因此效率相对较低。因此,优化编译器不会生成这样的代码,尤其是在您具有基本三元表达式的简单情况下:

(a >= 13) ? 1 : 0


甚至:

(a >= 13) ? 125 : -8


可使用位旋转技巧来进行此比较并获得相应的整数,而不必执行分支。

这将我们带到第二种实现方法,即使用 SETcc指令。 cc部分代表“条件代码”,并且所有条件代码都与条件跳转指令的条件代码相同。 (实际上,您可以将所有条件跳转指令写为 Jcc。)例如, jge表示“如果大于等于则跳转”。类似地, setge表示“如果大于等于则设置”。简单。

关于 SETcc的技巧是设置一个BYTE大小的寄存器,这基本上意味着 ALCLDLBL(还有更多选项;您可以设置其中一个的高字节这些寄存器,和/或在64位长模式下,还有更多选项,但这是操作数的基本选择)。

这是实现此策略的代码示例:

xor   edx, edx        ; clear EDX
cmp [a], 13 ; compare 'a' to 13, setting flags like subtraction
setge dl ; set DL to 1 if greater-than-or-equal, or 0 otherwise
mov eax, [x * edx * sizeof(x[0])]
mov [y], eax


酷吧?分支淘汰。所需的0或1直接加载到 DL中,然后用作加载的一部分( MOV指令)。

唯一有点令人困惑的是,您需要知道 DL是完整的32位 EDX寄存器的低字节。这就是为什么我们需要预先清除完整的 EDX的原因,因为 setge dl仅影响低字节,但是我们希望完整的 EDX为0或1。事实证明,将全部寄存器预清零是 the most optimal way of doing this on all processors,但是还有其他方法,例如在 MOVZX指令后使用 SETcc。链接的答案对此进行了详细介绍,因此在这里我不会感到惊讶。关键在于 SETcc仅设置寄存器的低字节,但是后续指令要求整个32位寄存器具有该值,因此您需要消除高字节中的垃圾。

无论如何,这是当您编写 y = x[a >= 13]之类的代码时,编译器将生成99%的代码。 SETcc指令为您提供了一种方法,可以根据一个或多个标志的状态来设置字节,就像可以在标志上进行分支一样。基本上,这就是您正在考虑的一条允许直接访问标志的指令。

这实现了

(a >= 13) ? 1 : 0


但是如果你想做什么

(a >= 13) ? 125 : -8


就像我之前提到的?好的,您仍然使用 SETcc指令,但是之后要花一些点花哨的时间来“固定”结果0或1为您想要的值。例如:

xor   edx, edx
cmp [a], 13
setge dl
dec edx
and dl, 123
add edx, 125
; do whatever with EDX


这几乎适用于任何二进制选择(取决于条件,两个可能的值),并且优化编译器足够聪明来解决这个问题。仍然是无分支的代码;很酷。

有第三种方法可以实现,但是从概念上讲,它与我们刚才讨论的第二种方法非常相似。它使用条件移动指令,这是基于标志状态进行无分支设置的另一种方法。条件移动指令为 CMOVcc,其中 cc再次引用“条件代码”,与前面示例中的完全一样。 CMOVcc指令是在1995年左右的Pentium Pro中引入的,此后一直在所有处理器中使用(不是Pentium MMX,而是Pentium II和更高版本),因此基本上就是您今天所见的一切。

该代码非常相似,只是顾名思义,它是有条件的移动,因此需要更多的初步设置。具体来说,您需要将候选值加载到寄存器中,以便可以选择合适的候选值:

xor    edx, edx    ; EDX = 0
mov eax, 1 ; EAX = 1
cmp [a], 13 ; compare 'a' to 13 and set flags
cmovge edx, eax ; EDX = (a >= 13 ? EAX : EDX)
mov eax, [x * edx * sizeof(x[0])]
mov [y], eax


请注意,将 EAX移到 EDX是有条件的-仅在标志指示条件 ge(大于或等于)时才会发生。因此,它可以执行基本的C三进制运算,如指令右侧的注释所述。如果标志指示 ge,则将 EAX移入 EDX。否则,什么都不会移动,并且 EDX保持其原始值。

请注意,尽管某些编译器(特别是Intel的编译器,称为ICC)将首选 CMOV指令而不是 SET指令,但这与我们先前在 SETGE中看到的以前的实现没有任何优势。实际上,这确实不是最佳选择。

CMOV真正有用的地方是,您可以消除获取除旧的0或1以外的值所需的位旋转代码。例如:

mov    edx, -8     ; EDX = -8
mov eax, 125 ; EAX = 125
cmp [a], 13 ; compare 'a' to 13 and set flags
cmovge edx, eax ; EDX = (a >= 13 ? EAX : EDX)
; do whatever with EDX


现在减少了指令的数量,因为正确的值被直接移入了 EDX寄存器,而不是将其设置为0或1,然后不得不将其操纵为所需的值。因此,编译器将使用 CMOV指令(当针对支持它们的处理器时,如上所述)实现更复杂的逻辑,例如

(a >= 13) ? 125 : -8


即使他们可以使用其他方法之一进行操作。当条件两侧的操作数不是编译时常量时(例如,它们是仅在运行时才知道的寄存器中的值),您还需要条件移动。

有帮助吗? :-)


我试图看一下gcc在此代码示例中吐出的内容,但是不可能从它抛出的所有额外垃圾中挑选出逻辑。


是的我为您提供一些提示:


将代码放到一个非常简单的函数中,该函数仅执行您想学习的内容。您将需要将输入作为参数(这样优化器就不会轻易折叠常量),并且您想从函数返回输出。例如:

int Foo(int a)
{
return a >= 13;
}


返回 bool也可以在这里工作。当然,如果使用条件运算符返回0或1以外的值,则需要返回 int

无论哪种方式,现在您都可以准确看到编译器生成的汇编指令以实现此目的,而不会产生任何其他噪音。确保已启用优化;看着调试代码是没有启发性的,并且非常嘈杂。
确保您要求GCC使用Intel / MASM格式生成程序集列表,该格式比其默认格式GAS / AT&T语法更容易阅读(至少在我看来)。我上面的所有汇编代码示例都是使用Intel语法编写的。要求的咒语是:

gcc -S -masm=intel MyFile.c


其中, -S为输入源代码文件生成一个程序集列表,而 -masm=intel将程序集列表语法格式切换为Intel样式。
使用类似 Godbolt Compiler Explorer的漂亮工具,该工具可以自动完成所有这些操作,从而大大减少周转时间。另一个好处是,它对汇编指令进行了颜色编码,以与原始源代码中的C代码行匹配。

Here is an example of what you'd use to study this。原始来源在最左边。中间窗格显示了GCC 7.1的现代处理器汇编输出,该处理器支持 CMOV指令。最右边的窗格显示了GCC 7.1的汇编输出,用于不支持 CMOV指令的非常旧的处理器。酷吧?您可以轻松地操纵编译器开关,并观察输出如何变化。例如,如果您使用 -m64(64位)而不是 -m32(32位),则会看到参数是通过寄存器( EDI)传递的,而不是通过堆栈,并且必须作为函数中的第一条指令加载到寄存器中。

关于x86 - 获取比较指令的值,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/45180785/

29 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com