机器指令翻译成 JavaScript —— No.2 跳转处理
上一篇,我们发现大多数 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 就是一个死循环,无法输入,也看不到输出,完全不符合浏览器的线程模型。
因此,还必须对流程进行改造,以一种可控的方式执行。
下一篇,我们讨论如何改造流程。