是否可以在不使用`jump`和`goto`的情况下在汇编中做出决定?

时间:2016-09-02 09:54:46

标签: assembly

在这个question中,一些答案显示了如何在不使用“if”语句的情况下做出决策,但是我怀疑这是可能的,因为“if”不是生成jump的唯一语句指令。

给定一个固定的编译语言(例如C ++),生成的程序集可以在不使用gotogulpfile.js指令的情况下做出某种决定吗?

请举例说明在肯定答案的情况下不使用此类说明的简单if / else语句。

3 个答案:

答案 0 :(得分:6)

ARM体系结构具有一个有趣的条件执行特性。在完全ARM模式下运行,几乎每条指令都可以附加条件。这些条件与B牧场指令中使用的条件相同。诸如add r0, r0, #15之类的指令将始终执行,但是addeq r0, r0, #15之类的指令只有在设置了零标志时才会执行。使用分支的等价物将是:

    beq afteradd       ; skip add if equal
    add r0, r0, #15    ; add 15 to R0
afteradd:

在使用Thumb-2的核心上运行时,条件执行更受限制,但仍可以使用IT指令。该指令将在不使用分支的情况下创建“if-then”构造。 IT构造实际上是ARM的统一汇编语言的一部分,无论您是为ARM还是Thumb-2编写,都应该使用它。这是上面条件添加的UAL版本。

it eq                 ; if equal
addeq r0, r0, #15     ; then add 15 to r0

IT构造可以包含多个指令。在指令中使用一系列TE,您可以添加更多条件指令。在下面的示例中,如果设置了零标志,我们将向R0添加15,否则我们将从R0中减去15。 ITE相当于字面意思,if-then-else。第一条指令应该具有与ITE条件匹配的条件,然后第二条指令将成为“else”,并且条件应该与ITE条件相反。

ite eq                ; if equal
addeq r0, r0, #15     ; then add 15 to r0
subne r0, r0, #15     ; else subtract 15 from r0

我想这种情况确实会使用,这可能违背了你的要求。在执行方面,这实现了if而不使用跳转。

答案 1 :(得分:4)

假设你的意思是x86,

  1. 条件移动(cmov)如上所述,
  2. 较旧的设置说明(根据标志设置)
  3. pushf / popf solutions
  4. SSE2(64位x86_64上的默认设置的一部分)具有pcmpeqb和类似指令,以便将比较结果记录到寄存器中。
  5. FPU指令,如FCOM + LAHF(来自Ruslan,见下文)
  6. 使用查找表将输入值映射到输出值(来自下面的Ped7g)
  7. 所有允许在寄存器中得到决策结果(cmp *),可以通过移位和结果进一步操作它们。

    这主要用于创建没有分支的函数返回值或用于流处理的某些形式(例如,基于SSE2的图像二值化)。它实际上并不是分支的通用替代品。

答案 2 :(得分:4)

请参阅@Marco van de Voort的答案,获取简单"正常"的列表写无分支代码的方法(cmov /其他预测执行,cmp / setcc,复制标志到GP寄存器等)。这构成了构建无分支版本事物的构建块,这些构建块可以通过分支完成。

当你说"做出决定"时,听起来你真的在问这样的机器是否可以完全编程。

计算机科学计算理论的一部分是弄清楚不同的models of computation有多强大。这不是关于模型的有效性,而是关于什么可以用它计算什么。我怀疑没有条件分支指令的寄存器机器不会是equivalent in power to a Turing machine, aka Universal Computer。 (即使您允许非间接无条件分支)。

更新:x86's mov instruction is Turing-complete即使没有自我修改代码,只需使用普通的索引寻址模式。您还需要围绕加载和存储序列循环。 TODO:重写此答案无效的部分。这增加了无条件分支的值而没有自修改代码(例如让IP在实模式下环绕,或者在没有jmp的情况下执行此操作的其他奇怪技巧,见下文。)

我认为mov - 只有图灵机有点模拟x86的寄存器和内存中的另一台机器,所以不是x86 [R / E] IP分支,而是通用寄存器中的值表现得像一个程序计数器分支。 (或者是一台穿越磁带的图灵机)

你可以制作非常简单的体系结构仍然是Univeral计算机,但我从目前为止所看到的是任何类似于register machine的计算模型(如x86,ARM,等)需要某种版本的条件分支作为通用计算机。

另见这个问题:Minimal instruction set to solve any problem with a computer program,特别是维基百科的One Instruction Set Computer文章的比特操纵机器部分;它至少像一个条件分支,但仍然做同样的工作。

我不太了解计算理论,无法在没有条件分支的情况下对可以计算的内容进行准确的界限或正式描述(在寄存器机器上) x86或ARM),但我可以说些什么:

我可以说,给定足够的指令空间,您可以对数组进行排序,但也许只有在为该数组长度生成正确的指令序列时才可以。使用像cmov这样的无分支技术,您可以根据比较有条件地交换两个内存位置。一个很好的算法选择是sorting network

您可以完全展开已知长度的任何循环,也可以通过展开您将需要的最大迭代次数来处理可变长度循环。当循环计数器超过数组末尾时,您可以使用无分支技术从虚拟地址加载/存储。 (例如,使用cmov将指针设置为指向一个小的静态数组,该数组仅用于存储垃圾而不会影响其他任何内容。您的代码将始终使用相同的固定数量的存储,但使用不同的输入不同的存储可以进入倾销场。)ARM预测任何指令的执行意味着你可以在不需要静态转储的情况下进行条件存储。

完全展开Merge Sort应该可以正常工作,因为对于给定的数组大小,它总是执行相同的工作量。另一方面,快速排序会非常不方便,因为工作量因数据和分区选择而异。此外,它有一个O(n ^ 2)最坏的情况,所以你至少需要那么多代码。

由于您必须完全展开您想要运行的任何内容,这意味着您需要知道它将运行多长时间。这意味着您无法实现完整的图灵机,因为并非所有图灵机停止。 (更不用说它是impossible for an algorithm to exist决定是否任何给定的图灵机停止了。)

自修改代码可能会提高效率,但如果您仍然不允许重写自己的机器代码以包含分支指令,我认为它不会增加计算能力。 (我们必须排除SMC生成任何类型的分支,而不仅仅是条件分支指令,因为我们可以有条件地生成无条件分支指令。)

我认为固定循环中的SMC可能比循环中的非自修改代码更强大,即使SMC不允许生成跳转指令。请参阅下面的一些关于没有分支指令的循环的疯狂想法(例如,计时器中断和IP环绕)。

只使用无条件跳转(无条件跳转),您可以创建循环来处理相同问题的任意大小版本,而不进行跳转。但是那时你的程序无法终止。如果有某种"终止程序"指令,把它放在循环中会使它成为一个非循环。

所以你最终得到的是一个程序,它最终有一个数组排序(或者它的工作是什么),也许它通过存储一个" true"来表示这个事实。在某处,并保持循环而不影响数据(使用无分支条件代码)。您可以将此视为具有"传输触发的"退出功能,因此可以有条件地使用。

更多方法来模拟无条件跳转以便您可以创建循环,我认为增加自修改代码的功能。 (即使你不使用那种力量来有条件地分支)。

函数调用指令很容易被滥用来实现普通跳转,所以我们显然想要排除它们。

在x86上,您可以使用call跳转到只弹出堆栈返回地址而不是返回的代码。 (所以你可以实现一个循环)。要使用x86' indirect call instruction构建条件分支,您可以使用无分支技术(如cmov)将调用后的指令或不同的目标放入寄存器中,然后运行{{1 }}。或者更简单地说,如果将eax设置为0或1(例如来自setcc),则可以运行call rax在分支的两个目的地之间进行选择。

同样,call [target_table + rax*8]很容易被滥用为间接跳转:ret地址并运行push。 (不要在现实生活中这样做:CPU针对正确的调用/返回嵌套进行了优化,并且它们不匹配会导致返回地址预测器堆栈在未来的RET上进行错误预测。)

中断导致CPU跳转到中断处理程序。软件中断(如x86和int 0x80指令,或ARM svc / ret)同步跳转到中断处理程序,因此它们实际上只是一个函数(和模式切换到内核模式)。

这意味着swi是我们需要排除的另一个跳转指令。但我们也可以使用定时器中断来循环。中断处理程序是循环的顶部。做你想做的任何工作,然后重新启用中断。之后执行NOP;下一次定时器中断最终会触发。或者在服务中断时启用中断的机器上:将循环体限制为可以始终适应定时器中断的工作量,即使在最坏情况下的缓存行为也是如此。

软件生成中断的另一种方法是运行非法指令。自修改代码可以通过有条件地生成非法指令有条件地分支到非法指令中断处理程序。或通过有条理地除以0有条件地分支到除错处理程序。因为你可以设置该中断处理程序的地址(例如通过存储到Interrupt Vector/Descriptor Table on x86),你可以有条件地分支到使用存储的任意地址和除以0.(所有分支目标都需要执行必要的操作,以便将该中断重新启用为可触发的。)

让程序计数器回滚:在平面内存的von-Neumann架构机器上,这需要自我修改代码来跟踪程序状态,因为没有内存没有作为代码执行。

在分段x86(例如16位实模式)IP will wrap around from 0xFFFF to 0x0000中。因此,您的循环是整个代码段,并且您在其他段中具有单独的数据存储。您只能使用intCS / far jmp(或中断/中断返回)修改far call,因此您无法执行{{1}之类的操作跳转(因为far ret不存在,与其他pop-segment-register指令不同)。

有趣的事实:代码段末尾的最后一条指令不必以0xFFFF结束。例如多字节指令可以从0xFFFF开始,到0x04结束。如果从不同的起始点解码,可以编写执行不同方式的x86机器代码,但这在此处没有用。 (这对你可以做的事情施加了很大的限制。)

ARM使程序计数器可作为普通通用寄存器访问,因此您可以使用pop cs或其他东西进行控制流程,而不是技术上使用pop cs(分支)或任何类型的呼叫(sub r15, #1024)或返回指令。但是,您仍然直接修改程序计数器,因此它实际上只是一个分支指令。 (bbl)的另一个名称。