上一篇,我们通过内置解释器的方案,解决任意跳转的问题。同时,也提到另一个问题:如果指令发生变化,又该如何应对。
指令自改
如果指令加载到 RAM 中,那就和普通数据一样,也是可以随意修改的。然而,对应的 JS 是事先翻译好的,已经不能改了。如果运行时突然变卦,那相应的 JS 就作废了 —— 如果修改的是跳转指令,甚至会影响已划分的流程。所以为了保守起见,指令被改后进入模拟状态。
因此,我们得监控指令区的修改:
function store(addr, val) {
// 修改指令区
if (0x0600 <= addr && addr <= ...) {
...
}
MEM[addr] = val
}
将存储操作封装成函数,这样就可在写入时做一些判断。如果修改了指令区的数据,则进入解释模式(甚至永远保持解释模式)。
当然,还可以做一个 JIT 引擎,在运行时翻译。不过很复杂,这里就不考虑了。
对存储进行封装,这也是有必要的。之前为了简化问题,将整个地址空间都当做内存,其实是不严谨的。事实上,内存只是地址空间的一部分而已。其他还有屏幕、键盘,以及各种 ABI,都是通过地址交互的。
所以,读写最终都需要一层封装:
function store(addr, val) {
...
MEM[addr] = val;
}
function load(addr) {
...
return MEM[addr];
}
指令变化
如果不修改指令数据,指令是否也会变化?听起来有些匪夷所思。不过,在那个系统资源极度匮乏的年代,总会有些神奇的东西。
我们在第一篇中提到,地址总线是 16 位,最多只能访问 64K 的空间。这是否意味着,程序最大只能做到 64K?
回顾一下,小时候玩的红白机游戏(当然,那时都用卡带,也看不出游戏有多大)。后来有电脑模拟器,就可以看到 NES 文件的大小了。
事实上超过 64K 的多得是,有些「大型游戏」甚至有好几百 K,例如:
光靠 16 位的地址,是如何访问的?为此,前辈们发明了一种叫 Mapper 的黑科技。它内置在游戏卡带芯片里,对 CPU 是透明的。
Mapper 的种类五花八门,但基本思路差不多:用某些特殊地址,来扩展寻址能力。
假设 0x0000 ~ 0x0fff 空间是给卡带的。若把 0x0fff 这个特殊地址设置成 1,这时访问 0x0005,实际访问到的物理位置是 1 * N + 5;如果设置的是 2,再访问 0x0005,对应的物理位置则是 2 * N + 5。这样,就可突破 64K 的限制了。
相当于把特殊地址,当做类似段寄存器的功能。只不过这是卡带自己搞的一套内部方案,CPU 是毫不知情的。
当然,实际情况比这复杂得多。本系列我们讨论的仅仅是指令翻译,并非要实现一个 NES 环境,所以就不考虑了。
结尾
关于 6502 指令翻译成 JavaScript 的可行性分析,就到此为止。
我们之所以要翻译,而不是纯粹模拟,就是为了尝试追求最高效率。不过,就目前的翻译过程来看,仍存在很多可优化的地方。
下一篇,我们尝试利用现有工具,对逻辑进行深度优化。