玩具 CPU:亲手体验一次“没有翻译官”
这是一台假想的小计算机,我们叫它小豆 CPU。它有 64 个字节的内存、一个累加器 A、一个输出口, 一共只有 12 条指令。你可以用三种方式给它输入同一段程序,然后数一数你按了多少下—— 这个数字,就是“汇编器”这份工作省下来的力气。
- 在汇编模式里点一个示例(比如「① 3 + 4」),按「运行」,看输出。
- 切到十六进制模式:刚才那段程序变成了数字。这就是没有汇编器时你要亲手敲的东西。
- 切到拨开关模式:数字又变成了一格一格的 0/1 开关。当年 Altair 8800 就是这样开机的。
- 看一眼右上角的按键计数:同一个程序,三种方式的代价差多少。
输入程序
写助记符,让汇编器替你查表、算地址。示例可以直接点。
(还没有汇编出任何东西)
机器只认数字。一条指令 2 个字节:先写“干什么”,再写“对哪个位置干”。 这一格一格敲进去的体验,就是 1950 年代程序员对着指令表干活的样子。
(还没有输入任何数字)
一格代表一位:亮起来是 1,暗着是 0。八格拼成一个字节,两格拼成一条指令。 面板只给你 16 个字节——当年的机器也一样小气。
你拨出来的字节(十六进制):
CPU 状态
| 状态 | 还没有装载程序 |
|---|---|
| 累加器 A | 0 |
| 程序计数器 PC | 0 |
| 已执行步数 | 0 |
输出口
内存(64 字节,橙色是程序占用,绿色是刚执行的字节)
这台小机器的指令表
一条指令 2 个字节:先写操作码,再写操作数。所有数字都可以写成十进制或 0x 开头的十六进制。
| 写法 | 编码 | 意思 |
|---|---|---|
HLT | 0 | 停机(内存里没写过的地方都是 0,所以程序跑飞了会自己停下) |
LDA m | 1 | 把内存第 m 个字节装进 A |
STA m | 2 | 把 A 存进内存第 m 个字节 |
ADD m | 3 | A = A + 内存[m] |
SUB m | 4 | A = A − 内存[m] |
JMP m | 5 | 跳到地址 m |
JZ m | 6 | A 等于 0 就跳到地址 m |
OUT | 7 | 把 A 打印出来 |
JNZ m | 8 | A 不等于 0 就跳到地址 m |
LDI n | 9 | A = n(把一个立刻数装进 A) |
ADI n | 10 | A = A + n |
SBI n | 11 | A = A − n |
DB n | — | 直接放一个字节的数据(0 – 255) |
五个挑战
- 热身:让机器打印出 42。(三条指令就够:
LDI 42/OUT/HLT) - 算数:打印 7 + 3 的结果。
- 循环:从 10 倒数到 1(提示:看示例②怎么做的)。
- 内存:算出 1+2+…+10(提示:示例③把 5 改成 10,答案应该是 55)。
- 最硬的一个:切到拨开关模式,用开关拨出「打印 6」这个程序,然后看看你按了多少下; 再切回汇编模式做同一件事,比较两个数字。这个差值,就是第 3 站讲的“翻译官”省下来的力气。
这台小机器不会乘法、不会比较大小、连“把两个数加起来存起来”都要绕好几步。 但它有跳转和条件跳转——就靠这两件事,它能做循环、能做判断, 也就是能做任何计算机能做的事。第 4 站讲的“256 字节种子”之所以可能,就是因为最底下的小程序只需要这么一点点能力。
它缺一个“把 A 变成 A + A”的指令,所以示例④要靠内存绕一圈。 真实 CPU 的指令表有几百上千条,而每一条都要由硬件电路去实现。 那么——你觉得是“加一条指令”难,还是“写一个汇编器”难?
回到你的问题
你一开始问的是:没有程序的时候,人们怎么编程?
玩过这一站,你其实已经亲手做过一遍了:
拨开关、敲数字,就是那个年代的“编程”。而自举的意思是——
用最笨的方式写下第一段能干活的东西,然后让它替你写出更好的工具,一层一层往上走。
走到今天,你打开编辑器写 print("hi") 的时候,脚下踩着的是七十多年的台阶。