在使用自制C编译器的自制CPU上运行类Unix操作系统

2020-10-07 08:43:13

我开始做软件工程师已经两年了,我有时会给同事们讲我在大学三年级做的一个学生项目,它非常受欢迎,所以我写了这篇文章。1个。

现在,让我问你一个问题。你有没有设计过你自己的ISA,在FPGA上建立了一个ISA的处理器,并为它构建了一个编译器?另外,你在那个处理器上运行过操作系统吗?实际上,我们有。

在这篇文章中,我将谈论我在2015年的本科生活,我们花了四个月的时间在自制的RISC ISA上构建了一个自制的CPU,构建了一个自制的C工具链,并将类似Unix的操作系统Xv6移植到了这个CPU上。

这都是一个学生实验项目,叫做CPU实验。所以,让我们从什么是CPU实验开始。

CPU实验是我所在的东京大学信息科学系大三冬天举行的一项著名的小练习。在实验中,学生们被分成4到5个学生一组,每组设计自己的CPU体系结构,在FPGA上实现,为该CPU构建OCaml子集编译器,然后在CPU上运行给定的光线跟踪程序。通常,CPU、FPU、CPU模拟器和编译器各由一到两个人负责,我在我所在的组第六组负责CPU。

这个练习以自学期望值很高而闻名。老师只要求学生“把这个用OCaml编写的光线追踪程序在你的FPGA上实现的CPU上运行”,课程结束了。他/她没有讲太多关于如何编写CPU和编译器的具体步骤。学生们自己学习如何把以前课程中学到的CPU和编译器的常识体现到实际电路和代码的水平。嗯,这是一个非常困难的练习,但非常令人兴奋和有教育意义。

你们中的一些人可能已经注意到,我根本没有谈到操作系统,我会补充一点解释。

通常情况下,实验是这样进行的。首先,你做了一个工作可靠的CPU,不管它有多慢。如果你能做一个工作正常的CPU,并成功地运行光线跟踪程序,你就可以赢得实验的荣誉。之后,你的团队就有空闲时间了。传统的利用这一空闲时间的方式是进一步加快CPU的速度。在过去的实验中,学生们已经制造了无序CPU、VLIEW CPU、多核CPU,甚至超标量CPU,这是令人惊叹的。在过去的实验中,学生们制造了无序CPU、VLIEW CPU、多核CPU,甚至超标量CPU,这是令人惊叹的。

然而,一些团队将更多的精力投入到有趣的事情上,比如通过将扬声器连接到他们的CPU来运行游戏或播放音乐。我所属的第6组就是一群热爱娱乐的人,我们决定运行OS作为我们的团队目标。

由于其他小组对这一想法表现出兴趣,一个由大约8人组成的联合小组X成立了,他们的目标是“让我们在自己的CPU上运行操作系统!”

虽然我在第六组负责创建CPU,但这次我选择了担任X组操作系统团队的领导,所以这篇文章主要是从操作系统团队的角度来写的,当然我也会介绍整个团队的成果。

作为要移植的操作系统,我们选择了Xv6,这是麻省理工学院为教育目的而创建的一个简单的Unix v6操作系统。Xv6是用ANSI C编写的,与Unix v6不同,它在x86上运行。Xv6是一个教育操作系统,所以它的功能有点差,但作为一个简单的类Unix操作系统,它有足够的功能。您可以在维基百科或GitHub存储库找到更多关于Xv6的信息。

在移植xv6时,仅在软件方面就有很多挑战,因为我们试图从头开始创建一切。

在CPU实验中,我们通常创建一个ML编译器。当然,您不能编译Xv6的C代码。

特权保护?虚拟地址?中断?是的,我们通过讲座对操作系统的作用有了全面的了解,但我们没有足够的扎实的理解来解释当时有哪些具体的CPU功能可以实现这一点。

我们在CPU实验的核心部分制作了一个模拟器,但它是一个简单的模拟器,按指令执行一条指令,没有中断,也没有虚拟地址转换。

Xv6的可移植性不是很好,例如,它假设char是1字节,int是4字节,并且对堆栈进行大量操作,我猜“Xv6”这个名字来自于x86和Unix“v6”,所以这是很自然的。

我们有很多顾虑,但是在12月份开始了X组的OS移植项目。从这里开始,我将按照大致的时间顺序来写我们所做的事情。篇幅有点长,所以如果你想快速查看我们的最终产品,请跳到3月份。

我们看到答案的第一个问题是编译器和工具链,令人惊讶的是,我们的决定是从头做起C89编译器,老实说,我没有想到我们会选择这样的方式,我记得我一开始和X集团负责cpu的Yuichi谈过做一个GCC或llvm移植的事情。

然而,团队成员之一Keiichi突然说他编写了一个C编译器,并向我们展示了一个具有简单解析器和发射器的编译器原型。从头开始编写工具链似乎更有趣,所以我们决定自己编写编译器。

来自第3组的Yuichi和Wataru当年已经完成了实验的核心部分,他们加入了Keiichi,X组的编译器团队诞生了,我们后来将我们的编译器命名为UCC。

12月初,我完成了CPU实验,第6组完成了CPU实验的核心部分,于是我们进入了有趣的部分--X组的OS移植任务,这时,我和第6组的Shohei开始在X组工作,成为OS团队。正吉也同时加入了。

顺便说一句,我猜没有多少软件工程师写过CPU,所以让我来谈谈CPU的制造。

如今,制造CPU并不意味着在面包板上布线每一条跳线;你用硬件描述语言编写电路,然后使用Vivado或Quartus将HDL综合成实际电路。这一过程称为逻辑综合,而不是编译。

HDL和编程语言是相似但不同的。认为它就像是编写一个函数,将寄存器的信号状态映射到由时钟或输入信号触发的另一个信号状态。如果你想体验真正的反应式编程,我建议你尝试编写一个HDL。请记住编写HDL,总是担心你编写的HDL的信号传播是否真的在一个时钟内结束。否则,人类将无法理解电路的行为。

实际开发中最难的是,这个逻辑综合花了很长时间,我们在开始综合后要等上30分钟的情况并不少见,所以一旦开始综合,我就经常和其他也在等待综合完成的CPU人玩Smash Bros.混战,供大家参考一下,我的角色是酋长(Sheik),我的角色是酋长(Sheik)。

我们开始找到“操作系统需要什么样的CPU功能?”的答案。

在OS团队诞生后,我们开始了每周一轮的Xv6源代码阅读活动。

同时,我开始将Xv6移植到MIPS。这部分是为了了解操作系统如何在实现级别工作,部分原因是似乎没有Xv6端口到MIPS。我完成了移植,直到进程调度程序在大约一周后启动。在这个移植过程中,我对MIPS和x86做了大量研究,以了解xv6的工作原理。多亏了这一点,我在实现级别上了解了有关中断和MMU的机制。在这个阶段,我对Xv6所需的CPU功能有了很好的理解。

此外,在1月中旬,我们通过注释掉各个部分来努力编译整个Xv6代码。结果,我们自制体系结构模拟器上的Xv6显示了引导序列的第一条消息,

同时,这意味着UCC已经成长到足以编译大部分xv6,这真是太棒了。2个。

在MIPS端口,我完成了PIC的初始化,这是一件非常痛苦的事情,同时也完成了中断处理程序的实现,因此,Xv6到MIPS的移植直到第一个用户程序启动之前才完成。

基于这一经验,我为我们的自制CPU制定了中断和虚拟地址转换的规范草案。为了简单起见,我们决定省略环保护等硬件特权机制。对于虚拟地址转换,我们决定使用硬件页面遍历的方法,就像x86一样。这看起来很难在硬件上实现,但我们认为如果牺牲速度而省略TLB实现会更便宜。毕竟,Yuichi后来做了一个很好的CPU内核,虽然从一开始就安装了TLB。

Yuichi完成了我们中央处理器ISA的总体设计,他将我们的CPU命名为GAIA,在典型的CPU实验项目中,我们没有实现中断或MMU,但是Yuichi开始在Xv6上实现它们,基于Group 3的CPU重构版本。

我会记下每周的记录,因为从那时起就开始快速的进步了!

Masayoshi开始实现CPU的实际初始化,Shohei将Xv6的x86程序集重写到我们的自制体系结构中。我在我们的模拟器中添加了Wataru在CPU实验的核心部分所做的中断模拟功能,并完成了对虚拟地址转换的支持。这为模拟器提供了足够的功能来运行操作系统。

我为我们的体系结构做了一个原始链接器来组装Xv6及其二进制blob.Shohei正在致力于实现中断处理程序,这是一个困难的部分,中断很难理解,很难弄清楚流程,很难调试,也很难开发。

当我把Xv6移植到MIPS时,我有GDB,所以还可以,但是我们自己的模拟器没有任何调试功能,所以调试一定很困难,Shohei无法忍受调试的困难,所以他在模拟器中增加了反汇编和调试转储功能,之后,OS团队对模拟器的调试功能进行了快速升级,最终模拟器成长为如下图所示。

克服了种种困难,Xv6的移植取得了进步,但是Xv6仍然不能正常工作。

尤其是UCC规范中的char和int都是32位,这不是UCC的错,实际上C规范只要求sizeof(Char)==1和sizeof(Char)<;=sizeof(Int),所以这是合法的。

但是,xv6是为x86编写的,所以它假设sizeof(Int)=4,并在指针的值上加上常量,这导致了很多不一致,因为由此造成的bug很难找到,而且数量也很大,所以最终决定要求UCC将char设置为8位。

在将char 32位问题委托给UCC团队之后,我编写了第一个进入阶段的分页初始化,并尝试通过反复尝试使中断正常工作。

底线是我们努力解决了挑战#4“Xv6可移植性低”。

当我重读松弛的时候,我可以看到这一天取得了很大的进展,在UCC团队非常迅速地完成了将char 8位的更改后,我们努力地进行了大量的调试,最后,我们的第一个用户程序init成功了!

在那之后,我们在向MIPS移植我还没有移植到MIPS的用户进程应用程序方面取得了越来越多的进展,发现并修复了许多难以重现的错误或中断规范中的不足之处,但我们还是以某种方式克服了它。

我们修复了一个有趣的错误是缓存别名问题。GAIA CPU选择虚拟地址而不是物理地址作为缓存索引。这是因为它允许您跳过虚拟地址转换来查找缓存。但是,由于这一点,我们发现缓存之间发生了不一致,因为虚拟地址的多个缓存可能指向相同的单个物理地址。当一个虚拟地址的缓存更新时,指向相同物理地址的其他虚拟地址的缓存没有更新。

这个错误很难在硬件端以低成本修复,所以我们通过在Xv6中引入“页面着色”来修复它。这为每个缓存行引入了“颜色”,并分配页面,以便指向相同物理地址的虚拟地址始终具有相同的颜色。这意味着指向相同物理地址的虚拟地址将始终只有一个缓存。这使得Xv6可以确保Gaia永远不会有多个共享相同物理地址的缓存。

在1号,xv6端口完成。Xv6现在正在模拟器…上运行。好了!

最初,Xv6移植本来是为了好玩,自从Xv6开始在模拟器上工作以来,我们努力添加了更多的乐趣。

首先,Masayoshi在大约4小时内创建了一个迷你诅咒,sl命令在我们的Xv6上运行。

在此期间,御池完成了X组CPU的实现,真实的CPU运行速度比模拟器快得多,使得游戏的玩法和开发变得更容易,这时,一个非常高质量的应用程序--2048就诞生了。

这个2048的质量非常高,Yuichi一直在玩,最初2048使用的是非线缓冲输入,但是xv6原来没有这个功能,为了支持这个功能,除了读写之外,还增加了一个devsw动作,并且增加了新的termios相关功能来控制ICANON和ECHO,所以唯一可以播放2048的完整程度如此之高的Xv6就是Gaia上的Xv6。

顺便说一句,对于受V6启发的Xv6,我想添加gtty和stty系统调用是一种更类似于Unix V6的方法。但是,我之所以采用ioctl,是因为Xv6没有tty的概念,而且ioctl是在下一个版本V7中引入的,V7历史悠久。

现在,作为一个更酷的东西,Xv6-Gaia有一个Keiichi制造的小汇编器,还有一个Shohei做的迷你vi,看看你能用这两个做什么。

这是在FPGA上的交互式编程!这是一个非常令人印象深刻的CPU实验演示,它通常不包括任何交互式程序。

CPU实验的最初任务是“在您的自制CPU上运行给定的光线跟踪程序”。现在您的CPU上已经运行了一个操作系统,您知道应该做什么了,对吧?我们决定在我们自己的CPU上“在操作系统上”运行光线跟踪程序。我们有一些错误,但我们设法在最后演示前一个小时完成了它。

因此,我们做了我们系历史上每个学生可能至少开玩笑过一次的事情:在CPU上运行操作系统,并在其上运行光线跟踪程序。

到目前为止,我写的基本上是我在2015年写的博客文章的重写。现在读它,我可以看到我当时很多技术上的缺乏经验,我们当时所做的绝对是令人兴奋的。

顺便说一句,现在你可以从这里看到我们的Xv6当时在你的浏览器上是什么样子的!在CPU实验之后,我把我们的Gaia模拟器移植到了Emscripten的JavaScript上。让我们试试我们的sl、扫雷舰和2048。

让我还告诉您,在CPU实验时还没有完成的Xv6到MIPS的移植在实验后一个月就完成了,GitHub存储库就在这里。

在我们在2015年发布了一篇关于X组挑战的博客文章后,后来的几代学生继续围绕操作系统接受新的挑战。

2018年,一些学生在自制CPU上运行自己的操作系统,2019年,一群学生在采用RISC-V作为自制CPU ISA的同时运行自己的操作系统。此外,2020年,这群学生最终在也采用RISC-V作为ISA的自制CPU上运行Linux。3个。

我相信未来会有更多的故事,所以每个人都请期待它们。就我个人而言,我期待着有一天有人在他们自己的ISA上运行Linux,或者在上面运行VM。

重新发明轮子通常被认为是要避免的,但从实际操作中可以学到相当多。它让我意识到,我并不能很好地理解它,因为我可以从头开始实现它。另外,我向你推荐它,最重要的是,它是为之而死的乐趣!

这就是我们CPU实验的故事的结尾。如果您对重新发明这个激动人心的轮子感兴趣,请尝试构建一个CPU或将操作系统移植到它上面。

如果你懂日语,你可以在这里阅读我以前的帖子。我在微软工作,并不是所有的同事都懂日语。所以,我正在写这篇英文帖子。↩︎。

Keiichi曾经告诉过,他们能够如此快速地发展UCC的原因之一是他们用OCaml编写了UCC。OCaml允许您如此轻松地操作树结构,而不会出现任何指针错误。另外,当然那是因为它们很棒。顺便说一句,对于那些对预处理部分感兴趣的人,我们使用了Clang的CPP。您知道Clang的CPP可以作为独立的命令使用吗?Keiichi用日语写了一篇关于编译器团队的文章。↩︎