操作系统设计与实现
0.引言
我们的实验在QEMU上模拟操作系统。QEMU是纯软件实现的虚拟化模拟器,几乎可以模拟任何硬件设备。我们最熟悉的就是能够模拟一台能够独立运行操作系统的虚拟机。虚拟机认为自己和硬件打交道,但其实是和 Qemu 模拟出来的硬件打交道,Qemu 将这些指令转译给真正的硬件。简而言之,就是QEMU帮我们模拟了一个虚拟的计算机硬件,而我们要实现的操作系统就在QEMU模拟的硬件上运行。
1-1 计算机的物理地址空间

第一代PC基于16位Intel 808
处理器,只能寻址1MB的物理内存。所以早期PC的物理地址空间将从0x00000000
开始,到0x000FFFFF
结束,而不是0xFFFFFFFF
(32位)。标记为Low Memory
的640KB
空间是早期PC能够使用的唯一随机访问内存(RAM)。
从0x000A0000
到0x000FFFFF
的384KB区域(也就是640KB到1MB之间的区域)由硬件预留,用于特殊用途,如视频显示缓冲区(显存)和一些系统固件。这个保留区域中最重要的部分是Basic Input/Output System (BIOS)(硬件厂商在硬件上自带的一段启动的代码),它占用了从0x000F0000
到0x000FFFFF
的64KB区域。
在早期的PC中,BIOS保存在真正的只读存储器(ROM)中,但现在的PC将BIOS存储在可更新的闪存中。 BIOS主要负责对系统进行基本的初始化操作,如激活显卡、检查内存安装量等。 执行这个初始化之后,BIOS从一些适当的位置(比如硬盘)加载操作系统,并将机器的控制权传递给操作系统。
随着技术的发展,Intel最终使用80286
和80386
处理器突破了1MB的寻址,它们分别支持16MB和4GB的物理地址空间,但PC架构师仍然保留了原始的1MB物理地址空间布局,以确保与现有软件的向后兼容性。因此,现代pc在物理内存中有一个从0x000A0000
到0x00100000
的一个洞,将RAM划分为 “low memory
” 或 “conventional memory
” (前640KB)和 “extended memory
” (其他的部分)。
无论技术如何发展,BIOS的设置被保留了下来。 (当然2010年之后的计算机大部分升级成了UEFI启动,UEFI可以看作BIOS的优化升级版。我们的实验还是会从BIOS入手,这足够我们了解计算机启动的过程了,而且现在考研还是考BIOS的知识点。如果你对UEFI和BIOS的区别感兴趣,可以参考这篇知乎专栏。) 当计算机的开机键被按下,一切都是从运行BIOS开始的。
1-2 计算机的启动过程
所以说,不管是i386
(采用intel 80386
架构)还是之前的芯片,在加电后的第一条指令都是跳转到BIOS固件进行开机自检,然后将磁盘的主引导扇区中的内容加载到内存0x7c00
的位置,然后跳转到这里。
1-2-1 主引导扇区
主引导扇区,Master Boot Record
,简称MBR
,是磁盘里的第0个扇区。MBR占一个扇区,共512字节。
MBR里面的内容一般是一段可执行的指令,我们常常叫做Bootloader,大概翻译过来就叫启动加载器。从名字就可以看出,它的主要用途是把真正的操作系统加载到内存中,然后把控制权交给OS。
(注意在后面的讲义中为了方便,在表述上不再区分MBR和bootloader)
MBR的最后两字节是魔数0x55和0xaa,作用是告诉BIOS:这里是MBR,你找对了。把我加载上去就可以启动操作系统了。
有了这个魔数,BIOS就可以很容易找到可启动设备了:BIOS依次将设备的首扇区加载到内存0x7c00的位置,然后检查末尾两个字节是否为 0x55
和0xaa
如果成功找到了魔数,BIOS将会跳到 0x7c00
的内存位置,执行刚刚加载的启动代码,这时BIOS已经完成了它的使命, 剩下的启动任务就交给 MBR了;如果没有检查到魔数,BIOS将会尝试下一个设备;如果所有的设备都不是可启动的, BIOS就会抱怨:找不到启动设备。
有关于这一点,可以用 sudo head -c 512 /dev/sda | hd
看一看自己Linux的MBR长什么样。 (不过我在WSL跑这个没用,虚拟机可以跑,原因在于虚拟机开机硬件的工作是由主机通过软件模拟的) 至于为什么是0x7c00
这个地址,可以参考这个地址。
1-2-2 QEMU小实验
我们准备了一个简单的MBR,可以用下面这两条指令下载并解压:
$ wget wget https://git.nju.edu.cn/nju-se-oslab/oslab2025autumn/-/wikis/assets/mbr.zip
$ unzip mbr.zip -d mbr
注意:这个小实验和框架代码是完全无关的,建议把它放在和框架代码不同的文件夹。
解压并进入文件夹之后,首先用make
指令把它编译出来,然后make qemu
用QEMU打开它,可以看到在终端打印出了Hello, World!
但是我们要研究的是QEMU里的启动过程,所以我们先按Ctrl+C
把之前那个关掉,然后用make qemu-gdb
打开。
看Makefile可以发现相比之前加了-S
和-s
两个参数,前者是要求QEMU在启动时停住便于我们调试,后者是开了个端口让GDB可以连接,可以通过man qemu-system-i386
来看QEMU手册来了解这些内容。
接下来是再打开一个终端(别把之前的那个关了),用make gdb
打开GDB并连接到QEMU,可以看到[f000:fff0] 0xffff0: ljmp $0xf000,$0xe05b
,这就是QEMU开机的第一条指令,最前面的f000
是现在CS
段寄存器的值,fff0
就是IP
寄存器的值。但是,IP
应该是EIP
的低16位,而0xf000
似乎这个CS地址太大了…