自制操作系统 DeepOs (一):准备
大学学习了操作系统、计算机原理等专业课程后,出于对知识的巩固和对这些领域前辈大佬的拙劣模仿,我想趁暑假学习一下自制操作系统。这一项工程一定会十分庞大,这里记录一下学习历程:)。
参考书籍
-
操作系统真象还原-郑刚
郑大大这本书
十分厚十分充实,目前学习的一些基本难题书中都有叙述,很适合拿来学习参考。并且这本书用的也是 NASM 汇编器,正好贴合之前学习的内容。 -
x86-64汇编-从实模式到保护模式(第二版)-李忠、王晓波和余洁
-
鸟叔的 Linux 教程系列-鸟哥
本次环境搭建在 Linux + Qemu 下,需要了解一些 Linux 的基本使用(不是特别重要)。
-
30 天自制操作系统-[日]川合秀实
我第一本读自制操作系统的书,写的比较浅显易懂。但是作者用的是自己改过的编译器,不太方便学习,原则上参考。
-
操作系统概念-[美] Abraham Silberschatz / Peter B. Galvin / Greg Gagne
在知乎推荐看的第一本介绍操作系统的书,个人感觉讲的要比《现代操作系统》这本书要好一点
看不懂。
复习知识
操作系统定义
预先定义好自己要完成的任务实质是很有必要的,我们完成地是一个操作系统内核类似的东西,首先我们复习操作系统的任务,这里只罗列一些基本的理解:)。
操作系统(英语:Operating System,缩写:OS)是一组主管并控制计算机操作、运用和运行硬件、软件资源和提供公共服务来组织用户交互的相互关联的系统软件程序,同时也是计算机系统的内核与基石。操作系统需要处理如管理与配置内存、决定系统资源供需的优先次序、控制输入与输出设备、操作网络与管理文件系统等基本事务。操作系统也提供一个让用户与系统交互的操作界面。
–摘自 Wiki 百科 操作系统
总结来说便是操作系统负责管理硬件,向应用软件(用户)提供一层抽象的接口,方便后者使用。上图可以看出现代操作系统包含的部分,我们首要任务便是完成出 GUI 以外的内容。
控制硬件
上面的定义提到了控制硬件,那么操作系统如何控制硬件?
I/O 设备大致可以分为两类:块设备和字符设备。块设备存储信息在固定大小的块中(如硬盘和 CD),字符设备则连续的从设备中读取字符流(如
打印机和鼠标)。
CPU 从这些 I/O 读取内容的方式主要有三个:控制器、内存映射和 DMA。
- 设备控制器。控制器经常以主板上芯片形式出现,与设备之间的接口层次较低。控制器设置某几个寄存器,从 I/O 设备读取信息,并做一些必要的工作(校验错误)然后通过总线返回。除此之外,某些设备还有专门的缓冲区,操作系统会为这些控制寄存器分配一个端口号,并且保证普通用户不能访问,可以通过特殊指令进行读写。
- 内存映射。操作系统除了分配端口号,还可以将控制寄存器的缓冲区映射到内存空间中(即虚拟地址空间)。每个控制寄存器都有一个地址,并且内存不会映射到这部分上面,这样可以方便进行管理(C 语言对这些地址进行访问)。但是也对高速缓存造成了挑战。
- DMA。I/O 设备具有 DMA 控制器时才可以使用 DMA(针对设备或者针对系统),这时控制器可以独立于 CPU 而访问系统总线。
中断和陷阱
中断是指计算机运行过程中,出现某些意外情况需主机干预时,机器能自动停止正在运行的程序并转入处理新情况的程序,处理完毕后又返回原被暂停的程序继续运行。–百度百科
中断分为硬件中断和软中断。硬件中断会在 I/O 设备完成任务后产生(被动),然后由处理器进行处理,还可以划分为外部中断(键盘中断)和内部中断(异常信号如零除)。中断发生时,若没有屏蔽或者有优先级较高的中断正在处理中,处理器会跳转到对应的中断向量进行处理。
软中断是由 int 指令触发,如 BIOS 中断、DOS 中断和 int 80 中断(处理器主动引起),还被称为陷阱。执行软中断指令后会设置一系列寄存器(如 IP 和 SP),然后陷入操作系统内核并执行相应的中断处理函数,可以用来设置系统调用。
BIOS 中断和 DOS 中断都建立在实模式下的中断向量表中,该表每个向量字节是 4 字节(段基址和段偏移),最多有 256 个中断向量。计算机启动后,BIOS 在地址 0x0 处建立中断向量例程,占用中断向量号 0x00 – 0x1F。DOS 也运行在实模式下,占用的中断向量号为 0x20 – 0x27,并且可以调用 BIOS 中断。Linux 则运行在保护模式中,系统调用建立在中断描述符(IDT),仍然是通过 int 指令来调用。
段页式设计
早期计算机使用内存时是直接按照物理地址进行使用,即所有的程序访问同一个地址时是使用的同一块空间,这种设计既不利于大地址空间程序的设计,也不利于多道程序设计的安全性(代码不可更改),对此考量,工程师提出了地址空间设计。
每个进程拥有自己的地址空间后,同一个地址如 0x88 在不同的程序中对应不同的物理地址。这种方法使用基址寄存器和界限寄存器来进行寻址,并对不合法的地址产生错误并终止访问,但这样还是将进程的空间放在物理内存的不同位置。
为了应对膨胀的大地址空间,虚拟内存系统应运而生。早期有人提出了分段式设计,将程序划分成不同的段(如程序段、代码段或未初始化段),在装载不同的程序时可以直接替换。之后为了解决碎片和手动划分段的问题又提出了分页式设计,程序可以被分割成页来对物理内存映射,并在一个程序需要更多的页来运行程序时进行申请物理页,使用地址时将其送入 MMU 映射为物理地址。这种方式非常适合物理内存比较紧张的场景,并且可以和计算机的 cache 相配合加速内存访问(数据 cache 和指令 cache)。
至于汇编语言中的 section 和 segement,则效果相似,都是在内存中划分属性相同的逻辑区域。对于应用程序来说,section 是由上述两个关键词定义的一个个逻辑区域,而 segement 是由多个相同属性的 section 链接而成。
库函数
为了方便用户使用操作系统,复杂的系统调用还需要进一步包装,因此出现了库函数。在 C 的库文件中,经常使用各种头文件如 stdlib.h 和 string.h,通过在源文件中包含,编译器链接时会将库文件对应的目标文件链接进来,从而可以调用预定义的函数(封装了系统调用)。Linux 通过存档文件实现了上述功能,而 Windows 中还可以使用 dll (动态链接库)来进行外部函数调用,这样就不必更改函数后重新进行编译。
操作系统引导
计算机启动后首先会进行 BIOS 自检,检查各种硬件并进行设置,检测后便会将执行权交给 MBR (主引导记录)。MBR 放在磁盘中的第一个扇区(0 盘 0 磁道 1 扇区),大小为 512 字节(一般也是一个扇区的大小),存放
- 446 字节的引导程序和参数
- 64 字节的分区表
- 2 字节的结束标记 0x55 和 0xAA
分区表分为主分区和逻辑拓展分区,用来为操作系统实现文件系统。MBR 接手后,按照引导程序设置参数,然后交给次引导程序(即操作系统的加载器),最终控制权来到操作系统内核。
在设置 MBR 的分区表项时,将一个表项的第一个字节设置为 0x80 即表明该分区存在引导程序,否则值为 0。如果某一个分区的活动标记设置了,MBR 会在该分区的第一个扇区(引导扇区)寻找引导程序,该加载器称为操作系统引导记录 OBR,一般约定 OBR 扇区的前三个字节存放跳转指令,跳转后处理器转入引导程序,内核程序运行。
除此之外,还有 DBR 和 EBR。DBR 是 DOS 系统的引导,内容为:
- 跳转指令
- 厂商信息、DOS 版本
- BIOS 参数块 BPB
- 引导程序
- 结束标记 0x55 和 0xAA
上面分区表中只有 4 个表项,想要多于 4 个分区就要使用拓展分区。为了个兼容 MBR,在拓展分区中存储分区表的的称为 EBR,拓展分区可以有多个 EBR。
实验环境准备
- Ubuntu 22.04
- Gcc + Nasm + Vscode
- qemu-system- 虚拟机
上述软件均是开源,并且可以直接下载到虚拟机中,这里不再记录过程。安装好软件之后,使用 dd 命令或者 qemu 提供的工具 qemu-img 来创建我们要用到的虚拟启动磁盘(已经进入工作目录)。
dd if=/dev/zero of=./deepos.img size=1M count=1
#or
qemu-img create -f raw ./deepos.img 1M
创建之后我们得到了一个虚拟的磁盘镜像,此时可以使用 qemu 虚拟机挂载该磁盘
qemu-system-i386 ./deepos.img
可以看到成功启动虚拟机,但是提示 No Bootable Device,这是因为此时磁盘还是空的,没有我们需要的 MBR 以及 OBR,后面我们会创建用于引导的 MBR。