自制 os 极简教程 2:史上最难的 hello world

来自公众号:低并发编程

然后这一期重新做了这个系列的封面,显得高科技一点,嘿~

前言

在上一篇《自制 os 极简教程 1:写一个操作系统有多难》中,以我的从小白到大白的入坑经历,聊了下我在自制操作系统这条路上的心路历程。

有的人可能小时候就完成了这个愿望。有的人可能是大学的某一门操作系统实验课上跟着课程进度完成了一款操作系统。还有的人可能是工作后一次次的好奇心或是对底层原理的渴求,慢慢熬出了一个操作系统。当然还有的人可能这辈子都在想却一直没有动手。

不论你是写一个操作系统,还是深入地了解一款操作系统的实现,我觉得都是成为高手的必经之路,也可以说是早晚要跨越的一道坎,那不如现在就跟我一起开始吧~

动手前要知道的事

  1. 别完全依赖这个系列。这个系列不能让你跟一遍就能写出一个操作系统。也就是说我只会将核心的代码贴出来,并且提供核心思路,以及我当时做某个地方时遇到的坑。很多细节的内容是需要你去看更详尽的资料了解的,但我会告诉你应该看什么。

  2. 不会讲汇编和 C 语法层面的知识。这也是极简的体现,好多书籍和教程比较大就是因为把操作系统层面的知识和汇编与 C 语言层面的知识混着讲,有点像电视剧里插播大量广告一样,这样也会导致主流程显得不突出。

  3. 抓大放小,但同时用力抠细节。听着比较矛盾,主要是操作系统这个东西要宏观非常宏观,要细节又超级细节,属于上天入地的存在。所以如果你是想用一次蹲厕所的时间吸收一个章节的内容,那你就抓大放小,不要去管各种细节。但如果你被一个细节卡住了,比如你不知道全局描述符表的具体数据结构你就无法进行下去了,这时候也不要吝啬自己的时间,花一个下午或者一整天把全局描述符表的结构一个字节一个字节地抠一遍,不用担心浪费时间,长远看这只会节省你的时间。

经过了这么多铺垫,终于到了几乎所有教程的第一部分:hello world

不知道为什么,我脑子里会想到 abandon 这个词...

这个 hello world 要做什么

要做的效果就是,把你写的操作系统代码,也就是一堆 0101..组成的二进制数,写到硬盘上的某个位置,把这块硬盘插在电脑上,按下电脑的开机键,最终在黑黑的屏幕上输出一个 hello world 字符串。

这个过程展开了揉碎了说,就是:

  1. 把硬盘从电脑里拿出来
  2. 从硬盘第一扇区开始,把 0100001000100111... 烧录进去
  3. 再把硬盘塞回去
  4. 按下开机键
  5. 屏幕出现 hello world

但这个成本比较大,我们将上面的过程全都用软件去模拟,这里需要你跨越的第一心理障碍就是,请相信它们是一样的

我们用 bochs 虚拟机代替真实的电脑,用无格式的虚拟硬盘文件代替真实的硬盘,用 dd 命令模仿烧录的过程。最终你看到的就是这个样子。

友情提示,bochs 软件的安装配置,虚拟硬盘文件是什么,dd 命令的安装与用法,需要自己去网上查哦,这也是为了不影响主流程(yin wei lan)。这个过程不简单,想当初 bochs 软件安装到配置完毕,折磨了我好久。

复习电脑开机启动流程

自制操作系统的 hello world 可能是最难的 hello world 程序了,难点不在于代码量上,而是在于对开机过程的理解上。还好这里我之前一篇文章做足了铺垫《全网最硬核解读计算机启动流程》,这里我也偷个懒,要求不了解这个过程的同学去读一下,不过这里我也对这篇文章进行了一个简单总结:三个前置知识四次跳跃

首先说下三个前置知识,这些必须假设你是知道的,否则我不可能从质子中子原子开始讲起。这三个前置知识就是:

  1. 内存是存储数据的地方,给出一个地址信号,内存可以返回该地址所对应的数据。
  2. CPU 的工作方式就是不断从内存中取出指令,并执行。
  3. CPU 从内存的哪个地址取出指令,是由一个寄存器中的值决定的,这个值会不断进行 +1(抽象意义的) 操作,或者由某条跳转指令指定其值是多少。

有这三个前置知识后,接下来就是要记住计算机开机后的四次关键跳跃,因为这些都是当时 Intel 和 BIOS 等制作厂商的大叔们定下来的,没什么道理可言,记住就好:

  1. 按下开机键,CPU 将 PC 寄存器的值强制初始化为 0xffff0,这个位置是 BIOS 程序的入口地址(一跳)

  2. 该入口地址处是一个跳转指令,跳转到 0xfe05b 位置,开始执行(二跳)

  3. 执行了一些硬件检测工作后,最后一步将启动区内容加载(复制)到内存 0x7c00,并跳转到这里(三跳)

  4. 启动区代码主要是加载操作系统内核,并跳转到加载处(四跳)

动手实现启动区代码

前三跳,都是 CPU 硬件以及 BIOS 写死的一段代码搞定的,不用我们管,你也千万不要去管它,否则你就再也到不了 hello world 这一步了...

我们要实现的,就是启动区代码这个第四跳。启动区本来的作用是加载操作系统内核的,不过我们由于是 hello world 程序,我们就只让启动区代码实现一个向屏幕输出字符串的功能就好啦~

铺垫了那么多,下面开始加速了!

第一步:新建一个文件 boot.s

;BIOS把启动区加载到内存的该位置
;所以需设置地址偏移量
section mbr vstart=0x7c00

;直接往显存中写数据
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'
mov byte [gs:0x0a],' '
mov byte [gs:0x0c],'w'
mov byte [gs:0x0e],'o'
mov byte [gs:0x10],'r'
mov byte [gs:0x12],'l'
mov byte [gs:0x14],'d'

jmp $

;512字节的最后两字节是启动区标识
times 510-($-$$) db 0
db 0x55,0xaa

代码很好理解,主要有三部分

开头:section mbr vstart=0x7c00

由于这个代码最终会被 BIOS 从硬盘的启动扇区加载到内存中的 0x7c00 位置,所以 section mbr vstart=0x7c00 就表示了这个偏移量,否则里面的变量地址呀、跳转地址呀,将会不正确。

结尾:db 0x55,0xaa

最后两个字节是 0x55 0xaa,是启动区的标志,不是这两个字节的话 BIOS 就不会把它当做启动区,仅仅就是硬盘的第一扇区而已,也就不会加载里面的内容。

中间:mov byte [gs:0x00],...

中间的代码就是最终看到效果的关键部分。启动流程中讲了实模式下的内存分布,知道 0xB8000 - 0xB8FFFF 这段内存空间是文本模式下显存的内存映射区域,往这个内存区域里写数据,就相当于往显卡的内存区域里写数据,也就相当于在屏幕上输出文本了。至于显卡是怎么把写到它内存上的数据转化成屏幕上的小亮点的,别问我,问我也不知道,如果你这块的好奇心大于代码,说明你适合做硬件工程师,嘿嘿~

第二步:编译它

nasm -o boot.bin boot.s

之前说好啦,不讲汇编知识,说不讲就一点也不讲,哈哈~

第三步:创建虚拟磁盘映像,并填充第一扇区

用 bochs 自带的工具创建一个大小为 60M 的无格式的虚拟磁盘映像

bximage -mode=create -hd=60 -q os.raw

将刚刚编译好的二进制文件 boot.bin 用 dd 命令写入磁盘第一扇区,就相当于将数据烧录进磁盘的过程。

dd if=boot.bin of=os.raw bs=512 count=1

由于是无格式的虚拟硬盘文件,我们用二进制编辑器打开 os.raw,和我们编译出的 boot.bin 完全一样,就是下面这一串内容:

当然 boot.bin 只有 512 字节,而 os.raw 有 60M,但他们开头的前 512 字节都是一样的。注意看最后两个字节是 55 AA,这是我们代码中写入的,作为启动区标识。开头有很多非零的数据,这个就是编译后的机器码。中间的那些零,就什么都没有啦,因为既没有被编译的机器指令落在这个区域,又没有数据放在这个区域。

你当然可以不用通过写汇编代码、编译、再dd写入这种方式获得这个二进制文件,你也可以直接把这个二进制文件用键盘一位一位地敲出来。

而且如果你把这一段二进制数据,在现实中真真正正烧录进一块真实硬盘的第一扇区,并且把它插在一个电脑上,那当你按下开机键后,屏幕上就会出现 hello world 字符串。嗯就是这么简单粗暴。

现在我们有了一块烧录上了“操作系统”的虚拟硬盘文件 os.raw,也有了模拟电脑的虚拟机 bochs,接下里就插入硬盘、按下开机键就好啦~

第四步:用 bochs 启动它

首先 bochs 配置文件 bochs.properties 中指定磁盘,则 bochs 虚拟机就可以读入这块虚拟磁盘文件了,就相当于在真机上把磁盘插进去。

ata0-master: type=disk, path='os.raw', mode=flat, cylinders=121, heads=16, spt=63

然后用 bochs 命令启动虚拟机,就相当于按下了电脑开机键。

bochs -f bochs.properties

于是就看到了下面的画面

然后请自行脑补在真机上运行的画面

总结流程图

之前的所有努力,下面一张图就搞定啦

总结起来真是简单得要死,把汇编语言写的 boot.s 文件编译成纯二进制格式的文件 boot.bin,再把这个纯二进制文件写入磁盘(用 bochs 生成的虚拟硬盘文件 os.raw)的第一扇区,最后用 bochs 启动电脑即可。

电脑启动后,BIOS 会把刚刚写入磁盘第一扇区的二进制数据,加载到内存 0x7c00 开始处的后面 512 字节中,然后一个跳转指令跳转到 0x7c00 处开始执行指令。

第一个指令就是由 mov ax, 0xb800 这条汇编指令所编译成的机器码指令,然后不断向后面执行,后面指令的含义就是往显卡映射的内存地址 0xB8000 - 0xB8FFFF 处写数据,显卡便会根据这篇区域的数据,往我们的屏幕上输出一个个小亮点构成的字符,我们就看到了

  hello world  

尾声

如果你跟到这里,并且可以用各种灵活的方式来实现这个 hello world,那么给自己点个赞吧!

这个 hello world 之所以难,我从我的经历来分析,难在一个理论,一个实践,和一个心理障碍

理论就是,计算机的启动流程,这就包括了一部分 CPU 硬件,计算机组成原理,以及 BIOS 规范等各种知识,杂糅在了一起,这部分可以通过《全网最硬核解读计算机启动流程》这篇文章梳理。

实践就是,环境搭建,这部分超级头疼,是一座大山。但一旦你熟练之后,它就只是工具而已了。bochs 我用不习惯,换 qemu 也行。windows 用不顺,在 linux 上也行。nasm 看着烦,用其他汇编编译器也可以。总之难的时候是一座无法翻越的大山,简单时就只是工具而已了。这部分我希望你自己去探索,碰几次壁别放弃,就过了这关。

心理障碍就是,总觉得虚拟机和真机不一样。我一开始就老是纠结,这东西在真机上能运行么?以前装系统都用光盘那我这东西能写到光盘里么?人家虚拟机都是 virtual box 那我能不能用 virtual box 启动啊?人家装系统都是 U 盘启动一个 PE 然后选择一个磁盘安装系统,我这个咋和那个有这么大差距呢?请先别纠结这些,就好比你不会加减乘除的时候,问人家那个积分啊微分啊都怎么搞的一样。你得一步步来,先忍住了,相信每一个知识到后面都会用到。等学到后面了,要么是你自己能做出这种工业级的操作系统,要么就是认为那些已经不重要了,你会觉得一开始纠结的时间,真不如先接受着,学下去。

不过,有些宝贵的知识又正是因为走了弯路才拥有的。所以,管他呢~

hello world 就讲到这里,这个程序是梦开始的地方,后面的世界更精彩,我们下节课见!

(0)

相关推荐