x86 sbb 具有与第一个和第二个操作数相同的寄存器

x86 sbb with same register as first and second operand

我正在分析一系列 x86 指令,并与以下代码混淆:

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

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

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

生成

C++代码实际上来自botan project. You can find the overall assembly code (the Botan RSA decryption example) at here。反汇编代码中这样的指令序列相当多

I understand that sbb equals to des = des - (src + CF), in other words, the first instruction somehow put -CF into edx.

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

Then it negtive -CF into CF, and test whether CF equals to zero??

几乎是,但不是。它否定 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 只是它们计算的副产品。

But note that jz checks flag ZF, not CF!

确实,由于 CF 在最后 test 之后确实包含 0,与 sbb 之前的初始 CF 值完全无关。另一方面,ZF 与原始 CF 值直接相关,如果代码以 CF=1 开头,则不会采用最后的 jz (ZF=0),如果代码在 CF=0 下开始,将采用最后一个 jz (ZF=1).

sbb edx, edx

您对该指令的分析是正确的。 SBB 表示 "subtract with borrow"。它以将进位标志 (CF) 考虑在内的方式从目标中减去源。

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

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

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, same 以有效地确定寄存器中的值是否为 0。您可以编写 CMP edx, 0,这是人类程序员天真的做法,但 test 更快. (为什么这行得通?因为 table 用于按位与。唯一 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 和我在评论中进行了各种其他观察和推测,但我认为将所有这些纳入答案是没有意义的,除非可以提供有关此代码来源的更多信息。