【问题标题】:x86 sbb with same register as first and second operandx86 sbb 与第一个和第二个操作数具有相同的寄存器
【发布时间】:2017-04-23 05:59:19
【问题描述】:

我正在分析一系列x86 指令,并且对以下代码感到困惑:

135328495: sbb edx, edx
135328497: neg edx
135328499: test edx, edx
135328503: jz 0x810f31c

我知道sbb 等于des = des - (src + CF),换句话说,第一条指令以某种方式将-CF 放入edx。然后把negtive-CF变成CFtestCF是否等于0??

但请注意,jz 检查标志 ZF,而不是 CF!那么基本上上面的代码序列试图做什么?这是一个合法的x86指令序列,由g++版本4.6.3产生。

C++ 代码实际上来自botan 项目。您可以在here 找到整个汇编代码(Botan RSA 解密示例)。反汇编代码中有不少这样的指令序列。

【问题讨论】:

  • 这来自什么来源?什么编译选项? (启用优化?任何-mtune=bdver2 或什么?AMD CPU 将sbb same,same 识别为独立于 reg 的旧值,并且仅取决于 CF。在 Intel CPU 上,setc dl / @987654346 可能会更快@(或者更好,have edx zeroed before running setc, preferably with a recognizes zeroing idiom to avoid a partial-register stall),所以我很惊讶 g++ 会将此序列用于任何事情。)它使用 TEST 很奇怪,因为 NEG 已经设置了标志。
  • sbb/neg 是一个非常常见的 MSVC 习惯用法,用于生成无分支代码,但我通常不会看到 GCC 使用它——正如 Peter 建议的那样,它往往更喜欢 setc。但是,无论哪种方式,由于这不是无分支代码,因此避免jc 的扭曲没有多大意义。在我看来,test 指令也是多余的。您说代码来自 GCC 4.6.3,但是 input 代码是从哪里来的?你有原始的 C 或 C++ 源代码,还是你在反编译一个不透明的二进制文件?
  • 我很想看看g++ 是如何产生这个以及何时产生的,因为该代码看起来不太好,而经过优化的 g++ 通常会产生更好的东西。这看起来更像是一些人为构建的序列。
  • @PeterCordes 我可以让 GCC 4.6.4 用一个有点做作的例子来生成这个序列:godbolt.org/g/K2rUPv
  • @PeterCordes 我不认为 GCC 曾经使用过 SBB/NEG 习语,NEG 只是因为代码中的明确否定(使它有点做作)而存在。 GCC 使用刚刚使用 SBB 计算 e = a > b ? 0 : -1,然后 NEG 计算 e = -e。如果将其简化为e = a > b ? 0 : 1 或只是e = a > b,则 GCC 4.6.4 使用 SETcc 指令。顺便提一句。现代版本的 GCC 在某些情况下仍将使用 SBB,例如 a > b ? 1 : 10

标签: assembly x86 cpu-registers carryflag eflags


【解决方案1】:
sbb edx, edx

您对该指令的分析是正确的。 SBB 表示“借位减法”。它以一种考虑进位标志 (CF) 的方式从目标中减去源。

因此,它等同于dst = dst - (src + CF),所以这是edx = edx - (edx + CF),或者简称为edx = -CF

这里的源操作数和目标操作数都是edx,不要让它愚弄您! SBB same, same 是编译器生成的代码中用于隔离进位标志 (CF) 的一个非常常见的习惯用法,尤其是当它们试图生成无分支代码时。还有其他方法可以做到这一点,即SETC 指令,它在大多数 x86 架构上可能更快(请参阅 cmets 以获得更彻底的剖析),但幅度不大。来自不同供应商(甚至可能是不同版本)的编译器往往会偏爱其中一种,并且在您不进行特定于架构的调整时到处使用它。

neg edx

同样,您对本说明的分析是正确的。这是一个非常简单的。 NEG 对其操作数执行二进制补码否定。因此,这只是edx = -edx

在这种情况下,我们知道edx最初包含-CF,这意味着它的初始值为0-1(因为CF总是0或1,开或关) .否定它意味着edx 现在包含01

也就是说,如果CF最初 设置的,edx 现在将包含1;否则,它将包含0。这确实是上面讨论的成语的完成;您需要NEG 来完全隔离CF 的值。

test edx, edx

TEST 指令与AND 指令相同,只是它不影响目标操作数——它只设置标志。

但这是另一种特殊情况。 TEST same, samea standard idiom in optimized code 以有效地确定寄存器中的值是否为 0。您可以编写 CMP edx, 0,这是人类程序员天真地做的事情,但 test 更快。 (为什么会这样?因为按位与的真值表。value & value == 0 的唯一情况是value 为 0。)

所以这具有设置标志的效果。具体来说,如果edx 为0,它设置零标志(ZF),如果edx 非零,则清除它。

因此,如果CF最初 设置,ZF 现在将被清除;否则,它将被设置。也许更简单的看待它的方式是这三个指令将ZF 设置为与CF 的原始值相反。

以下是两种可能的数据流:

  • CF == 0 → edx = 0 → edx = 0ZF = 1
  • CF == 1 → edx = -1 → edx = 1ZF = 0
jz 0x810f31c

最后,这是一个基于ZF 值的条件跳转。如果设置了ZF,则跳转到0x810f31c;否则,它会进入下一条指令。

然后,将所有内容放在一起,此代码通过涉及零标志 (ZF) 的间接路径测试进位标志 (CF) 的补码。如果进位标志最初被清除,它会分支,如果进位标志最初被设置,它就会掉线。

这就是它的工作原理。也就是说,我无法解释为什么编译器选择以这种方式生成代码。它似乎在许多层面上都不是最优的。最明显的是,编译器可以简单地发出JNC 指令(如果不进位则跳转)。尽管 Peter Cordes 和我在 cmets 中进行了各种其他观察和推测,但我认为将所有这些都纳入答案是没有意义的,除非可以提供有关此代码来源的更多信息。

【讨论】:

  • sbb edx,edx / neg 显然比setc 更差到异或零寄存器(last part of this answer)。 SBB 在除 AMD 之外的所有东西上都对 EDX 有错误的依赖,而 SBB 在 Intel 前 Broadwell 上是 2 微秒。 sbb same,same 在您想要全 1(即 -1)时可能是一个有用的习惯用法,但使用 NEG 会将其拖到最好的水平。这是一个很小的差异,但我不会说这是一个折腾,因为没有 SBB/NEG 更好的 CPU,而许多 CPU 更差。请注意,即使在 AMD 上,SBB 和 NEG 都在关键路径上,这与 XOR/SETC 不同。
  • 我认为 OP 意味着 NEG 将 EDX 中的 -CF 值更改为 CF 的原始值,仍在 EDX 中。并不是说它在 EFLAGS 中设置了 CF。所以我认为 OP 理解正确,但措辞含糊。
  • @peter 嗯,我清楚地记得在基准测试中阅读和确认xor/setcsbb/neg 慢。这不是针对 AMD 处理器的,绝对是针对英特尔的。它几乎可以肯定是旧的。最初的消息来源可能一直在谈论 PII,而我的测试是在 PIII 和 P4 上进行的(而 P4 确实是一只奇怪的鸟)。 MSVC 一直以来生成此代码的事实也强烈表明它曾经更可取,可能是在他们第一次做出此选择的 486 或 Pentium 上。英特尔曾经提醒setCC 速度慢,应该避免使用。
  • setCC 在 PIII 和 P4 上也是 2 微秒,所以 sbb 的唯一问题是错误的依赖。 (英特尔还指出setCC 是高延迟,但我相信这并不重要,因为它不在关键路径上。)我必须再次对此进行分析才能确定。你不能真的相信微商来讲述整个故事!但感谢您指出这一点。可能我的知识已经过时了,需要重新审视。 (另外,我知道 OP 的意思。我没有说他的分析错了,我说他是对的!)
  • 嗯,我没有看过旧 CPU 上的 SETCC。仍然相关的所有内容都有 1 uop / 1c 延迟。 Agner 在 Merom 之前的延迟为 1c,在此之前的延迟为 1 uop(在 P4 上很慢)。在 AMD 上,它是 1m-op / 1c 回到 K7,具有高吞吐量。我只是说 2 uops 作为 2 个具有 2c 延迟的依赖 uops 的简写,因为评论空间是有限的。但无论如何,我想这解释了 MSVC 选择的成语;谢谢你的历史课!
【解决方案2】:

我知道 sbb 等于 des = des - (src + CF),换句话说,第一条指令以某种方式将 -CF 放入 edx。

是的,edx = edx - (edx + CF) = -CF。所以sbb edx,edx 将在 CF=0 时将 edx 设置为 0,在 CF=1 时设置为 -1 (0xFFFFFFFF)。减法本身也会产生新的 CF 值,如果我不太困惑的话,它等于旧值。

然后将-CF负入CF,并测试CF是否等于0??

几乎是,但不是。它否定edx,而不是CF。要否定 CF,有单独的指令 CMC(来自 stc/clc/cmc 进位标志修改指令系列)。

所以从 0/-1 edx 将被修改为 0/1,CF 将再次设置为 0/1(哇,我不知道 neg 将 CF 设置为 ~ZF)。另外neg 已经设置了 ZF,所以下面的test edx,edx 是多余的。

test edx,edx 不测试 CF,而是测试 edx(此时为 01),它会产生 CF=0 和 ZF=1/0 的 0/1 值。

所以你继续思考edx 中的数值源自 CF 这一事实,你一直在想 CF,但实际上从第一个 sbb 开始你就可以忘记旧的 CF,每条下一条指令(包括sbb)是算术的,因此它确实以自己的方式修改了CF。但是那些neg/test 指令是edx 关注的,在寄存器中的数字上,CF 只是他们计算的副产品。

但请注意,jz 检查标志 ZF,而不是 CF!

确实,因为 CF 在最后一个 test 之后确实包含 0,所以与 sbb 之前的初始 CF 值完全无关。另一方面,ZF 与原始 CF 值直接相关,如果代码以 CF=1 开头,那么最后一个jz 将不会被取走(ZF=0),如果代码开头在 CF=0 下,最后一个jz 将被取走(ZF=1)。

【讨论】:

  • 我认为OP在谈论EDX中的值,只是将其标记为-CF然后CF,表示CF的起始值。或者也许 jz 检查标志 ZF,而不是 CF 是他们没有正确遵循它的线索。
  • @PeterCordes 非常感谢您的示例和回复。我更新了问题以供您参考。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-03-28
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多