操作系统开发入门,第一部分






4.94/5 (64投票s)
简介和环境设置
引言
如果你正在阅读这篇文章,那么你很可能想了解更多关于如何创建自己的操作系统。首先你需要知道的是,不可能在几篇文章中涵盖所有内容。我将介绍一些基础知识,但你需要知道如何研究和查阅技术文档,才能真正理解你的操作系统是如何工作的。还在读吗?很好。那么,让我们开始吧。
关于操作系统开发(OSDev)你需要了解的首要一点是,你真的要从零开始,并且与裸硬件打交道。你可能会遇到硬件本身的 Bug,以及在意想不到的地方出现的不一致。这里没有标准的库,也没有 .NET Framework。你只能靠自己。
首先,你需要了解你正在创建什么,以及控制权是如何交给你的。奇怪的是,计算机开机时的状态并没有一个明确的定义标准。唯一能真正保证的是,执行将从引导扇区开始,它位于可引导存储介质的开头,加载到 0x7C00 地址,长度为 512 字节,并且文件末尾有一个签名 0xAA55。这通常是用汇编语言编写的。为了节省时间,我们将使用 GRUB(Grand Unified Bootloader)。它为我们提供了一个标准化的计算机状态作为基础。
GRUB 使计算机进入以下状态:
- 保护模式
- A20 Gate 已启用
- EBX 包含指向 Multiboot 信息结构的指针
- EAX 包含值 0x2BADB002
- 分页关闭
- 堆栈位于内存中的某个位置
- 中断已禁用
我将逐一介绍这些。保护模式允许内核开发者访问每个虚拟地址空间高达 4GB 的内存,并引入了“环”的概念。稍后你会找到更多关于这些的信息。A20 Gate 是一个位于键盘控制器附近的老式线路。它最初是为了兼容 8086 而设计的,当禁用时,任何试图访问超过 1MB 内存的尝试都会被环绕到内存的开头。
Multiboot 信息结构确实是一个非常棒的东西。它告诉我们很多关于系统的信息,例如程序信息(如果你想加载符号表进行调试)、内存映射、GRUB 可能加载的模块、引导设备以及其他信息。完整规范可以在这里找到。
Multiboot 魔术值在很大程度上是一个安全机制。通过将值与必需值进行比较,你可以看到内核是否由符合 Multiboot 标准的引导加载程序加载。这很不错,但不是特别有用。分页是一个非常复杂的主题,我在这里不深入探讨。可以肯定的是,它允许你为每个进程提供自己的地址空间,该地址空间已映射到物理内存地址。它还可以防止进程使用特权级别改写内核内存。
特权级别,也称为处理器环,是一种基于硬件的保护机制,通常与保护模式一起使用。它阻止在特定环中执行的代码执行特权指令,如 HLT、CLI 和 STI。当遇到这些指令时,会引发通用保护故障,OS 必须以适当的方式处理。
堆栈是这里的主要问题。它可能位于可用内存中的任何位置。目前没问题,但当分页和多任务处理激活时,大多数内核可能会重新定位它。你可能会发现它很有趣,因为它是向下增长的,所以理论上可以覆盖内核代码。
中断已关闭。这是一件好事,因为你还没有处理它们的必要基础设施。当你构建这个基础设施时,你将能够响应来自键盘、鼠标、网卡、时钟等硬件的请求。当中断发生时,正常的程序流程会被挂起,你提供的 CPU 的偏移量指向的函数将被调用(实际过程稍微复杂一些,但这只是大致原理)。当中断被确认后,正常的程序流程将继续。
环境
现在理论知识已经到位,我们可以开始构建工具链了。这是一个相当简单的过程,但你不会使用微软的编译器;主要是因为它们无法生成 GRUB 可以加载的 ELF 文件。
为了编译和使用工具链,你需要 Cygwin 环境。它本质上是一个模拟层,允许我们在 Windows 下编译和使用符合 POSIX 标准的软件。安装此软件时,请确保安装 GCC、NASM、Make、Bison、GenIsoImage 和 Flex。这些将使我们能够编译源代码。
交叉编译器是一种普通的编译器,在一个系统上运行,但生成在另一个系统上运行的可执行文件。我们的编译器将是 GCC 版本 4.2.4 和 NASM 版本 2.02。为了支持这些编译器,我们还需要 Binutils 版本 2.18。Binutils 基本上是一组信息工具和低级编译器,提供了编译所需的低级基础设施。
为了节省时间,你可以从本文的网页下载所有必需的编译器。但是,如果你想自己编译它们,请参阅这里的说明。我不会详细介绍。
如果你按照 OSDev Wiki 上的说明进行操作,那么你的编译器目录将已添加到 PATH 环境变量中。我们也需要这样,所以将 zip 文件中的所有编译器解压到 *C:\Cygwin\usr\cross\bin*,并将其添加到上述环境变量中。这意味着你不再需要为交叉编译器指定完整路径,从而节省时间。
如果一切顺利,你现在就拥有了一个工作的交叉编译器。它仍然需要一些调整来去除操作系统相关的 `#includes`,但这已经足够了。查看 *C:\Cygwin\usr\cross\bin*。那里有一整套可执行文件,但这里是主要的几个及其功能:
AS | GCC 的汇编代码编译器。我们将使用 NASM,但 GCC 会坚持使用这个。 |
G++ | C++ 编译器。GCC 应该会自动转交。 |
GCC | 我们的主要编译器。它将所有内容编译成汇编代码,然后将其传递给 AS。 |
LD | 链接器。它会去除未使用的引用并生成最终的可执行文件。 |
一个重要的注意事项是,交叉编译器的任何部分都无法在 Cygwin 之外运行——它们会抱怨缺少 *cygwin1.dll*。
现在你已经拥有了将源代码完全链接成独立文件的工具。接下来需要的是能够实际打包运行这个文件。传统上,我们会将文件复制到虚拟软盘,并使用 Virtual PC 或 Bochs 等模拟器。然而,随着光存储的出现,我们现在可以将编译好的内核放入 CD 镜像(ISO 文件)中。然后,我们就可以直接从它启动。
我们需要的第一件事是 GRUB(除非你想挑战一下,自己编写一个可以解析文件系统的 Multiboot 兼容引导加载程序)。要获取它,只需下载这个文件,并将“*stage2_eltorito*”文件复制到将包含 CD 镜像解压内容的文件夹中。
这是一个非常重要的步骤:确保你有一个非常明确的文件夹结构。没有它,你将很难组织你的代码,并将头文件与实际源代码分开。此外,使用 genisoimage 会更加困难,你将要设置的 make 文件也会过于复杂。
要制作 ISO 镜像,你需要我在前一段中提到的那个明确的文件夹结构。你还需要一个 Cygwin 自带的程序,名为 genisoimage。在我们开始之前,你需要一个将被复制到 ISO 镜像中的目录。为了简单起见,我们称之为 *IsoSource*。当我们从它启动时,GRUB 会将其视为 CDROM 驱动器。在 *IsoSource* 内部,你需要两个目录,一个在另一个里面。第一级目录必须名为 *boot*,其内部应有一个名为 *grub* 的目录。*grub* 目录应包含“*stage2_eltorito*”文件,以及一个“*menu.lst*”文件。
为了更容易理解,此图代表了你需要的目录结构

“*menu.lst*”是 GRUB 解析的菜单文件。一个标准的菜单文件将有两行;一行指定内核的标题,另一行包含路径。在我们的例子中,*menu.lst* 文件将如下所示:
title TutorialKernel
kernel /kernel
这个文件本身解释得很清楚,但它的文档位于这里。正如我们所见,第一行简单地指定了标题,它将显示在要加载的操作系统列表中,第二行提供了内核文件的相对路径。你可以很容易地创建一个更复杂的配置文件,它会缩短菜单的超时时间或添加模块(加载到内存中并传递给内核的文件),但这些是你真正需要的。
生成镜像
现在我们已经整理好了文件夹结构,只需生成 ISO 镜像。我们使用的命令类似这样:
genisoimage -R -b boot/grub/stage2_eltorito -no-emul-boot -boot-load-size 4
-boot-info-table -o "ISO Image/Compressed image.iso" "ISO Image/IsoSource"
快速浏览一下这些参数是有益的。我将逐个解释。
-R | 生成 Rock Ridge 信息 |
-b | 下一个文件路径包含引导镜像 |
-no-emul-boot | 不要尝试模拟软盘 |
-boot-load-size | 下一个参数包含一次加载的扇区数 |
-boot-info-table | 向 ISO 镜像添加一个信息表 |
-o | 下一个文件路径是输出文件 |
“ISO Image/IsoSource” | 这是最后一个参数,它告诉 GenIsoImage 在哪里找到目录结构。 |
只要你的目录结构完全正确,它就会生成一个 ISO 镜像,你可以从中启动真实计算机或虚拟机。
现在,这只是将所有东西组合起来的问题。简而言之,要从源代码创建内核,我们需要:
- 使用 NASM 和 i586-elf-g++ 编译所有 ASM 和 CPP 文件。
- 使用我们的专用链接文件和 i586-elf-ld 将生成的 *.o 文件链接成一个文件。
- 将该文件复制到 ISO 镜像的源文件夹。
- 使用上述参数运行 GenIsoImage。
任务分解后,我们可以轻松地使用 Makefile。我将其留给读者作为练习;请记住,你必须按照给定的确切顺序执行这些步骤。不要弄混!
现在,你可能想测试你的内核。为此,我们使用虚拟机。最好在多个虚拟机上进行测试——不同的机器具有不同的功能,并且对你的代码的反应也不同。我的电脑上安装了 Bochs 和 Microsoft Virtual PC;Bochs 用于图形调试功能,Virtual PC 用于实时模拟。
我在这里不介绍如何设置 Virtual PC 镜像;你可以自己很容易地做到。我们的内核目前可以在任何设置下运行,所以这不是很重要。唯一的怪癖是,你可能还不需要虚拟硬盘文件——你还没有办法使用它。
另一方面,Bochs 则完全不同。有数百个选项可以让你按照需要设置配置。这些选项可以手动输入,也可以从文本文件中加载。你可以在这里下载 Bochs;只需安装它。
安装完成后,你可以第一次运行它。你会看到一个命令行窗口和一个标有“Bochs Start Menu”的窗口。在这里,你实际创建配置文件。我将介绍你可能想要使用并觉得有用的选项。
日志文件 | 非常有用。如果你的操作系统有问题,它会为你提供大量信息。将文件名设置为一个你可以轻松访问的位置,可能在你的项目目录中。 |
CPU | 如果你在检查处理器识别函数,你会发现在这里检查结果很有用。 |
内存 | 你可以在这里更改客户操作系统使用的内存量。非常有帮助,同样重要。 |
磁盘和引导 | 最重要的一部分。通过在这里摸索,你可以为模拟添加一个 CD 驱动器,该驱动器从 ISO 镜像加载数据。最后一个主要选项卡“引导选项”也允许你指定引导顺序。 |
其他 | 这允许你启用端口 0xE9 技巧。基本上,你向这个端口写入一个值,然后在 Bochs 中触发一个断点。非常适合调试。 |
然而,图形界面只能带你到这里——它似乎没有启用图形调试器的选项。将你选择的选项保存到 BXRC 文件,并在记事本中打开该文件。应该有一行以“display_library”开头。将该行更改为“display_library: win32, options="gui_debug"”。现在你已经设置好了 Bochs 来使用图形调试器,并且有一个 ISO 镜像可以从中引导。现在你只需要一些代码来编译和运行!
我说过不止一次了:你没有标准库。C++ 仅在你不使用异常或运行时类型信息(RTTI)的情况下才受支持。你必须设置编译器,使其在链接时避免引用这些内容,因此你需要添加多个命令行选项。没有内置的内存分配,所以你需要实现自己的 NEW 和 DELETE 运算符。
编译器选项的一般格式如下:
i586-elf-g++ -W -Werror -Wall -Wpointer-arith -Wcast-align
-Wno-unused-parameter -nostdlib -fno-builtin -fno-rtti -fno-exceptions
-c “Input file” -o “Output file”
以“W”开头的每个参数都是一个警告。如果你在指针算术和类型转换对齐方面犯了一些错误,它会警告你。Werror 会将所有警告视为错误,以防止你忽略错误。
现在我们只需将所有内容组合起来。无论你使用批处理文件还是 Makefile,都由你决定;我个人推荐使用 Makefile,因为它具有可扩展性,可以随着你创建新文件而扩展。这是基本模板:
i586-elf-g++ -W -Werror -Wall -Wpointer-arith -Wcast-align
-Wno-unused-parameter -nostdlib -fno-builtin -fno-rtti -fno-exceptions -c
“Main.cpp” -o “Main.o”
i586-elf-ld –T “Link.ld” –o “kernel” “Main.o”
cp “kernel” “ISO Image/IsoSource”
genisoimage -R -b boot/grub/stage2_eltorito -no-emul-boot -boot-load-size 4
-boot-info-table -o "ISO Image/Compressed image.iso" "ISO Image/IsoSource"
然后,瞧!你的内核已经编译、链接、复制并压缩。从该 ISO 镜像启动 Virtual PC,你的代码就会被执行。现在你唯一需要的就是一些代码来编译!你可以在下一篇文章中找到这段代码的起点,链接如下。
下一篇:C++ 支持代码和控制台