【自制操作系统02】环境准备与启动区实现
一、计算机启动过程回顾
要想写一个启动区代码,就需要了解开机的启动过程,因为开机过程中一些硬件的规定决定了这段代码应该怎么写,不明白没关系,且听我慢慢道来。
具体过程在我上一篇文章 【自制操作系统01】硬核讲解计算机的启动过程 讲述得一清二楚,这里我们简单回顾一下。了解开机过程,并不是一个简单的问题,我总结成你需要有三个前置知识,并记住四次跳跃。
首先说下三个前置知识,这些必须假设你是知道的,否则我不可能从质子中字原子开始讲起。这三个前置知识就是:
- 内存是存储数据的地方,给出一个地址信号,内存可以返回该地址所对应的数据。
- CPU 的工作方式就是不断从内存中取出指令,并执行。
- CPU 从内存的哪个地址取出指令,是由一个寄存器中的值决定的,这个值会不断进行 +1 操作,或者由某条跳转指令指定其值是多少。
有这三个前置知识后,接下来就是要记住计算机开机后的四次关键跳跃,因为这些都是当时 Intel 和 BIOS 等制作厂商的大叔们定下来的,没什么道理可言,记住就好:
- 按下开机键,CPU 将 PC 寄存器的值强制初始化为 0xffff0,这个位置是 BIOS 程序的入口地址(一跳)
- 该入口地址处是一个跳转指令,跳转到 0xfe05b 位置,开始执行(二跳)
- 执行了一些硬件检测工作后,最后一步将启动区内容加载(复制)到内存 0x7c00,并跳转到这里(三跳)
- 启动区代码主要是加载操作系统内核,并跳转到加载处(四跳)
二、我们需要做什么
假如我们有上帝之手
知道了上面这些后,我们就可以写启动区代码了。别急,我们先想一下我们需要做什么。先不说需要什么软件,需要什么代码,这些都不是核心问题,我们先想一下,假如我们有无所不能的上帝之手,我们应该怎么做才能让开机过程顺利走下去呢?
首先,BIOS 里面有一段写死的代码,会帮我们把启动区的第一扇区的 512 字节的内容,原封不动复制到内存 0x7c00 这个位置,并跳转到此处,这个是不用我们管的。
所以我们要做的就是,把我们的指令,写到硬盘(当然也可以是光盘、软盘、U 盘,这里我们就只拿硬盘举例)的第一扇区的 512 字节,并把这部分标记为“启动区”(就是把最后两个字节写死为 0x55, 0xaa)。
好了,那我们可以试想下,如果我们有上帝之手,应该是这样一个操作流程。
- 把硬盘从电脑里拿出来
- 在其第一扇区的位置从头开始写,010100111001010...,保证最后两个字节是 0x55, 0xaa(十六进制表示)
- 再把硬盘塞回去
- 按下开机键
就是这么简单,这样电脑就会乖乖从开机后的某一时刻,开始执行你写的 010100111001010 的代码了。
没有上帝之手的我们该怎么办?
其实并不需要上帝之手,有很多工具可以直接往磁盘中写数据,或者你把硬盘拿出来用电路板操作也是可以的,然后在真机上跑你的操作系统。但这样做太麻烦了,不过就是有人会特别纠结这一点,非要在真机上跑出来才肯罢休,这种精神是值得称赞的。但学习计算机,该有的抽象还是要有,能用低成本的方式实现等价的事情,不去纠结这一点是很必要的。
所以我们还是用低成本的方式来做今天这个事,刚刚上面说的真机,我们用虚拟机来实现。上面说的硬盘,我们用虚拟磁盘映像文件来实现。刚刚说的写硬盘这个过程,我们也用软件命令往这个虚拟磁盘映像里写。这其实和上面的效果完全等价,只不过都虚拟化了而已。总结起来就是:
- 真机:用虚拟机实现(QEMU)
- 硬盘:用虚拟磁盘映像实现(img)
- 写数据:用软件往磁盘映像中写(dd 命令)
三、最直观的方式写一个操作系统的启动区
好了,有了上面的知识储备,终于可以实践了。可是一上来就三个工具,听起来还是很恐怖,别怕,这部分我们只用到上述的 QEMU 工具即可。
因为虚拟磁盘映像,其实就只是一个二进制文件而已,在 Windows 电脑上直接右键,新建一个文本文件就可以了。而写数据这个过程,我们可以直接用二进制编辑器打开这个文件,直接往里面写就好了,暂时也不需要什么工具帮助。
因此总结起来,我们这部分的开发环境,就是下面的两个(具体安装步骤见章节四)
- Notepad++(含二进制编辑插件)
- QEMU
此时问题已经简化到极致了,我们只需要编辑一个虚拟磁盘映像文件,再用 QEMU 启动一下就好了。那么我再简化一下,虚拟磁盘映像有很多种格式,但其实映射到真正的硬盘中,是一样的,只不过多种多样的格式,适合我们一些工具进行方便读取和展示。
这里我们使用和真正硬盘中数据一一对应的无格式的格式(raw),哈哈这个对刷过 B 站的人应该很容易理解,就是生肉,不经任何加工的原汁原味的格式。这个格式是什么样的呢?该格式中记录磁盘第一扇区的 512 字节的位置,就是该文件从第一个字节开始往后的 512 字节,就是这么简单粗暴。
ok,接下来,我们终于可以开始真正动笔啦!
--------------- 正片开始预警 ------------------
不废话,直接上三部曲。
第一步:新建文件
右键,新建,文本文档。随便取一个名字和扩展名。我这里取的是 mbr.raw。mbr 的意思是主引导记录,raw 的意思是无格式的虚拟磁盘映像格式。但你不必和我一样。
第二步:写入数据并保存
用 Notepad++ 文本编辑工具打开它,切换到 16 进制模式,开始一个字节一个字节写如下内容,写好后记得保存。
第三步:QEMU 启动
shift + 右键,在此处打开命令行窗口,输入如下命令并按下回车
qemu-system-i386 mbr.raw
等一秒钟,QEMU 启动,并展示出如下画面。
第四步:没了
哈哈就是这么简单,如果你看《30 天自制操作系统》,也有一部分会让你编辑这个二进制文本文件,但你几乎要花上半小时时间敲出来,再花半小时时间检查下到底哪里错了,最后放弃了,再花十分钟时间找到这本书的源码直接 copy 出来,我觉得完全没有必要。
如果你还觉得多,那你可以试试下面这版:
但再用 QEMU 启动时会是这个效果(注意第一个字母 h 是笔者通过指令打上去的哦):
嘿嘿,这时你是不是发现了什么,这版比上一版少了哪部分呢?
如果你还不服,那我再来个赖皮版:
别找了,除了最后两个字节之外,其余的都是 0,用 QEMU 启动后是这个样子,这些纯是 QEMU 虚拟机中 BIOS 的输出了,完全没有我们的一行指令,但它却正确地开机了。
哈哈,这回我可以自豪地说,我这个是世界上最简单的启动区了么?没有骗你吧。相信你已经有很多疑问了,那我们接下来就是揭秘这个二进制文件的时刻。
四、开发环境准备
不废话,先直接上开发环境。
文本编辑器:Notepad++
不要下载太高版本,似乎运行插件会有问题,我的是 Notepad++ 7.5.4 release,可以参考。
虚拟机:QEMU
找到 Windows 版的下载按钮,同样也是无脑下载,下载好后记得把目录加入到环境变量里。
汇编工具:NASM
官网下载地址:https://www.nasm.us/
同样也是找到对应的 Windows 系统,选择一个版本下载,这里我推荐:
直接点这个连接选择 nasm-2.14.02-installer-x64.exe 下载即可。
下载好后同样也记得把 bin 目录加入到环境变量。
虚拟磁盘映像工具:dd
下载完之后其实就是一个 dd.exe 文件,把这个文件所在的目录,也加到环境变量里。
OK,整个环境搭建的工作,就结束了,直接全部官方网站一键下载,配置下环境变量,就大功告成了,是不是很简单。这里又想吐槽下《30 天自制操作系统》,作者用了自己写的汇编工具,自己写的磁盘拷贝工具,自己写的虚拟映像生成工具,还有自己为 QEMU 写的启动脚本。虽然直接下载其代码就能直接运行启动,但我就是感觉很不友好。
五、最简启动区代码的实现
好了,现在我们开始真正实现这个启动区代码了。咦,没错,刚刚其实已经实现过了,但没有人会用这种方式写操作系统。不过当然也可以,你可以只用一个文本编辑器,从第一个字节开始敲,敲到最后一个字节,完成一个操作系统的制作。话说在没有汇编语言的时候,可不就是这样做的么,那时候虚拟机更是没有,需要用纸带在真机上一遍一遍试。这样想想看现在幸福多了。
通过上面的环境介绍不难猜出,我们将使用 NASM 汇编语言来实现这个启动区,最后编译出来的二进制文件,其实和我们上面手写的二进制文件是一样的,反过来说就是,上面给大家展示的二进制文件,可不是我一个个手打的哦,是我实现写好了汇编代码,然后编译出来的(偷笑)。
我们就拿刚刚的第一版二进制文件来看,我们可以把这里面的字节一一取出来,去查 Intel x86 指令集架构下,这些机器指令对应的 NASM 汇编代码是什么?如果你足够耐心,是可以一个个查出来的,但我们 NASM 这个软件本身就有现成的工具来实现这个“反编译”的功能。
ndisasm -o0x7c00 mbr.raw
我们看到:
- 第一列就是内存地址,第一行 00007c00,刚好应了我们上面说的 BIOS 会把启动区的代码加载到内存 0x7c00 这个位置。
- 第二列就是机器指令,对比上面的二进制文件,我们可以看到他们是一一对应的。
- 第三列就是反编译出来的汇编指令:后面中文已经标注了,第一部分是一段清屏指令的代码,不然屏幕会乱糟糟出现 QEMU 本身的 bios 输出。第二部分就是为什么能在屏幕上打印出 hello。第三部分都是 0,其实这不是指令,但如果硬要给他解读成指令也是可以的。
也就是说,我们按照第三列的汇编指令把代码写出来,再编译,就可以了,对吧?没错,不过还需要进行一些小调整,我们直接上代码。
第一步:新建一个文本文件,命名为 mbr.asm
;----BIOS把启动区加载到内存的该位置,所以需设置地址偏移量
section mbr vstart=0x7c00
;----卷屏中断,目的是清屏
mov ax,0x0600
mov bx,0x0700
mov cx,0
mov dx,0x184f
int 0x10
;----直接往显存中写数据
mov ax,0xb800
mov gs,ax
mov byte [gs:0x00],'h'
mov byte [gs:0x02],'e'
mov byte [gs:0x04],'l'
mov byte [gs:0x06],'l'
mov byte [gs:0x08],'o'
;----512字节的最后两字节是启动区标识
times 510-($-$$) db 0
db 0x55,0xaa
我们看到,和上面反编译出来的汇编语句,几乎是一样的,但还是有几处不同,你找找看。下面我来一一解释。
首先第一行
section mbr vstart=0x7c00
其实也可以写成:
org 0x7c00
这个的意思用通俗的话讲就是把以下代码加载到内存 0x7c00 这个位置,但这个说法不正确,其本质是 指定一个地址,后面的程序或数据从这个地址值开始分配。比如有个跳转指令,跳转到某处的代码,编译器可以通过该处代码偏移了第一行代码多少来计算出一个相对的偏移地址,但却无法知道其绝对地址是多少,所以有必要让程序员来告诉编译器这个信息。
第二部分
;----卷屏中断,目的是清屏
mov ax,0x0600
mov bx,0x0700
mov cx,0
mov dx,0x184f
int 0x10
前面几行是设置一些寄存器的值,有点像高级语言中方法的入参。最后一行 int 0x10 是调用 BIOS 的 10 号中断程序,有点像高级语言中调用一个方法。这个不是我们的重点,总之它实现了一个效果就是清屏。但你不写这一段也可以,无非就是让屏幕多了 QEMU 本身的输出,难看一点罢了。
第三部分
;----直接往显存中写数据
mov ax,0xb800
mov gs,ax
mov byte [gs:0x00],'h'
mov byte [gs:0x02],'e'
mov byte [gs:0x04],'l'
mov byte [gs:0x06],'l'
mov byte [gs:0x08],'o'
这部分的目的就是让我们的操作系统运行能够看出一点效果,有很多种方式,我们选择直接往显存中写数据的方式,这种方式最简单,也最直观。
上一节课我们讲了实模式下的内存分布,知道 0xB8000 - 0xB8FFFF 这段内存空间是文本模式下显存的内存映射区域,往这个区域里写数据,就相当于在屏幕上输出文本了。
所以前两行就是往 gs 段寄存器中写入该内存区域的起始地址 0xB800(注意这里不是少写了一个 0,因为实模式下段寄存器还需要乘以 16,所以此处先除以 16,详见上一节课的内容)。
后面的五条 mov 语句,就是往这片内存区域的开始的几个字节处,分别写入 'h'、'e'、'l'、'l'、'o'。你可能注意到上面反汇编出来的语句中后面不太一样,其实也可以写成:
mov byte [gs:0x0],0x68
mov byte [gs:0x2],0x65
mov byte [gs:0x4],0x6c
mov byte [gs:0x6],0x6c
mov byte [gs:0x8],0x6f
这是完全等价的,0x68 是 ‘h’ 的 ASCII 码,你写成 ‘h’,汇编工具会自动帮你转换成 0x68。
第四部分
;----512字节的最后两字节是启动区标识
times 510-($-$$) db 0
db 0x55,0xaa
- $ 代表当前行行首的标号(段内地址)
-
\[ 代表当前段的起始汇编地址(段内地址) \]
第二步:编译
我们上一步得到了 mbr.asm,现在执行下面的命令来编译它,生成一个 mbr.bin:
nasm -o mbr.bin mbr.asm
执行完后可以看到文件夹下多了一个叫 mbr.bin 的文件,用 Notepad++ 打开它,看看是不是和上面我们看到的二进制文件一样呢?
第三步:创建虚拟磁盘映像,并填充第一扇区
实际上,我们上面得到的 mbr.bin 文件,直接用 QEMU 启动也是可以的,因为它与一个 raw 格式的虚拟磁盘映像是一模一样的。不过我们还是装模作样地创建一个虚拟磁盘映像,然后把刚刚的 bin 文件填充到磁盘的第一扇区,这样才像回事嘛。
执行下面的命令创建一个虚拟磁盘映像:
qemu-img create -f raw mbr.raw 1440K
我们看到后面的数字为 1440K,这就更像回事了,刚刚我们自己手写的二进制文件,只有 512 字节,没有那个硬盘是这么小的。这回我们用工具直接创建一个 1440K 的映像,就更接近真实的硬盘啦。
将 mbr.bin 文件的内容,装载到 mbr.raw 这个空磁盘映像文件的第一扇区:
dd if=mbr.bin of=mbr.raw bs=512 count=1
该命令也是能够见名知意,就是从 if=mbr.bin 这个文件,往 of=mbr.raw 这个文件拷贝数据,以 bs=512 字节大小作为单位,拷贝 count=1 这么多单位的数据。
第四步:启动
hooo!到了这步,和我们一开始的情形就是一样的了,我们有了一个 raw 格式的虚拟磁盘映像文件,我们也有了 QEMU 这个虚拟机可以进行模拟,自然接下来就是最激动人心的时刻(其实早就激动过了)。
qemu-system-i386 mbr.raw
运行效果也是见了很多次的:
好了,启动区代码就是这么简单,其实重点在于对整个过程的理解,并不在于最终运行的那一刻,所以你可以看到大部分时间是在帮你捋顺这个过程,实际的代码量,很少很少。如果本 chat 中有不了解的地方,欢迎在留言区留言,或者先去看一下我上一节课的内容,好多问题可能都会找到答案:
六、开源项目和课程规划
如果你对自制一个操作系统感兴趣,不妨跟随这个系列课程看下去,甚至加入我们(下方有公众号和小助手微信),一起来开发。
项目开源
当你看到该文章时,代码可能已经比文章中的又多写了一些部分了。你可以通过提交记录历史来查看历史的代码,我会慢慢梳理提交历史以及项目说明文档,争取给每一课都准备一个可执行的代码。当然文章中的代码也是全的,采用复制粘贴的方式也是完全可以的。
如果你有兴趣加入这个自制操作系统的大军,也可以在留言区留下您的联系方式,或者在 gitee 私信我您的联系方式。
课程规划
本课程打算出系列课程,我写到哪觉得可以写成一篇文章了就写出来分享给大家,最终会完成一个功能全面的操作系统,我觉得这是最好的学习操作系统的方式了。所以中间遇到的各种坎也会写进去,如果你能持续跟进,跟着我一块写,必然会有很好的收货。即使没有,交个朋友也是好的哈哈。
公众号 - 低并发编程