写在前面

此时此刻,2023 年 8 月 29 日,我一个人坐在中山大学南实验楼 D502 Matrix 实验室里

在我高中还不知天高地厚的时候,我曾荒诞地认为微软开发的 Windows 系统不过如此;而自我步入大学生活以来,真正地接触过计算机技术后,才愈发感受到面前这七八十年计算机大厦的坚不可摧和绝对统治。我尝试追逐前人的步伐,用 C++ 实现一个操作系统便是其中的一步

寒假时期我便已读完了 《深入理解计算机系统》 ,又称 CSAPP ,这是一本神书!我在 CSAPP 里学到了汇编、CPU 原理等一大堆很重要的前置概念。再然后,我翻阅到我的一位学长 —— GZTime 他用 Rust 实现 GGOS 的纪念博客,我逐渐有了用编写属于自己的操作系统的企划,并且大致敲定了是用 C++ 来完成,命名为 TKSKOS 。2023 年 4 月,因为一些意外我的生活天翻地覆;某一天的高等数学课,我无聊时翻阅知乎,意外地翻到一篇中大学姐的亲手实现操作系统的记录 —— 这对我影响很深,那段时间我满脑子都是一个全新的企划。紧接着我入手了一本 《操作系统概念》 ,开始阅读里面的知识,从进程调度一直到虚拟内存我都有了个概念,却苦于无法用编程语言亲手实现,那段时间我虽然知道操作系统的原理,却没有一点实现的办法……

转变发生在我去贵州册亨支教的那几天,那几天支教闲得无聊,我发现在麻省理工的官网公开课上有关于用 C++ 实现一个操作系统的教程,我欣喜若狂,跟之前学习新的知识一样先去把教程看了个遍 —— 我终于知道怎么用编程来实现我学到的操作系统概念了! 至此我摆脱了纸上谈兵的时期,正式开始了 TKSKOS 的编写

Before TKSKOS

在开始 TKSKOS 编写时,需要先学习一些预备知识

汇编语言

汇编语言这部分我是在《深入理解计算机系统》这本书里面学到的,这本书的第二章就是汇编语言的介绍,当时是寒假,单单看这一章用的时间都差不多有一周左右。汇编语言是写操作系统之前一定要学会的东西,其重要性高的一批

关于汇编语言的部分可以查看此处

Vscode 远程 Linux 开发

Linux 平台上有很多编写 OS 时需要用上的好工具,所以非常建议在 Linux 环境下编写 OS。为了有一个良好的编写环境和体验,也非常建议在 Vscode 上远程连接 Linux 云服务器,这样子就可以在 Windows 或 MacOS 环境下远程操作 Linux 系统。

我建议是上云服务器平台租一个 Linux 云服务器,然后在本地 Vscode 连接。最开始我想着在本地开一个 Linux 虚拟机,开完之后发现网络配置这一块如果要我自己设置会非常地麻烦,当时准备一个 Linux 开发环境已经折腾了我快一天的时间,取舍之下,还是用回了之前自己租的 ubuntu 服务器

连接成功后的界面跟在本地上用 Vscode 打开一个项目差不多,使用起来是非常便捷的

关于此部分的操作可以查看此处

img1

G++ 编译

G++ 最早我是在寒假的时候接触的,那会在看《深入理解计算机系统》,第一次用 G++ 亲手编译一个 .cpp 程序让我感觉异常兴奋。编写 TKSKOS 时我全程用的都是 Linux 平台下的 G++ 编译器,这是一个功能强大且使用便捷的 C++ 编译器。当然,它除了编译还能实现其他的功能,比如功能多样的编译选项、能细化的编译过程 —— 这能帮助我们实现汇编语言与 C++ 语言的联合编译

关于 G++ 的用法可以查看此处

G++ 内联汇编

G++ 编译器作为一款强大的编译器,不仅支持多种编程语言,还提供了内联汇编的特性,允许在 C++ 代码中嵌入汇编代码。内联汇编可以在一些特定场景下优化关键代码,提高程序性能,最关键的是,能够实现一些 C++ 代码实现不了的底层逻辑

关于 G++ 内联汇编的用法可以查看此处

Makefile

Linux 环境下的 C++ 程序编译需要我们手动去输入命令才能编译,当需要的文件变多时,编译会变得非常麻烦,所以我们需要使用 Makefile 来脚本化我们的编译操作

Makefile 文件描述了 Linux 系统下 C/C++ 工程的编译规则,它用来自动化编译 C/C++ 项目。一旦写编写好 Makefile 文件,只需要一个 make 命令,整个工程就开始自动编译,不再需要手动执行 GCC 命令。一个中大型 C/C++ 工程的源文件有成百上千个,它们按照功能、模块、类型分别放在不同的目录中,Makefile 文件定义了一系列规则,指明了源文件的编译顺序、依赖关系、是否需要重新编译等

关于 Makefile 的用法可以查看此处

TKSKOS’s Realized

至此,我们开始用 C++ 实现 TKSKOS

引导程序与内核程序

在开机时,系统会进入实模式,会自动将引导程序加载至 0x7c00 处,再由引导程序加载整个操作系统。引导程序要用汇编语言编写,当然涉及汇编的地方不会很多,但很可惜引导程序是全部都需要汇编来操作的

BIOS 主要的一个功能就是存储了磁盘的启动顺序,BIOS 会按照启动顺序去查找第一个磁盘头的 MBR 信息,并加载和执行 MBR 中的 Bootloader 程序,若第一个磁盘不存在 MBR ,则会继续查找第二个磁盘(PS:启动顺序可以在 BIOS 的界面中进行设置),一旦BootLoader程序被检测并加载内存中,BIOS就将控制权交接给了BootLoader程序。我们要在 Bootloader 的开头做标志好让我们的 BIOS 程序能找到我们的引导程序,这里涉及到一些标准,上网查找一些资料即可

内核程序也就是一个 .cpp 程序,kernel.cpp,可以看作为操作系统运行时的主函数 main。这部分的内容我前前后后花了大概一天的时间,因为很多东西都是现场学习的,比如用汇编语言写一个引导程序……

封装 IO

封装 IO 的目的很简单,因为现在是在写操作系统,**C++ 里的官方标准库例如 或 <stdio.h> 都是不能使用的,所以为了能够输出日志我们就只能自己手动实现一些 IO 函数,比如 printf()

我一开始是做了图形驱动的,用 BIOS 的显卡驱动来作图并且也实现了相应的 IO 函数,后面发现这么做没有封装好,并且局限住了后续的代码编写,所以最终还是取消了图形界面的编写,转为专注命令行模式的操作

下图为早期我编写的图形化界面

最初的图形界面

要往 BIOS 上打字也很简单,从 0xb8000 开始往后的 2500 个地址位置就是 BIOS 的命令行输出源位置,只要我们修改这几个地址位置的数据,我们的屏幕就会跟着刷新。我先实现了一个简单的 void put(uint8_t x, uint8_t y, char ch) 函数,可以往指定的 x,y 坐标打印指定的字符,然后再基于这个函数做了一些封装,比如打印字符串、自动换行、自动切屏…我把这些功能都集成在 Screan 类里,这个类会在 kernel 主函数的开场实例化,接着我再封装了一个 io.h 文件,里面有一些函数,现在我可以很方便地往屏幕上输出日志了

编译并运行

当我们有了一些代码源文件后,我们该如何编译,编译成什么。又如何运行,运行成什么,在哪运行。这都是这个阶段需要思考的问题

.s 和 .cpp 文件用 G++ 编译成 .o 文件,所有的 .o 文件用 ld(链接程序) 链接为 kernel.bin 文件,kernel.bin 文件是一个可执行文件,但我们不能直接运行(因为 ta 的启动头跟 multiboot 有关,不能看做一般的可执行文件)。提前准备好 GRUB 的配置文件,并以合适的文件结构做成 grub2 引导包,最后用 GRUB 将这个引导包做成 .iso 光盘镜像文件,那么 TKSKOS 的代码就集成在了 TKSKOS.iso 里,这个 .iso 文件就是编写好的系统

.iso 文件可以运行在虚拟机也可以运行在裸机上,在虚拟机上运行只要按照正常的配置操作就可以配置好了,在裸机上运行的话有点看运气,因为裸机的开机配置可能不太一样,在这里我推荐一个很厉害的软件,是我的学长推荐给我的 —— Ventoy,一个很方便的操作系统引导程序,在本地上安装这个程序,然后接入一个空的 USB 存储介质,Ventoy 就可以把这个空的 USB 介质变为一个操作系统引导盘,直接复制 .iso 文件到介质里,再把介质安装到裸机上,就可以在裸机上运行对应的系统了

关于虚拟机的操作可以查看此处

关于Ventoy的操作可以查看此处

下图为 TKSKOS 在虚拟机上的运行界面

虚拟机上的运行界面

分段寻址表

因为 TKSKOS 是 16-bit 架构的操作系统,所以在寻址方式上选择了分段寻址,而不是分页寻址。首先我查阅了一下分段寻址中**段描述符(SegmentDescript)中的构成,多个段描述符就组合成了全局段描述表(GlobalDescriptTable/GDT)**,操作系统可以通过 GDT 用 16 位的地址来实际访问 20 位的内存空间,同时还可以获取每个段的使用权限情况,做到内存使用的安全保护

我们在 kernel.cpp 中加上 GDT 的实例化的部分,并且在 GDT 的构造函数部门就用 G++ 的内联汇编将 GDT 的地址传给段寄存器,至此 CPU 就可以通过段寄存器获取到 GDT 的地址,并通过 GDT 来实现分段寻址。这部分的代码是最折磨人的,因为一旦编写有误虚拟机会直接崩溃推出,因为是在编写操作系统,也不会有合理的日志打印给你去排错,我只好不断地试了又试再试了又试…最后终于是搞定了,是段描述符的权限位设置错误QAQ

端口读写

端口读写就非常简单了,分别在 8bit、16bit、32bit 定义一个读端口和一个写端口,然后再用 G++ 的内联汇编用汇编实现对应的操作就 ok 了,其实就是给 C++ 程序提供一个调用端口读写的接口而已

异常中断表

异常中断表(InterruptDescriptTable/IDT)绝对是我写的最想死的一个部分,一方面我既要考虑汇编与 C++ 程序的联合使用,另一方面我要考虑中断表在计算机底层上的硬件形式 —— 期间又需要去查阅很多的参考资料。这一部分的编写经历正如其名,总是给我的虚拟机带来各种各样的异常…经常虚拟机爆炸然后崩溃。当然,我在这一个部分学习到了非常多的技术,而且对 C++ 写类也有了一个全新的认识

异常中断是 CPU 的一个内在机制,通过这个机制我们可以来给对应的中断留下可以扩展的 C++ 函数接口。当我们点击键盘和移动鼠标时,这种中断也在无时不刻地发生着…同时我们可以提供时钟中断的对外接口,这样子也就给 TKSKOS 获得了读取时间的功能

键鼠驱动

当实现了异常中断表后,我们便在每一个系统中断上设置了一个可以扩展的 C++ 函数接口,键鼠的驱动其实跟这个接口函数关系非常大。键鼠跟操作系统的交互是通过端口来实现数据交换的,键鼠在使用时会给负责中断的端口发送消息 —— 这个消息会被中断表捕获到,触发对应的中断函数;再给两个特定的端口发送相关的数据,比如哪个键被按下了、鼠标移动了多少距离…我只需要把对应的端口里的消息拿到,剩下的其实就是对端口的数据做分析,并给予相应的反馈就行了

这一部分的编写没有占用我太多的时间,真正的工作量也就大概一天左右,同时我为了能够统一接口,还多写一个 Device 类和 DeviceManager 类,这能帮我统一管理外设接口

总线

总线的实现其实是非常简单的,因为我们先前已经实现了端口的读写,我们先定义一个总线的类,然后实现一些接口函数。我们只需要向指定的端口发送一个指令码,对应的总线读端口就可以给我们返回相应的数据

问题就是如何去使用总线返回的数据,这些数据是有规律的,对应的规律在网络上都能找到对应的协议文档,然后对照着去翻译就行了。总线是计算机与外部硬件连接的一个抽象接口,能够给我们提供外部硬件的厂商、设备号等信息。将这些信息加以转换,我们便可以通过总线得知我们电脑连接的外部设备的信息

下图为 TKSKOS 上总线的输出情况

总线的输出情况

多进程调度

多进程调度是操作系统能够同时运行多个程序的重要技术,在 TKSKOS 中,我先定义了一个 CPU’s state 的 struct,里面用来记录一个 CPU 的状态 —— 也就是寄存器的状态。同时我还编写了 Task 类,用来标记每个进程,我给每个进程都分配了 4kb 大小的栈,并且每个进程都有自己的 CPU’s state,这可以在我们切换进程的时候记录或存储当前的 CPU 状态

最后,我还编写了一个 TaskManager 类,用来管理 256 个 Task 的调度,因为 TKSKOS 比较简陋,所以我选择了等时调度策略,为此我还给 TaskManager 传入了时钟中断的扩展接口。同时进程管理还可以实现进程的创建、切换和销毁

动态内存管理

TKSKOS 的(线程)栈空间是有限的,平时写代码时大部分变量使用的都是动态分配来的堆内存,这些动态申请来的堆内存是需要开发者通过代码去自行管理的,所以 TKSKOS 也需要向用户层面提供能够用动态管理内存的接口,即 malloc、new、free、delete 四个函数的实现

我先定义了一个内存块的类(MenmoryChunk),我用这个类来标注每一个内存块的大小、是否使用、以及这个内存块与前后内存块的联系。一顿操作下来,你会发现内存的分配和销毁其实就是一个链表的操作(。我在想以后优化的时候要不要先实现一个链表类再去把内存块从这个链表类里继承而来(Linux 就是这么做的),可惜我现在没有时间,以后有空看看

最后我再编写了一个 MenmoryManager 类,我用这个类来管理前面出现的所有内存块,并通过这个类来提供用户接口,顺带着实现了 malloc、new、free、delete 四个函数

你好世界

至此 TKSKOS 终于编写完毕!

[INF0] Scraem Loaded.
[INFO] Global Descriptor Table Loaded.
[INFO] Heap Started In 0x0OA00000.
[INFO] TaskManager Loaded.
[INFO] Interrupt Descriptor Table Loaded.
[INFO] PCI BUS 00, DEVICE 00, FUNCTION 00 = VENDOR 8086, DEVICE 1237.
[INFO] PCI BUS 00, DEVICE 01, FUNCTION 00 = VENDOR 8086, DEVICE 7000.
[INFO] PCI BUS 00, DEVICE 01, FUNCTION 01 = VENDOR 8086, DEVICE 7111.
[INFO] PCI BUS 00, DEVICE 02, FUNCTION 00 = VENDOR 80EE, DEVICE BEEF.
[INFO] PCI BUS 00, DEVICE 03, FUNCTION 00 = VENDOR 1022, DEVICE 2000.
[INFO] PCI BUS 00, DEVICE 04, FUNCTION 00 = VENDOR 80EE, DEVICE CAFE.
[INFO] PCI BUS 00, DEVICE 05, FUNCTION 00 = VENDOR 8086, DEVICE 2415.
[INFO] PCI BUS 00, DEVICE 06, FUNCTION 00 = VENDOR 106B, DEVICE 003F.
[INFO] PCI BUS 00, DEVICE 07, FUNCTION 00 = VENDOR 8086, DEVICE 7113.
[INFO] PCI BUS 00, DEVICE 0B, FUNCTION 00 = VENDOR 8086, DEVICE 265C.
[INFO] Keyboard Driver Loaded.
[INFO] TKSKOS’s Kernel Loaded.
[INFO] Welcome To TKSKOS!
[INFO] Maker : Tokisakix.

TKSKOS PowerShell.
[WARNING] TKSKOS PowerShell Is Developing…

虚拟机输出情况

写在后面

因为暑假时间有限,最终的 TKSKOS 并不算一个很完善的操作系统,并且因为架构原因,也不能算是一个现代操作系统,而偏向古典

TKSKOS 最终实现如下:

  • x86 指令集
  • 16-bit 架构
  • 分段寻址
  • 中断管理器
  • 键鼠驱动
  • 命令行界面
  • 多进程管理
  • 总线
  • 动态内存管理

当然,他日若有时间,我一定会改善 TKSKOS ,它现在包括此后都将会是我的一个重要的经历
[INFO] The Rebel Path…
[INFO] Download TKSKOS Here.