Z80 REPL:在浏览器里一条一条敲 Z80 汇编指令
文章摘要
Z80 REPL 是一个跑在浏览器里的 Z80 CPU「交互式解释器」:像在 Python 提示符里敲表达式一样,你输入一条 Z80 汇编指令,按回车它就立即执行,然后把这条指令的机器码、耗费的时钟周期、寄存器和内存的变化全部显示出来。它出自日本独立游戏开发者长健太(Kenta Cho,GitHub 账号 abagames)之手,仓库创建于 2018 年 9 月,所以 HN 标题标注了「(2018)」。项目用 TypeScript 编写,CPU 模拟借用了开源的 Z80.js,三个终端面板都用 xterm.js 渲染。README 只有一句话和一张动图,下面的介绍主要根据页面实际行为和源码整理。
界面:三块终端。 页面从上到下是三个深色终端窗口:
- 内存视图:以十六进制显示地址 0x00–0xFF 这 256 个字节,16×16 的网格,右侧一栏把每个字节显示成可打印的 ASCII 字符(小于 0x20 的显示为点)。
- 寄存器栏:一行列出 A、B/C、D/E、H/L、IX、IY、PC、SP、标志位 SZYHXPNC(置位显示 o,清零显示点),以及 I 和 R 寄存器。
- REPL 输入区:启动时打印三行提示:「输入一条 Z80 指令并按回车执行,例如 ld c,20h、add a,c、push bc;按 Tab 显示候选指令。」
内存视图会用颜色把寄存器「指向」的位置标出来:PC 所在字节是红底,SP 是黄底,HL、BC、DE、IX、IY 指向的地址各用不同颜色,且与寄存器栏里对应寄存器名的颜色一致。所以当你执行 ld hl,40h 再 ld (hl),a,就能直接看到那个彩色字节变成 A 的值。初始状态下 PC 指向 0x80,SP 指向 0xF0,也就是说栈在内存视图的底部,push 时会看到黄色标记往上爬。
它实际上是边敲边「写程序」。 看源码就会发现,REPL 并不是凭空解释指令:它把你输入的指令汇编成字节,写入当前 PC 所指的内存,再让模拟器从那里执行。因此你敲下的每条指令都会在 0x80 往后依次落成真实的机器码,内存视图里能看到程序逐渐变长。执行完会输出类似 0E 20 (7 cycles) 的结果:0E 20 就是 ld c,20h 的机器码,7 是它的 T 周期数。
汇编器的实现相当取巧:源码里有一张约 1800 行的指令表,按操作码顺序排列,外加无前缀、ED、DD、FD、CB、DD CB、FD CB 七组前缀。你的输入先被规范化,转成大写、去掉空白,把数字替换成占位符 n(8 位)或 nn(16 位),然后到表里查找,得到的下标本身就编码了前缀和操作码;最后把立即数按小端序追加在后面。数字支持十进制和带 h 后缀的十六进制。
几个方便的交互细节:
- 空回车等于单步:不输入任何东西直接按回车,就从当前 PC 继续执行内存里已有的下一条指令,并显示它的机器码和周期。配合跳转指令,你可以先敲出一个小循环,再用
jp 80h跳回开头,然后连按回车一步步看它跑。 - 重复类指令的保护:执行逻辑是「一直跑,直到 PC 发生变化」,最多 64 次。像 LDIR 这类会原地重复执行的块传送指令,或者跳回自身的死循环,跑满 64 次还没动,就会提示「已循环 64 次,按回车继续」。
- Tab 补全:按 Tab 列出以当前输入开头的候选指令,最多显示 20 个,其余的提示「……还有 N 条」。
- 上下方向键浏览历史命令。
它没有的东西:没有 I/O,端口读永远返回 0、写入被忽略;没有屏幕显存,也没有标签和符号。想「输出」字符,只能把 ASCII 码写进内存,然后在内存视图右侧那一栏里看。
作为一件小玩具,它的价值在于把汇编语言最抽象的部分变得即时可见:一条 add a,c 之后哪些标志位翻转了,push bc 之后栈指针往哪走、写了哪两个字节,jr 为什么编码成一个相对偏移……不用配置汇编器、链接器和模拟器,打开网页就能摸。
HN 评论精华
24 条评论,篇幅不大。除了对工具本身的几条反馈,讨论很快变成了一场老电脑玩家的怀旧会:Apple II 的迷你汇编器、C64 的冻结卡带、BBC Micro 的内联汇编。
对工具本身的反馈。 networked 发现了一个小 bug:没有操作数的指令后面如果多一个空格就无法识别,nop 正常,nop 却报「unknown instruction」(带操作数的 ld c,20h 没问题)。对照源码能看出原因:规范化时会把第一个空格当作助记符和操作数之间的分隔符保留下来,于是 NOP 变成了带尾随空格的 NOP ,查表失败。他还希望 Tab 补全能像 fish shell 那样,不用多打字就能按 Tab 在候选项之间循环、按回车确认。j4cobgarby 建议内置几个可以直接载入内存的示例程序。jhallenworld 建议支持符号,并展示了他自己的 6800/6809 Exorciser 模拟器里的监控程序:输入汇编时可以写 start、skip、loop 这样的标签,遇到前向引用时它会在后面自动回填偏移量(打印「Offset at 1001 set to 02」),sy 命令列出符号表,u 命令反汇编。他觉得更好的做法是让汇编命令弹出一个内置的迷你编辑器,退出时再整体汇编。ilaksh 问有没有屏幕显存、怎么输出一个字符,talideon 回答:没有,随便挑一块内存往里写,右边那一栏会显示每个地址对应的可打印 ASCII 字符。torutofu 说有了即时反馈,Z80 的肌肉记忆回来得出奇地快。
关于作者。 jottinger 指出源码已经是 8 年前的了,「不过我还是很喜欢这个点子」。wkjagt 说「这让我更喜欢它了」。NewsaHackO 话里有话:「是啊,它没有那种……缺乏灵感的设计,看得出是真人自己做出来的。」Jare 介绍说,Kenta Cho 在过去 20 多年里做了数以百计的游戏和小玩具,「有很多非常精彩的东西」。
Apple II 那个被拆掉的迷你汇编器。 flyinghamster 说他对 Apple II Plus(相对于带 Integer BASIC 的初代)最大的遗憾,就是为了给自动启动功能腾出空间,Monitor ROM 里的迷你汇编器被拆掉了,「那是在这个工具之前,我见过的最接近汇编语言 REPL 的东西。好在反汇编器保住了」。EvanAnderson 回忆自己 10 岁时在公共图书馆的 Apple IIe 上照着一本 Apple II 时代的书做练习,怎么也调不出迷你汇编器,「功能会在后来的机器上被移除,这种事我当时根本想不到」。kevin_thibedeau 解释说,在 II+ 和 IIe 上从 Integer BASIC 里仍能调出迷你汇编器,后期型号的 IIe 又把它放回了 ROM。EvanAnderson 又说,当年他从 Applesoft 起步,觉得 Integer BASIC 莫名其妙,也不明白为什么进入 Applesoft 的命令叫「FP」,「如果那时有个导师,我大概能在更小的年纪走得更远」。之后他讲了一大段收藏故事:90 年代末在价格飞涨前攒了 IIe、II+ 和 IIgs,14 岁时淘到一本《Inside the Apple IIe》,整个暑假泡在图书馆那几台已经没人用的旧 IIe 前,从开门待到关门,玩双高分辨率图形、用磁带接口采样音频,还做了点简单的软件破解。
abecedarius 从 Applesoft 引出一段往事:Applesoft BASIC 的授权让微软手里有了一个「人质」,后来微软就用它逼苹果放弃了 MacBASIC,结果初代 Mac 出厂时没有任何用户可编程的环境,而此前人们普遍默认个人电脑应该可编程。Gracana 看完说自己之前完全不知道,现在很生气,「每台 Mac 都附带一个官方 BASIC,对小时候的我意义会非常巨大」。
其他平台的同类工具。 boomlinde 说 C64 圈子常用「冻结卡带」,可以随时中断正常运行、调出一个监控程序,通常带有简单的汇编器和反汇编器,用于打补丁和逆向非常方便;他还贴了 pagetable.com 上一篇文章,解释 The Final Cartridge III 是怎么利用不可屏蔽中断实现这一点的。Zeetah 补充说 Atari 8 位机上有个叫 Omnimon 的类似产品。ErroneousBosh 提到 BBC Micro 和更早的 Acorn Atom 直接把汇编器内置在 BASIC 里,「想想看,80 年代初,BASIC 不够快的话你就可以写内联汇编」。MomsAVoxell 回忆 13 岁时有一盒磁带,标签上写着「0 DEBUG」「10 ASS」「28 MEMHAK」「30 UNDEL」,数字是那段工具在磁带上的位置,每天放学回家第一件事就是把它们加载进来,做梦都想要一台 ROM 烧录器,把这些工具烧进最后 16KB 地址空间里。他的藏品里还有一台跑着古老 DOS 的 Commodore PC,上面有 DEBUG.com,而且感染着一个那个年代的早期病毒,按特定的启动路径操作,就能在调试器下看着病毒感染文件。他最后说 Z80 REPL 很讨喜,现在他想要一个 6502 REPL,并且已经忍不住在琢磨怎么把它弄到 Oric Atmos 上。