获取比较指令的值

Chu*_*uck 3 x86 compare

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

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

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

y = x[a >= 13];
Run Code Online (Sandbox Code Playgroud)

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

我能想到的一些事情是:

进行比较,然后分支到 x[0] 或 x[1]

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

也许对标志做一些奇特的逻辑(不确定是否有直接访问标志的指令)

我试图查看 gcc 为该代码示例吐出的内容,但不可能从它抛出的所有额外垃圾中找出逻辑。

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

Cod*_*ray 6

基本上可以通过三种方法来完成此操作。我会一次一个地回顾它们。

\n\n

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

\n\n
    cmp  [a], 13                     ; compare \'a\' to 13, setting flags like subtraction\n    jge  GreaterThanOrEqual          ; jump if \'a\' >= 13, otherwise fall through\n\n    mov  eax, [x * 0 * sizeof(x[0])] ; EAX = x[0]\n    jmp  Next                        ; EAX now loaded with value, so do unconditional jump\n\nGreaterThanOrEqual:\n    mov  eax, [x * 1 * sizeof(x[0])] ; EAX = x[1]\n                                     ; EAX now loaded with value; fall through\n\nNext:\n    mov  [y], eax                    ; store value of EAX in \'y\'\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n\n

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

\n\n
(a >= 13) ? 1 : 0\n
Run Code Online (Sandbox Code Playgroud)\n\n

甚至:

\n\n
(a >= 13) ? 125 : -8\n
Run Code Online (Sandbox Code Playgroud)\n\n

有一些位操作技巧可以用来进行这种比较并获得相应的整数,而无需进行分支。

\n\n

这给我们带来了第二种方法,即使用指令SETcc。该cc部分代表“条件代码”,所有条件代码与条件跳转指令的条件代码相同。(事实上​​,您可以将所有条件跳转指令写为Jcc。)例如,jge表示“如果大于或等于则跳转”;类似地,setge意思是“如果大于或等于则设置”。简单的。

\n\n

诀窍在于SETcc它设置了一个字节大小的寄存器,这基本上意味着ALCLDL、 或BL(还有更多选项;你可以设置这些寄存器之一的高字节,和/或在 64 位长模式下有更多选项,但这些是操作数的基本选择)。

\n\n

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

\n\n
xor   edx, edx        ; clear EDX\ncmp   [a], 13         ; compare \'a\' to 13, setting flags like subtraction\nsetge dl              ; set DL to 1 if greater-than-or-equal, or 0 otherwise\nmov   eax, [x * edx * sizeof(x[0])]\nmov   [y], eax\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n\n

这里唯一有点令人困惑的是,您需要知道它DL是完整 32 位EDX寄存器的低字节。这就是为什么我们需要预清除 full EDX,因为setge dl只影响低字节,但我们希望 fullEDX为 0 或 1。事实证明,预清零完整寄存器是执行此操作的最佳方法所有处理器,但还有其他方法,例如在指令MOVZX 之后SETcc使用。链接的答案对此进行了非常详细的介绍,因此我不会在这里进行详细说明。关键是只SETcc设置了寄存器的低字节,但后续指令需要整个32位寄存器都有值,所以需要消除高字节中的垃圾。

\n\n

不管怎样,当你编写类似y = x[a >= 13]. 该SETcc指令为您提供了一种根据一个或多个标志的状态设置字节的方法,就像您可以在标志上进行分支一样。这基本上就是您所想到的允许直接访问标志的指令。

\n\n

这实现了以下逻辑

\n\n
(a >= 13) ? 1 : 0\n
Run Code Online (Sandbox Code Playgroud)\n\n

但如果你想做怎么办

\n\n
(a >= 13) ? 125 : -8\n
Run Code Online (Sandbox Code Playgroud)\n\n

就像我之前提到的?好吧,您仍然使用该SETcc指令,但之后您会进行一些花哨的位调整,以将结果 0 或 1“修复”为您实际想要的值。例如:

\n\n
xor   edx, edx\ncmp   [a], 13\nsetge dl\ndec   edx\nand   dl, 123\nadd   edx, 125\n; do whatever with EDX\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n\n

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

\n\n

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

\n\n
xor    edx, edx    ; EDX = 0\nmov    eax, 1      ; EAX = 1\ncmp    [a], 13     ; compare \'a\' to 13 and set flags\ncmovge edx, eax    ; EDX = (a >= 13 ? EAX : EDX)\nmov    eax, [x * edx * sizeof(x[0])]\nmov    [y], eax\n
Run Code Online (Sandbox Code Playgroud)\n\n

EAX请注意,进入的移动EDX有条件的\xe2\x80\x94 仅当标志指示条件ge(大于或等于)时才会发生。因此,它可以实现基本的 C 三元运算,如指令右侧的注释中所述。如果标志指示ge,则EAX移至EDX。否则,什么都不会移动,并EDX保持其原始值。

\n\n

请注意,尽管某些编译器(特别是英特尔的编译器,称为 ICC)更喜欢CMOV指令而不是SET指令,但这与我们之前在SETGE. 事实上,这确实是次优的。

\n\n

真正CMOV发挥作用的是允许您消除获取除旧的 0 或 1 之外的值所需的位操作代码。例如:

\n\n
mov    edx, -8     ; EDX = -8\nmov    eax, 125    ; EAX = 125\ncmp    [a], 13     ; compare \'a\' to 13 and set flags\ncmovge edx, eax    ; EDX = (a >= 13 ? EAX : EDX)\n; do whatever with EDX\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n\n
(a >= 13) ? 125 : -8\n
Run Code Online (Sandbox Code Playgroud)\n\n

即使他们可以使用其他方法之一来完成这些任务。当条件两侧的操作数不是编译时常量(即,它们是寄存器中的值,仅在运行时已知)时,您还需要条件移动。

\n\n

这有帮助吗?:-)

\n\n
\n

我试图查看 gcc 为这个代码示例吐出的内容,但不可能从它抛出的所有额外垃圾中找出逻辑。

\n
\n\n

是的。我有一些提示给你:

\n\n
    \n
  1. 将您的代码精简为一个非常简单的函数,只做您想要研究的事情。您需要将输入作为参数(以便优化器不能简单地折叠常量),并且您需要返回函数的输出。例如:

    \n\n
    int Foo(int a)\n{\n    return a >= 13;\n}\n
    Run Code Online (Sandbox Code Playgroud)\n\n

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

    \n\n

    无论哪种方式,现在您都可以准确地看到编译器正在生成哪些汇编指令来实现此目的,而不会产生任何其他噪音。确保您已启用优化;查看调试代码没有指导意义,而且非常有帮助嘈杂。

  2. \n
  3. 确保您要求 GCC 使用 Intel/MASM 格式生成汇编列表,这很重要容易阅读(至少在我看来)。我上面的所有汇编代码示例都是使用 Intel 语法编写的。所需的咒语是:

    \n\n
    gcc -S -masm=intel MyFile.c\n
    Run Code Online (Sandbox Code Playgroud)\n\n

    其中为-S输入源代码文件生成汇编列表,并将-masm=intel汇编列表语法格式切换为 Intel 风格。

  4. \n
  5. 使用像Godbolt Compiler Explorer这样的好工具,它可以自动执行所有这些操作,从而大大减少周转时间。另一个好处是,它对汇编指令进行颜色编码,以与原始源代码中的 C 代码行相匹配。

    \n\n

    这是您用来研究此内容的示例。原始来源位于最左侧。中间窗格显示了现代处理器的 GCC 7.1 汇编输出,它支持CMOV指令。最右侧的窗格显示了 GCC 7.1 对于不支持指令的非常旧的处理器的汇编输出CMOV。很酷,对吧?您可以轻松操作编译器开关并观察输出如何变化。例如,如果您执行-m64(64 位)而不是-m32(32 位),那么您将看到参数在寄存器 ( EDI) 中传递,而不是在堆栈上传递并必须加载到注册为函数中的第一条指令。

  6. \n
\n