【问题标题】:How do conditional jumps work, based on the values of the flags in x86 assembly?基于 x86 汇编中的标志值,条件跳转是如何工作的?
【发布时间】:2017-12-28 20:50:18
【问题描述】:

我正在阅读 Jeff Duntemann 的Assembly Language Step-by-Step,我对某些条件跳转的工作原理感到困惑。我知道CMP 用于使用减法比较两个值,然后丢弃结果以设置标志。

有什么方法可以确定需要设置/取消设置哪些标志?我了解JEJNE 会查看是否设置了ZF,但我不确定其他分支操作。

这是我坚持的部分:

ClearLine:
    pushad                  ; Save all caller’s GP registers
    mov edx,15              ; We’re going to go 16 pokes, counting from 0
    .poke:  mov eax,1   
    sub edx,1
    jae .poke               ; Loop back if EDX >= 0
    popad                   
    ret                     `

如果 EDX >= 0,为什么JAE 会循环返回?如果 EDX >= 1,它不会循环吗?毕竟,SUB 操作就像CMP 操作一样,但多了一个保存结果的步骤。所以基本上说CMP edx,1不是说“如果第一个操作数(EDX)大于或等于第二个操作数(1)则跳转”?但是当我在调试器中测试它时,它显示它循环了 16 次,而不是 15 次。我不明白为什么会这样。

【问题讨论】:

  • 是的,你是对的。您可以在调试器中对其进行测试。另外,jae 是一个无符号比较,所以 >= 0 没有意义,对于所有无符号数字都是如此。
  • 当 edx=1 时,sub 会做:edx = 0, ZF=1, CF=0。所以jae 将循环到.poke。当edx=0时,sub会导致edx=0xFFFFFFFF,ZF=0,CF=1,即“jnae”或“jb”,所以会跳过jae。所以 jae 确实将 edx 值(在 sub 之前)从 15 跳回到 1 => 15 次 => +1 非跳转(edx=0 在 sub 之前)=> 循环将执行 16 次。
  • 如果它引用了减法之后的值,则该语句是有意义的。如果 edx 在 sub 之前为 1,则之后将为 0,但“比较”使用之前的值。使用 do { ... } while(edx-- >= 1) 的 C 语法(注意后增量)
  • 如果仍然不确定...做mov edx,0 => 循环将被执行1次。 mov edx,1 => 2 次,等等...因为它是 do { ... } while(...); 类型的循环,而不是 for(..) {},它将首先测试 edx 是否应该至少第一次执行循环。
  • 那么当使用跳转指令时,我怎么知道它和什么比较呢?

标签: assembly x86 conditional-statements branch


【解决方案1】:

根据问题中的措辞,您的困惑似乎至少部分源于没有正确地将比较指令与条件跳转指令分开。 CMP 首先设置标志,然后根据标志的状态进行条件跳转。有许多不同的指令设置标志(几乎所有算术和按位指令都设置标志;有关详细信息,请参阅每条指令的文档),其中没有 执行任何分支。为了根据标志进行分支,您需要一条Jcc 指令(其中cc 是条件代码,表示它将检查的标志,例如ae,表示“高于或等于")。

我指出这一点的原因是因为您说的是:

所以基本上说 CMP edx,1 不是说“如果第一个操作数 (EDX) 大于或等于第二个操作数 (1),则跳转”?

可能只是用来描述实际发生的事情的捷径,但仍然——它是一个不正确的心智模型,不可避免地会导致混乱。 CMP 指令从不进行任何跳转。它所做的只是设置标志。您是正确的,它设置标志的方式与减法 (SUB) 完全一样,但是在您执行读取它们并相应地分支的 Jcc 指令之前,标志不会任何事情。

虽然您已经了解它们,但我们将从JE/JZJNE/JNZ 开始,因为它们是最容易理解的条件。这些只是查看零标志 (ZF),并根据其状态进行分支。 JE 完全等同于 JZ。程序员可以选择两种不同的助记符,这取决于他们认为哪些会使他们的代码更清晰、更易于阅读。例如,当您执行CMP 时,跟在JE 后面通常是有意义的,因为逻辑上,如果两个值相等,您就会跳跃。 技术上,如果减法的结果是 0,你实际上是在跳跃,因为 CMP 设置了像 SUB 这样的标志,所以这就是为什么它 100% 等同于写 JZ,你只是不会经常看到程序员这样做。相反,当您执行TEST reg, reg 之类的操作时,您通常会看到后面跟着JZ,因为如果最后一个操作的结果为零,则将其视为跳跃更具语义。条件中加“不”,效果明显。

您可以找到一个非常有用的条件分支指令表here。我仍然发现自己会定期查阅这张桌子或类似的东西。作为初学者,最有用的将是助记符的文字描述。作为一个更高级的程序员,最有用的将是助记符到被检查的实际标志的映射。 (实际的代码字节有时也很方便。)

如您所见,JAE 表示“如果高于或等于则跳转”,这由进位标志 (CF) 的状态决定。如果进位未设置,则将采用分支。如果设置了进位,则执行将失败。正如该表还告诉您的,这对于 unsigned 比较很方便。为什么?因为这就是进位标志的用途。 I just wrote a lengthy answer explaining the carry and overflow flags here。它比您需要的要详细一些,但仍然包含相关位,例如这些标志的定义。

您还会在该图表中看到JAE 有多个助记符,就像我们在JEJZ 中看到的一样。替代的助记符是JNBJNC。第一个,JNB,非常明显——这与JAE 正好相反。如果一个值高于或等于另一个值,那么它也不低于该值。 JNC 只是对跳转所基于的标志的更文字描述:进位标志。同样,技术上您使用哪个并不重要,但如果您仔细选择,它通常会使您的代码语义更正确和可读。

有了这个概念性的理解,让我们更详细地看一下你的代码:

    mov edx, 15
.poke:
    mov eax, 1
    sub edx, 1
    jae .poke

(我不喜欢你的格式,所以我稍微重写了它。:-p)显然,这将EDX设置为15,然后进入循环。在循环内部,它从EDX 中减去 1 并设置标志。然后,下面的JAE 指令查看标志的状态并分支回到.poke(继续循环)当且仅当进位标志(CF)未设置。

另一种思考方式是,当且仅当 EDX 中的值大于或等于 1 时,循环才会继续。象征性地,就是:EDX >= 1。当然,除了这个符号表达式不能正确地表示我们正在进行 unsigned 比较。正如我在上面链接的另一个答案中提到的,CPU 不知道也不关心值是有符号还是无符号。这是程序员要解释的东西。您使用相同的 SUB(或 CMP)指令来执行有符号和无符号减法(比较)。改变的是您之后查看的标志。进位标志(CF)用于无符号减法/比较;溢出标志 (OF) 用于有符号比较/减法。

让我们看一下EDX 的几个示例值,以确保我们理解逻辑。

第一次循环,当EDX 为15 时,SUB 指令将15 减1。结果当然是14。因此,零标志(ZF)设置为0 (因为结果非零)。进位标志 (CF) 设置为 0,因为没有进位(没有无符号溢出)。溢出标志 (OF) 设置为 0,因为没有符号溢出。符号标志 (SF) 设置为 0,因为结果是无符号的(它的符号标志,即最高有效位,未设置,意味着值为正)。基于CF 的状态,JAE 将分支回.poke 并继续循环。从逻辑上讲,您将继续循环,因为EDX (15) 中的值大于或等于 1。

同样的事情会持续一段时间。我们将让循环旋转,然后在EDX 为 1 时中断它。现在,SUB 指令从 1 中减去 1。结果为 0。因此,ZF 为 1(结果为零),@ 987654380@ 为 0(未发生有符号溢出),CF 为 0(无进位,即无符号溢出),SF 为 0(结果无符号)。那么,分支会被占用吗?是的,CF 是 0。从逻辑上讲,1 大于或等于 1(当然,它等于)。

下一次,EDX 为 0,因此将从 0 中减去 1。结果为 -1。 ZF 为 0(结果非零),OF 为 0(未发生有符号溢出),CF 为 1(发生进位,即无符号溢出),SF 为 1(结果有符号)。这次采用分支,因为CF 为1。从逻辑上讲,这是有道理的,因为0 高于或等于1(记住,这是一个无符号比较)。

这就是它总共循环 16 次的原因。它在EDX 为 15 时循环,并继续循环直到 EDX 为 0。这是因为您的条件测试位于循环的 底部。也就是说,在 C 表示法中:

do
{
    ...
}
while (edx-- >= 1);

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-12-03
    • 2016-01-21
    相关资源
    最近更新 更多