操作系统开发小书

Erik Helin, Adam Renberg

2015-01-19 | Commit: fe83e27dab3c39930354d2dea83f6d4ee2928212 PDF version

目录

1 介绍

本文是一本关于编写自己的 x86 操作系统的实用指南。它旨在提供足够的技术细节帮助,同时不过多地通过示例和代码摘录进行展示。 我们尝试收集了网络和其他渠道中大量(通常也很棒的)材料和教程中的部分内容,并将我们自己对遇到的问题和斗争的见解添加进去。 本书不是关于操作系统的理论,也不是关于任何特定操作系统(OS)如何工作的。对于操作系统理论,我们推荐 Andrew Tanenbaum 的 Modern Operating Systems [1]一书。有关当前操作系统的列表和详细信息可在 Internet 上找到。 开头的章节非常详细和明确,可以让你快速进入编码状态。后面的章节更多地概述了需要什么,因为越来越多的实现和设计取决于读者,他们现在应该更熟悉 Kernel 开发的世界。在某些章节的末尾,有一些延伸阅读的链接,这些链接可能很有趣,并且可以更深入地了解所涵盖的主题。 在 第 2 章第 3 章 中,我们设置开发环境并在虚拟机中启动操作系统 Kernel,最终开始用 C 语言编写代码。 在 第 4 章 中,我们继续编写屏幕和串口的输出,然后在 第 5 章 中深入研究分段,并在 第 6 章 中深入研究中断和输入。 在此之后,我们有了一个功能相当完备但非常简陋的操作系统 Kernel。 在 第 7 章 中,我们开始迈向用户模式应用程序的道路,通过分页实现虚拟内存(第 8 章第 9 章), 内存分配(第 10 章),最后在 第 11 章 中运行用户应用程序。 在最后三章中,我们讨论了文件系统(第 12 章)、系统调用(第 13 章) 和多任务(第 14 章) 等更高级的主题。

1.1 关于本书

操作系统 Kernel 和本书是斯德哥尔摩皇家理工学院 [2] 高级个人课程的一部分。 作者之前参加过操作系统理论课程,但只有少量的操作系统 Kernel 开发实践经验。 为了更深入地了解和理解以前的操作系统课程中的理论如何在实践中发挥作用,作者决定创建一个新课程,重点是开发小型操作系统。 该课程的另一个目标是编写一份关于如何从头开始开发小型操作系统的详尽教程,而这本小书就是成果。 x86 架构一直是,并且在很长一段时间内都是最常见的硬件架构之一。 使用 x86 架构作为操作系统的目标并不是一个困难的选择,因为它拥有庞大的社区、大量的参考资料和成熟的模拟器。 尽管(或可能由于)该架构的年代久远,但我们不得不处理的硬件细节的文档和信息并不总是容易找到或理解。 操作系统是在大约六周的全职工作中开发的。 该实现分许多小步骤完成,并且在每个步骤之后手动测试操作系统。 通过以这种增量和迭代的方式进行开发,通常更容易找到引入的任何错误,因为自代码上次已知良好状态以来,只有一小部分代码发生了更改。 我们鼓励读者以类似的方式工作。 在六周的开发过程中,几乎每一行代码都是由作者一起编写的(这种工作方式也称为 结对编程 )。 我们相信,由于这种开发方式,我们设法避免了很多错误,但这很难用科学的方法来证明。

1.2 读者

本书的读者应该熟悉 UNIX/Linux、系统编程、C 语言和一般的计算机系统(例如十六进制 [3])。 本书可以成为开始学习这些东西的一种方式,但它会更加困难,而且开发操作系统本身就具有挑战性。 如果遇到困难,搜索引擎和其他教程通常会很有帮助。

1.3 Credits, Thanks and Acknowledgements

我们要感谢 OSDev 社区 [4] 的精彩 Wiki 和乐于助人的成员,以及 James Malloy 的杰出 Kernel 开发教程 [5]。 我们还要感谢我们的主管 Torbjörn Granlund 提出的深刻问题和有趣的讨论。 本书的大部分 CSS 格式都基于 Scott Chacon 为 Pro Git 一书所做的工作, http://progit.org/.

1.4 Contributors

我们非常感谢大家发送给我们的补丁。 以下用户都为本书做出了贡献:

1.5 Changes and Corrections

本书托管在 Github 上 - 如果你有任何建议、意见或更正,只需 fork 本书,编写你的更改,然后向我们发送 pull request。 我们很乐意采纳任何使本书变得更好的内容。

1.6 Issues and where to get help

如果在阅读本书时遇到问题,请查看 Github 上的 issues 以寻求帮助: https://github.com/littleosbook/littleosbook/issues.

1.7 License

所有内容均根据 Creative Commons Attribution Non Commercial Share Alike 3.0 许可, http://creativecommons.org/licenses/by-nc-sa/3.0/us/. 代码示例位于公共领域 - 随意使用它们。 提及本书总是会受到热烈欢迎。

2 第一步

开发操作系统(OS)并非易事,在项目的过程中,对于不同的问题,"我该如何着手解决这个问题?" 这个问题可能会出现很多次。 本章将帮助你设置开发环境并启动一个非常小(且原始)的操作系统。

2.1 工具

2.1.1 快速设置

我们(作者)使用 Ubuntu [6] 作为操作系统进行操作系统开发,在物理和虚拟环境(使用虚拟机 VirtualBox [7])中运行它。 快速启动并运行所有内容的方法是使用与我们相同的设置,因为我们知道这些工具可以与本书中提供的示例一起使用。 一旦安装了 Ubuntu(物理或虚拟),应使用 apt-get 安装以下软件包:

  sudo apt-get install build-essential nasm genisoimage bochs bochs-sdl

2.1.2 编程语言

操作系统将使用 C 编程语言 [8][9],并使用 GCC [10] 进行开发。 我们使用 C 是因为开发操作系统需要对生成的代码进行非常精确的控制以及直接访问内存。 也可以使用提供相同功能的其他语言,但本书仅介绍 C。 该代码将使用一种特定于 GCC 的类型属性:

  __attribute__((packed))

此属性使我们能够确保编译器使用 struct 的内存布局,就像我们在代码中定义的那样。 下一章将对此进行更详细的说明。 由于此属性,示例代码可能很难使用 GCC 以外的 C 编译器进行编译。 对于编写汇编代码,我们选择了 NASM [11] 作为汇编器,因为我们更喜欢 NASM 的语法而不是 GNU Assembler。 在本书中,Bash [12] 将用作脚本语言。

2.1.3 Host Operating System

所有代码示例都假定代码在类似于 UNIX 的操作系统上编译。 所有代码示例都已使用 Ubuntu [6] 版本 11.04 和 11.10 成功编译。

2.1.4 Build System

在构建 Makefile 示例时使用了 Make [13]。

2.1.5 Virtual Machine

在开发操作系统时,能够在 虚拟机 中运行代码非常方便,而不是在物理计算机上运行代码,因为在虚拟机中启动操作系统比将操作系统安装到物理介质上然后在物理机器上运行它要快得多。 Bochs [14] 是 x86 (IA-32) 平台的模拟器,由于其调试功能,非常适合操作系统开发。 其他流行的选择是 QEMU [15] 和 VirtualBox [7]。 本书使用 Bochs。 通过使用虚拟机,我们无法确保我们的操作系统可以在真实的物理硬件上运行。 虚拟机模拟的环境旨在与物理环境非常相似,并且只需将可执行文件复制到 CD 并找到合适的机器,即可在一个虚拟机上测试操作系统。

2.2 启动

启动操作系统包括沿着一系列小型程序传输控制权,每个程序都比前一个程序更"强大",其中操作系统是最后一个"程序"。 有关启动过程的示例,请参见下图: 启动过程的示例。 每个框都是一个程序。 启动过程的示例。 每个框都是一个程序。

2.2.1 BIOS

当 PC 启动时,计算机将启动一个符合 Basic Input Output System (BIOS) [16] 标准的小程序。 此程序通常存储在 PC 主板上的只读内存芯片上。 BIOS 程序的最初作用是导出一些库函数,用于打印到屏幕、读取键盘输入等。 现代操作系统不使用 BIOS 的函数,它们使用直接与硬件交互的驱动程序,绕过 BIOS。 如今,BIOS 主要运行一些早期诊断(加电自检),然后将控制权转移到引导加载程序。

2.2.2 The Bootloader

BIOS 程序会将 PC 的控制权转移到一个名为 bootloader 的程序。 Bootloader 的任务是将控制权转移给我们,操作系统开发人员,以及我们的代码。 但是,由于硬件的一些限制1,并且由于向后兼容性,bootloader 通常分为两部分:bootloader 的第一部分会将控制权转移到第二部分,第二部分最终将 PC 的控制权交给操作系统。 编写 bootloader 涉及编写大量与 BIOS 交互的底层代码。 因此,将使用现有的 bootloader:GNU GRand Unified Bootloader (GRUB) [17]。 使用 GRUB,操作系统可以构建为普通的 ELF [18] 可执行文件,该文件将由 GRUB 加载到正确的内存位置。 Kernel 的编译需要代码以特定的方式布置在内存中(如何编译 Kernel 将在本章稍后讨论)。

2.2.3 The Operating System

GRUB 将通过跳转到内存中的某个位置来将控制权转移到操作系统。 在跳转之前,GRUB 将查找一个幻数,以确保它实际上是跳转到操作系统而不是某些随机代码。 此幻数是 GRUB 遵循的 multiboot specification [19] 的一部分。 一旦 GRUB 进行了跳转,操作系统就完全控制了计算机。

2.3 Hello Cafebabe

本节将介绍如何实现可与 GRUB 一起使用的最小操作系统。 操作系统要做的唯一事情是将 0xCAFEBABE 写入 eax 寄存器(大多数人可能甚至不会将其称为操作系统)。

2.3.1 编译操作系统

操作系统的这一部分必须用汇编代码编写,因为 C 需要一个栈,而栈不可用(“进入 C 的世界” 一章介绍了如何设置一个栈)。 将以下代码保存在名为 loader.s 的文件中:

  global loader          ; the entry symbol for ELF
  MAGIC_NUMBER equ 0x1BADB002   ; define the magic number constant
  FLAGS    equ 0x0      ; multiboot flags
  CHECKSUM   equ -MAGIC_NUMBER ; calculate the checksum
                  ; (magic number + checksum + flags should equal 0)
  section .text:         ; start of the text (code) section
  align 4             ; the code must be 4 byte aligned
    dd MAGIC_NUMBER       ; write the magic number to the machine code,
    dd FLAGS          ; the flags,
    dd CHECKSUM         ; and the checksum
  loader:             ; the loader label (defined as entry point in linker script)
    mov eax, 0xCAFEBABE     ; place the number 0xCAFEBABE in the register eax
  .loop:
    jmp .loop          ; loop forever

该操作系统将要做的唯一事情是将非常特定的数字 0xCAFEBABE 写入 eax 寄存器。 如果操作系统 没有 将数字 0xCAFEBABE 放入 eax 寄存器中,则该数字 非常 不可能在 eax 寄存器中。 可以使用以下命令将文件 loader.s 编译为 32 位 ELF [18] 目标文件:

  nasm -f elf32 loader.s

2.3.2 链接 Kernel

现在必须链接代码以生成可执行文件,与链接大多数程序相比,这需要进行一些额外的考虑。 我们希望 GRUB 将 Kernel 加载到大于或等于 0x00100000(1 兆字节 (MB))的内存地址,因为小于 1 MB 的地址由 GRUB 本身、BIOS 和内存映射 I/O 使用。 因此,需要以下链接器脚本(为 GNU LD [20] 编写):

ENTRY(loader)        /* the name of the entry label */
SECTIONS {
  . = 0x00100000;     /* the code should be loaded at 1 MB */
  .text ALIGN (0x1000) :  /* align at 4 KB */
  {
    *(.text)       /* all text sections from all files */
  }
  .rodata ALIGN (0x1000) : /* align at 4 KB */
  {
    *(.rodata*)     /* all read-only data sections from all files */
  }
  .data ALIGN (0x1000) :  /* align at 4 KB */
  {
    *(.data)       /* all data sections from all files */
  }
  .bss ALIGN (0x1000) :  /* align at 4 KB */
  {
    *(COMMON)      /* all COMMON sections from all files */
    *(.bss)       /* all bss sections from all files */
  }
}

将链接器脚本保存到名为 link.ld 的文件中。 现在可以使用以下命令链接可执行文件:

  ld -T link.ld -melf_i386 loader.o -o kernel.elf

最终的可执行文件将被称为 kernel.elf

2.3.3 获取 GRUB

我们将使用的 GRUB 版本是 GRUB Legacy,因为操作系统 ISO 镜像随后可以在同时使用 GRUB Legacy 和 GRUB 2 的系统上生成。 更具体地说,将使用 GRUB Legacy stage2_eltorito 引导加载程序。 可以通过从 ftp://alpha.gnu.org/gnu/grub/grub-0.97.tar.gz 下载源代码来从 GRUB 0.97 构建此文件。 但是,configure 脚本在 Ubuntu [21] 上运行效果不佳,因此可以从 http://littleosbook.github.com/files/stage2_eltorito 下载二进制文件。 将文件 stage2_eltorito 复制到已经包含 loader.slink.ld 的文件夹。

2.3.4 构建 ISO 镜像

必须将可执行文件放置在可由虚拟机或物理机加载的介质上。 在本书中,我们将使用 ISO [22] 镜像文件作为介质,但是也可以使用软盘镜像,具体取决于虚拟机或物理机支持的内容。 我们将使用程序 genisoimage 创建 Kernel ISO 镜像。 必须首先创建一个文件夹,其中包含将位于 ISO 镜像上的文件。 以下命令创建文件夹并将文件复制到其正确的位置:

  mkdir -p iso/boot/grub       # create the folder structure
  cp stage2_eltorito iso/boot/grub/  # copy the bootloader
  cp kernel.elf iso/boot/       # copy the kernel

必须为 GRUB 创建一个配置文件 menu.lst。 此文件告诉 GRUB Kernel 的位置并配置一些选项:

  default=0
  timeout=0
  title os
  kernel /boot/kernel.elf

将文件 menu.lst 放置在文件夹 iso/boot/grub/ 中。 iso 文件夹的内容现在应如下图所示:

  iso
  |-- boot
   |-- grub
   | |-- menu.lst
   | |-- stage2_eltorito
   |-- kernel.elf

然后可以使用以下命令生成 ISO 镜像:

  genisoimage -R               \
        -b boot/grub/stage2_eltorito  \
        -no-emul-boot          \
        -boot-load-size 4        \
        -A os              \
        -input-charset utf8       \
        -quiet             \
        -boot-info-table        \
        -o os.iso            \
        iso

有关该命令中使用的标志的更多信息,请参见 genisoimage 的手册。 ISO 镜像 os.iso 现在包含 Kernel 可执行文件、GRUB 引导加载程序和配置文件。

2.3.5 运行 Bochs

现在,我们可以使用 os.iso ISO 镜像在 Bochs 模拟器中运行操作系统。 Bochs 需要一个配置文件才能启动,下面给出了一个简单配置文件的示例:

  megs:      32
  display_library: sdl
  romimage:    file=/usr/share/bochs/BIOS-bochs-latest
  vgaromimage:   file=/usr/share/bochs/VGABIOS-lgpl-latest
  ata0-master:   type=cdrom, path=os.iso, status=inserted
  boot:      cdrom
  log:       bochslog.txt
  clock:      sync=realtime, time0=local
  cpu:       count=1, ips=1000000

你可能需要根据 Bochs 的安装方式更改 romimagevgaromimage 的路径。 有关 Bochs 配置文件的更多信息,请访问 Boch 的网站 [23]。 如果将配置保存在名为