0.引言

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

1-1 计算机的物理地址空间

Alt text

第一代PC基于16位Intel 808处理器,只能寻址1MB的物理内存。所以早期PC的物理地址空间将从0x00000000开始,到0x000FFFFF结束,而不是0xFFFFFFFF(32位)。标记为Low Memory640KB空间是早期PC能够使用的唯一随机访问内存(RAM)。

0x000A00000x000FFFFF的384KB区域(也就是640KB到1MB之间的区域)由硬件预留,用于特殊用途,如视频显示缓冲区(显存)和一些系统固件。这个保留区域中最重要的部分是Basic Input/Output System (BIOS)(硬件厂商在硬件上自带的一段启动的代码),它占用了从0x000F00000x000FFFFF的64KB区域。

在早期的PC中,BIOS保存在真正的只读存储器(ROM)中,但现在的PC将BIOS存储在可更新的闪存中。 BIOS主要负责对系统进行基本的初始化操作,如激活显卡、检查内存安装量等。 执行这个初始化之后,BIOS从一些适当的位置(比如硬盘)加载操作系统,并将机器的控制权传递给操作系统。

随着技术的发展,Intel最终使用8028680386处理器突破了1MB的寻址,它们分别支持16MB和4GB的物理地址空间,但PC架构师仍然保留了原始的1MB物理地址空间布局,以确保与现有软件的向后兼容性。因此,现代pc在物理内存中有一个从0x000A00000x00100000的一个洞,将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的位置,然后检查末尾两个字节是否为 0x550xaa如果成功找到了魔数,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地址太大了…