开发模拟器大家的第一反应往往是fopen打开 ROM ──malloc申请几兆内存 ──fread一把全塞进去。但在 16 位实模式下malloc最大只能给你 64KB 的段Segment。疯子的解法如果内存装不下我们为什么非要装下它让我们把思维逆转过来。在真实的世嘉 MD 主机里主 CPU (M68K) 的引脚是直接焊接在卡带插槽上的。当 CPU 需要读取下一条指令时它通过地址线发射电信号卡带芯片瞬间返回数据。在 DOS 环境下我们难道不能把 RAM Disk内存盘里的 ROM 文件 当成那块物理卡带吗我们的模拟器 PC 指针不再是一个实模式的内存地址而是一个unsigned long类型的文件偏移量。每当 M68K 的指令指针PC向前移动我们就调用fseek和fread去 RAM Disk 里临时抠出 2 个字节的机器码。猜想二“滑动窗口”降维打击 ── 徒手造一个 MMU既然每次读文件太慢全读进去又没地方放那我们就实现一个软分段Segmentation机制。在 640KB 的常规内存里我们牙缝里挤出 64KB开辟一个far数组缓冲区。这个 64KB 的缓冲区就是我们观察 4MB 卡带的“视口”。当 M68K 的逻辑 PC 指针在这个 64KB 范围内活动时我们的模拟器直接在内存里指针寻址速度快如闪电只有当《索尼克》打赢了 Boss发生远距离代码跳转JMPPC 指针飞出了这 64KB 边界时我们才触发一次“缺页异常” ── 刹车、移动文件指针、用 RAM Disk 重新填满这 64KB 缓冲区。看我们没有使用任何保护模式的硬件支持却用纯 C 语言逻辑在 16 位实模式里模拟出了现代 CPU 赖以生存的 MMU内存管理单元 核心思想。猜想三大端序与小端序的“迎头痛击”解决了卡带加载紧接着就是硬件底层不可调和的矛盾MD 的 M68000 是大端序Big-Endian高位字节在前低位字节在后。PC 的 8086/Pentium 是小端序Little-Endian低位字节在前高位字节在后。这意味着如果我们从卡带读取一条指令0x4E75M68K 的 RTS 返回指令直接存入 PC 内存PC 的 CPU 会把它理解成0x754E── 逻辑瞬间崩溃。如果我们在每读取一个字Word的时候都去用 C 语言做位移转换(data 8) | (data 8)Pentium 处理器那可怜的流水线会被这种无效的算术运算彻底挤爆。我们要搞就搞极致的在“滑动窗口”从 RAM Disk 读取 64KB 数据进内存的瞬间直接用一段内联汇编调用 8086 的硬件绝活 ──XCHG AH, AL。一个循环在一瞬间把整个 64KB 缓冲区的字节两两对调。这样后续的指令解码器在读取数据时就可以直接享受“免转换”的本地速度。猜想四在单任务 DOS 中编织双处理器的时间线世嘉 MD 内部是一套非常奇葩的“双 CPU 主从架构”主 CPU 是一代神芯 Motorola 68000副 CPU 则是从上个世代红白机、SMS 时代退役下来的 8 位老将 Z80。说白了MD 的日常就是大哥 M68K 在前面疯狂跑游戏程序、拼命渲染画面把小弟 Z80 死死按在椅子上吹喇叭专职负责播放 FM 音乐和音效。 两个 CPU 的频率比大约是 2.14 : 1它们通过一个狭窄的共享内存通道协作。可是我们的 MS-DOS 是个不折不扣的“单任务单线程”系统既没有现代操作系统的多线程Pthread并发更不懂得什么叫并行。我们要在只有单核的 PC 上怎么同时还原“大哥冲锋、小弟吹喇叭”的场面还不让声音和画面卡顿答案是我们只能化身为“微观时间的主宰”把它们俩按在同一个时间线上轮流摩擦。既然不能并行那我们就用时钟周期计数器Cycle Counter把时间切成碎屑。游戏画面的一帧在硬件上是由 262 条扫描线Scanline构成的。每一条扫描线正好对应大哥 M68K 执行 488 个时钟周期。那我们就写一个密不透风的大循环for (line 0; line 262; line) { execute_m68k(488); // 强制让大哥 M68K 跑 488 个周期算完这一行的画面 execute_z80(228); // 按照频率比例强制把 Z80 戳醒让它吹 228 个周期的喇叭 render_scanline(line); // 顺手把这一行的 VGA 图形吐到 PC 显存里 }两个处理器在我们的微观调度下就像拉锯一样交替向前推进时间。在宏观上看声音和画面就达成了完美的同步读者在 DOSBox 里听到的《索尼克》BGM 也就绝不会变调结语这是一场致敬也是一次底层的突围这场猜想可行吗逻辑上它完全闭环技术上它退无可退。我们放弃了现代操作系统提供的几吉字节GB的虚拟内存放弃了高级编译器的自动优化退回到了 1990 年代初那个只有 640KB 常规内存的荒凉世界。但正是这种极端的限制逼着我们去思考什么是地址总线什么是内存映射如何用纯粹的算法逻辑去跨越硬件代差的鸿沟这个极限挑战项目我已经正式立项。接下来我将尝试在 DOSBox 和 TC3 环境下敲下这个“16位实模式 MD 模拟器”的第一行代码。你觉得这个近乎疯狂的方案会在哪一个环节率先翻车是 M68K 变长指令集的解码地狱还是 VGA Mode 13h 的显存吞吐瓶颈欢迎在评论区留下你的毒奶和见解。下一期我们将正式解剖《索尼克 1》的 ROM Header看看游戏是如何在我们手搓的虚拟空间里睁开第一双眼睛的