前言
去年写的内核开发教程因为学业原因,不得不暂停更新。但是由于之前是临时起意,很多内容缺少安排和考虑,决定重新开始写一个新的系列。之前的专栏《从零开发UEFI引导的64位操作系统》不会被删除,并且可能会在未来的更新中被引用。
本教程将从内核被引导开始,开发一个基本的操作系统内核。
一、什么是操作系统内核
- 系统的内核:对于任何一个软件系统,在其中承载了系统的核心功能、为系统提供实现任意功能的基础设施的软件就被称为内核。在一定程度上,软件系统甚至可能仅需要内核就能正常运行。有linux服务器维护经验的读者大概会有类似的经历:在启动时修改内核参数
init为init=/usr/bin/bash,进入系统直接获得root权限恢复系统;系统启动后,唯二运行的软件就是linux内核和bash,就算systemd等核心系统服务没有在运行,系统也确实正常启动了。只要内核还在,软件系统就能够继续运行。 - 操作系统:操作系统是管理计算机硬件和软件资源的软件系统。操作系统内核通常包含
内存管理、进程管理、进程间通信、虚拟文件系统、设备管理、权限和用户系统等功能。这些功能也是本教程的主要内容。除了内核外,操作系统通常还有具有用户界面、网络、服务管理、构建工具套件等功能的软件。
二、准备工作
1. 技术准备(前置知识)
C++Rust:本教程将使用C++和Rust语言完成一个基本的操作系统内核。- 一些数据结构与算法的知识。
- 汇编语言:
gas或nasm基本语法,如果你要为非x86平台编写内核,那么必须会gas语法。另外还有C/C++和Rust的内嵌汇编,大致与gas语法相同,但是有一些区别。 - 构建系统:你需要自己编写一套构建脚本,本文会给出一些建议,但不会直接给出完整的构建脚本。
有关Rust:没学过Rust的不要紧,只要会C++,Rust中的概念就都能理解,读者可以自行选择自己喜欢的语言,对于Rust中不同于C++的语法,会在一段结束后在注释中解释。
2. 开发环境
linux发行版:能点进来看的读者,想必或多或少对linux有些了解,所以最好在linux环境下开发内核。用Windows或者Mac也可以,只要你能找到相同或者类似功能的工具就可以。这篇文章的目标是编写一个操作系统内核,所以除编写内核的内容之外,只要方便就好。C++和Rust编译工具链。qemu:调试内核。EDK2-OVMF:UEFI固件,将qemu内置的smbios换成OVMF以支持UEFI启动。- 称手的编辑器:不能是集成度很高的IDE,也不能是功能过于单一的文本编辑器。在项目配置和构建功能上,它应该给你很高的自由度。但是也应该有功能完善的语言服务器。
3. 资料准备
- 指令集手册:在设置处理器状态时需要用到。
4. 心态上的准备
- 开发内核并不是非常难,但也不是轻轻松松就能完成:
内核仅仅是操作系统中的一个核心的软件,只是难度比其他软件高一些。以学习为目的开发的玩具内核实际上可能并没有比学会数据结构与算法这种对数学水平要求较高的学科更难。操作系统说到底是一门工程学科,而不是理论学科,它是由大量程序员的大量实践产生的。而不是像算法或编译原理这种直接由数学理论指导产生的学科。它并没有理论学科那么依赖人的天赋。只是操作系统内核不得不包含很多功能,复杂程度高于其它软件。
- 做好查阅大量资料和反复调试内核的准备:
本教程基于x86_64架构开发内核,其它的指令集架构或许会有与x86_64架构相通的部分、不相通的部分。对于内核的引导、启动和内存管理等高度依赖硬件架构的部分,读者需要举一反三、自己编写和调试代码,同时需要查阅很多相关的文档等资料。本教程仅会对于x86_64相关的部分进行详细说明。
三、如何调试你的内核
现在假设你已经有一个编译好的multiboot2可引导内核镜像,这一节会指导你如何在qemu中引导你的内核,并进行调试。
1. 制作一个带有grub的虚拟磁盘镜像
简单起见,我们可以直接使用raw格式,即直接把文件当成磁盘设备进行读写。
我习惯使用doas而不是sudo来获取root权限,不使用doas的读者可自行换成sudo
- 创建空的磁盘镜像
$ touch debug.img
$ truncate debug.img 128M
- 格式化磁盘
$ fdisk debug.img
在fdisk中依次执行如下命令:
g # 创建gpt分区表
n # 创建一个新分区,一路回车即可
t # 给分区打EFI分区标签,EFI分区类型为1
w # 写入磁盘镜像并退出fdisk
- 挂载磁盘分区
$ doas losetup -f --show debug.img
此时终端会打印这个文件挂载到的设备名,这个命令是设置循环设备的,所以总是会挂载到/dev/loopX,将X替换为实际终端打印的那个即可。
$ doas partprobe
$ doas mount -m /dev/loopXp1 ./mnt
- 将编译好的multiboot2可引导内核镜像放进磁盘中
假设镜像文件为kernel,位于当前目录下
$ doas cp kernel ./mnt
- 写入grub
$ doas grub-install --target=x86_64-efi --efi-directory=./mnt --bootloader-id=GRUB --removable
如果是通常的安装linux系统,这一步之后就结束了,但是如果这时就卸载镜像放到qemu中运行,你会发现完全没法运行。
输入grub-install --help命令可以发现一条说明:
grub-install copies GRUB images into /boot/grub.
在GRUB镜像的拷贝阶段,grub-install会直接将/boot视为efi目录,而不是在参数--efi-directory中指定的目录。所以,需要手动将/boot/grub复制到./mnt中:
$ doas cp -r /root/grub ./mnt/
接下来需要生成一个grub.cfg来创建一个引导你的内核的选项:
$ grub-mkconfig > ./mnt/grub/grub.cfg
编辑mnt/grub/grub.cfg,生成的文件中会包含至少两个选项(menuentry块):你使用的linux发行版、UEFI Firmware Settings。将你使用的linux发行版的menuentry块删除,并添加一个用来引导你自己的内核的menuentry:
menuentry "YourKernel" {
set root=(hd0,gpt1)
multiboot2 /kernel
}
其中YourKernel会出现在grub引导界面的选项上。第二行是root目录挂载命令,挂载的设备取决于这个磁盘镜像会被挂载到qemu的哪个设备上,当挂载到-hda设备时,应该使用hd0,gpt1代表这个设备使用了gpt分区表,分区表的第一个分区会被挂载到root目录。
- 卸载镜像
$ doas umount /dev/loopXp1
$ doas losetup -d /dev/loopXp1
至此,可引导的磁盘镜像完成。
2. 找到UEFI镜像
qemu默认使用smbios启动,我们需要使用UEFI启动。通常,qemu的完整安装包会附带已经编译好的edk2,我们直接使用即可。通常我们需要使用的镜像位于/usr/share/edk2/x64/OVMF.4m.fd。对于不同的linux发行版它有可能存在于不同的目录中,可以在/usr目录中递归搜索名为edk2的目录。
3. 使用qemu启动和调试
$ qemu-system-x86_64 -m 1G -hda debug.img -bios /usr/share/edk2/x64/OVMF.4m.fd -s -S
-m选项指定了内存大小。-s -S这两个选项会在localhost:1234端口上开启一个gdb远程调试服务端。
假设在编译内核时开启了调试信息,那么在multiboot2可引导内核镜像中就会包含所有的调试信息,假设内核镜像文件kernel在当前目录下。
- gdb连接qemu调试
$ gdb
(gdb) file kernel
(gdb) target remote localhost:1234
- lldb连接qemu调试
$ lldb
(lldb) target create kernel
(lldb) gdb-remote localhost:1234
另外,在内核调试的过程中需要注意:
- qemu并不支持gdb和lldb的所有调试功能。
- qemu在x86_64环境下开启kvm时,若设置的断点处的内存还没有被映射至物理地址,则无法真正插入断点。gdb会忽略这个问题,并照常运行。而lldb则会显示一个警告并阻止continue命令。这是因为qemu的调试服务端仅在continue命令时在设置好的断点地址处尝试插入断点,并不会因为页表的更新而重新尝试插入。
断点的原理:大多数指令集都有数个用于调试的指令,例如x86_64的
int1和int3指令。int1用于硬件调试,int3用于软件调试。调试软件/虚拟机会将断点的地址处临时换成int1或int3指令,int1和int3触发中断后,会被用于调试的虚拟机截停,或被调试软件截停,截停后立即将断点处的指令复原,然后等待使用者使用命令让程序/虚拟机继续运行。因此,断点处的地址已经被映射到有效的物理地址(也就是此处的内存可以被读写),是插入断点的必要条件。
浙公网安备 33010602011771号