芯片的设计到底有多难?想要回答这个问题最好还是先自己实践一下。最近,来自 BBC 的一名资深软件工程师 Daniel Harper 使用 Go 语言成功模拟出了一个 CPU 的所有功能,并把自己的经历写成了博客,引起了人们的热议。这篇文章也告诉我们:完整地了解计算机的工作原理是多么重要。
Daniel 的经历在社交网络上不仅被好奇的群众点赞,也引发了大学芯片相关课程的学生和助教们的共鸣。如果你不知道 L1/L2 缓存的意义,认为自己没有搞清楚英特尔、ARM 芯片上著名的 Meltdown、Spectre 漏洞的意义,现在是时候开始学习了。
让我们看看 Daniel 是如何做到模拟 CPU 的:
几个月前,我实在不理解计算机如何在后台工作,也不清楚现代计算机的工作原理。之后,我读了 J. Clark Scott 的书籍《But How Do I Know?》,这本书讲述了一台与非门(NAND gate)8 位计算机,包括寄存器、RAM 以及 CPU、ALU 和 I/O 的位元,于是我想用代码进行模拟。
我对电路学知识的兴趣不大,而这本书只是简单概述了一些基础知识,包括接线以及在没有必备电气工程知识的情况下位元如何在计算系统中移动。对我而言,从这本书中获得的知识不多,所以必须亲身实践,并从不可避免的错误中吸取经验教训,这样使我能够有条理地用代码编写电路。
这台简单的计算机可以用来计算。
示例程序
这是一个相当整洁的小东西,CPU 代码被实现为一个开闭的门,但它奏效了,我已经进行了测试,以此证明它能够运行。
该程序操控键盘输入,并将文本呈现给显示器,其中使用了一套精心制作的字形作为专业字体,我将其命名为「Daniel Code Pro」。唯一作弊的一点是获取键盘输入并显示输出内容,为此我必须通过 GLFW 与外界通信,但剩余部分是模拟电路。
我甚至编写了一个简单的汇编程序,这令人大开眼界。这并不是完美的,实际上有点胡扯。但是,我了解到了其他人很多年前已经解决了的问题,并认为自己的这项工作更好。
我曾看到一个 13 岁的孩子在 Minecraft 中做这项工作,所以等你用电报继电器制作出一个真正的 CPU 时再来质问我吧!
我心中的计算模型还停留在计算机科学初级教科书的层面,并且驱动我在 2013 年所编写的 Gameboy Emulator 的 CPU 与现今计算机运行的 CPU 完全不同。甚至可以说,模拟器只不过是一种状态机(state machine),它没有从逻辑门(logic gate)层面描述 CPU。仅使用 switch 语句即可以实现大多数 CPU 且能够存储寄存器状态。
我不知道 L1/L2 缓存(cache)和 pipelining 是什么,也完全不确定自己是否理解 Meltdown 和 Spectre 漏洞论文,所以想要更深入地了解这些东西。一些人告诉我,他们正在优化代码以更好地利用 CPU 缓存,而我却不知道如何验证真假,只能选择相信他们。我真的不确定 x86 指令是什么。我不了解人们如何能够将工作交给 GPU 或 TPU 处理,也不清楚什么是 TPU,更不清楚如何使用这些 SIMD 指令。
但所有这一切都需要具有相关的基础知识,所以我只有在阅读相关书籍后才能对此有所了解。这意味着我需要回到基础知识和原理上面,并从一些简单的程序上手。《But How Do I Know?》一本书中的「Scott 计算机」就很简单,也是我选择它的原因。
Scott 计算机是一个附有 256 字节 RAM 的 8 位处理器,并通过 8 位系统总线连接。该计算机拥有 4 个通用寄存器,能够执行 17 个机器指令。一些人搭建了一个很酷的视觉模拟器(visual simulator),无法想象需要花费多长时间才能跟踪全部的接线状态!
组成 Scott CPU 的所有组件图解
《But How Do I Know?》这本书将带你从一个不起眼的与非门开始,然后是内存和寄存器,最后继续对这些组件分层,直到你得出与上述类似的结果。这本书对相关内容做了非常好的概述,所以我极力推荐大家阅读,即使你已经熟悉了相关概念。我不建议大家在 Kindle 上阅读,因为书中的一些图有时很难在屏幕上放大和辨认,这是 Kindle 的一大弊端。唯一不同的一点是我将计算机升级到了 16 位,因为仅存储 ASCII 表的字形就令书中所描述的大多数 8 位机器无法做到,因此留给有用代码的空间就不多了。
开发过程实际上只是阅读文本、查找图表、然后尝试使用通用编程语言代码(而不是使用为集成电路开发而设计的代码)来翻译。
之所以用 Go 语言来写,是因为我对 Go 了解一点。杠精们可能会说,我不信你没有将时间花在学 VHDL、Verilog 或 LogSim 上,但我那时已经编写好了我的位元、字节和 NAND,我陷得太深了。也许我接下来会学那些东西。
从全局来看,大多数计算机只是传递一堆布尔值,所以任何对布尔值友好的语言都可以完成这项工作。
将模式应用于这些布尔值能够帮助程序员获得其含义,任何人都要做的最大决策是确定系统将使用哪种字节顺序(endianness),并确保所有组件都以正确的顺序在总线之间传递信息。
这绝对是实现中隐藏的痛点之一。从偏移量上看,我选择了较小的字节顺序。但在测试 ALU 时,我就遇到麻烦了。我试图找出为什么出来的数字是错误的。很多很多打印语句都发生在这个上面。
开发的确花了一些时间,大约是一两个月的业余时间。但一旦成功搭建出 CPU 并用它执行 2 + 2 = 5,我还是感到很欣慰。
书中讨论了 I/O 特性,设计了一个简单的键盘和显示界面,这样你就可以把东西放进机器或拿出来。我给自己设定了一个目标,那就是能够在键盘上输入一些东西,并在显示器上显示这些字母。
在这里,外设使用的是适配模式,充当 CPU 和外部世界之间的硬件接口。这里并不难猜,肯定是软件设计模式获取灵感的地方。
I/O 适配器是如何连接到 GLFW 窗口的
通过分离关注点,使用 GLFW 将键盘输入的内容输出到屏幕是非常简单的过程。事实上我只是从模拟器中提取了大部分代码并整形了一下,使用 go 通道作为进出机器的信号。
这可能是最难的一部分,至少也是最麻烦的。用如此有限的指令集编写程序集真的很糟糕。使用我编写的粗糙的汇编程序编写程序集更糟糕,因为你怪不得别人。
最大的问题在于同时处理这 4 个寄存器并跟踪它们,将它们作为临时存储存储到内存中。在这个过程中,我记得 Gameboy CPU 有一个堆栈指针寄存器,这样你就可以推送和弹出状态。不幸的是,这台电脑没有这么奢侈,所以我主要是在定制的基础上对内存里的东西进行移进移出操作。
我唯一花时间实现的伪指令是 CALL,以帮助调用函数。这可以让你运行一个函数,然后在函数被调用后返回到该点。由于没有堆栈,你只能调用一层的深度。
由于机器不支持中断,为获取键盘状态等函数,你必须实现糟糕的轮询代码。书中的确提到了实现中断的步骤,但那需要写更多代码。
无论如何,我最终编写出了四个程序,其中多数程序使用一些共享代码来绘制字体、获取键盘输入等。虽然与操作系统还有一定的距离,但它确实让我意识到一个简单的操作系统也可能提供一些服务。
但这其实并不容易,文本编写程序最棘手的部分是计算出何时转到新行,或当你按回车键时发生了什么。
<code>main-getInput:</code> <code> CALL ROUTINE-io-pollKeyboard</code><code> </code> <code><code> </code>CALL ROUTINE-io-drawFontCharacter</code><code> </code> <code><code> </code>JMP main-getInput</code>
上述文本编写程序的主要循环。
我也没有抽出时间来实现退格键或其他任何修改键。这让我意识到制作文本编辑器需要做多少工作,这项工作可能是多么乏味。
这个项目对我来说非常有趣,也很有收获。在用汇编语言编程的过程中,我基本上放弃了底层的 NAND、AND 和 OR 门。我上升到了上面的抽象层。虽然我做的这个 CPU 很简单,距离电脑里的 CPU 还很远,但通过这个项目我学到了很多,如:
如果你对于芯片的工作原理非常有兴趣,先上一些在线课程也是一个好方法。这一 Udacity 免费课程《高性能计算架构》源自佐治亚理工:
原文地址:https://djhworld.github.io/post/2019/05/21/i-dont-know-how-cpus-work-so-i-simulated-one-in-code/
领取专属 10元无门槛券
私享最新 技术干货