机器指令翻译成 JavaScript —— No.2 跳转处理

时间:2022-03-07 20:20:09

上一篇,我们发现大多数 6502 指令都可以直接 1:1 翻译成 JS 代码,但除了「跳转指令」。

跳转指令,分无条件跳转、条件跳转。从另一个角度,也可分:

  • 静态跳转:目标地址已知

  • 动态跳转:目标地址运行时才知道

为了让问题更简单,本文只讨论「静态跳转」,并且是在固定的指令之间跳转。

我们用 JXX 表示跳转指令,XXX 表示其他指令:

L1:
XXX
JXX L1 -
XXX
JXX L2 +
XXX
L2:
XXX

这里 JXX L1 跳转到之前的位置,暂且称为「负跳转」。相应的,JXX L2 跳到之后的位置,称为「正跳转」。

正跳转

先来讨论正跳转。因为正跳转只会离终点越来越近,所以简单一些。先看个例子:

    XXX 1
JXX L1 --.
XXX 2 |
JXX L2 --|--.
L1: | |
XXX 3 <-| |
L2: |
XXX 4 <-|

这个分支很简单,可轻松翻译成 JS 代码:

XXX 1
if (...) {
XXX 3
} else {
XXX 2
}
XXX 4

但是,遇到稍复杂的分支,用 if..else.. 就会显得力不从心,因此需要用 break 来控制流程。

然而 break 只能用在循环里 —— 没事,套个空循环就可以:

do {
...
if (...) break // JXX L1
...
if (...) break // JXX L1
...
} while (0)
... // L1:

我们把 do 放在程序顶部,while 放在目标位置之前。这样,用 break 即可实现跳转 —— 退出 while,进入目标位置。

在很多语言里 do..while(0) 都有一些妙用,这是个有趣的语法糖。

当然,如果有多个不同的跳转,就需要嵌套多个 do 了。这时,如何指定 break 对应的层次?

熟悉 JS 的朋友都知道,JS 虽然没有 goto,但 label 的概念还是存在的。因此我们给每个 do 都贴上标签,同时 break 也指明标签:

a1: do {
a2: do {
...
if (...) break a1 // JXX L1
...
if (...) break a2 // JXX L2
...
} while (0) // L2:
...
} while (0) // L1:
...

我们把 do 放在最开头;每个 while(0) 对应一个目标位置。这样就实现了「正跳转」的翻译!

负跳转

现实中,不可能全是正跳转,否则程序一下子就执行到底了。负跳转,则可跳回先前位置,反复执行指令。

对于负跳转,是否也能用类似的方案?看起来好像不难,把 break 换成 continue,就可以回到 do 的后面。

a1: do {                    // L1
...
a2: do { // L2
...
...
if (...) continue a1 // JXX L1
...
} while (0)
...
if (...) continue a2 // JXX L2
...
} while (0)
...

对于这种简单的分支,貌似还行得通。但是对于稍复杂的情况,例如原本就已存在正跳转:

a1: do {
...
if (...) break --.
... |
if (...) break --|
... |
DST | <-.
... | |
} while (0) <-! |
... ???
... |
SRC --!

我们打算从 SRC 跳到 DST 位置 —— 这时的负跳转,又该如何实现?

因为 do..while 是单向的,只能从里面跳出来,无法从外面跳进去。

也许你会说,可以在 DST 处放一个 do。很不幸,这个 do 会和 a1 的 while 结合,完全不符合我们的意图。

简单地说,do..while 只能嵌套,无法交叉。

所以,负跳转很难翻译成 JS 代码。


退一步说,就算能翻译出来,最终也未必适用。因为传统程序,很多是这样的逻辑:

BEGIN:
输入查询
程序逻辑
输出结果
空跑耗时
GOTO BEGIN

这翻译成 JS 就是一个死循环,无法输入,也看不到输出,完全不符合浏览器的线程模型。

因此,还必须对流程进行改造,以一种可控的方式执行。

下一篇,我们讨论如何改造流程。