Linux0.11源码趣读

用源码打开操作系统“黑盒子”

Linux,Linux 0.11,内核,源码,操作系统,

闪客 “低并发编程”公号作者

01|开机后最开始的两行代码是什么?

讲述:闪客 大小:9.41MB 时长:00:09:49
00:00
1.0×

你好,我是闪客。

从这一讲开始,我将带你一起进入操作系统的梦幻之旅!

一提起操作系统,是不是感觉很硬核?不过你别担心,每一讲的内容都很精练,而且你也不要带着很大的负担去学习,只需要像读小说一样,跟着我一讲一讲地看下去就好。

这一讲我们要讨论的问题是,开机后执行的第一行操作系统代码是什么?在这行代码之前又发生了什么?看完这一讲,你就知道答案了。

当你按下开机键的那一刻,在主板上提前写死的固件程序 BIOS 会将硬盘中启动区的 512 字节的数据,原封不动复制到内存中的 0x7c00 位置处,并跳转到这里运行。

图片

如果你能理解我上面说的话,那么恭喜你,接下来你只需要跟着代码一点点往后推导和品味,就会慢慢建立起整个操作系统的大厦。

但如果上面的这个过程你很困惑,那可能会在这卡一阵子。不过没关系,很多人都会卡在这个原本很简单的问题上,然后就从入坑到放弃了,我曾经也在这卡了很久。

接下来,我们一步一步地梳理。

开机后初始化指向 BIOS

首先,CPU 中有个 PC 寄存器,这里面存储着将要执行的指令在内存中的地址。当我们按下开机键后,CPU 就会有个初始化 PC 寄存器的过程,然后 CPU 就按照 PC 寄存器中的数值,去内存中对应的地址处寻找这条指令,然后进行执行。

初始化的值是多少呢?Intel 手册规定,开机后 PC 寄存器要初始化为 0xFFFF0,也就是从这个内存地址开始,执行 CPU 的第一条指令。

简单来说,初始化的这个值,其实就是 Intel 强行规定的而已,硬件厂商照着做就行。

你可能会有个疑问,不是说 CPU 根据这个地址值去内存中找指令吗,现在怎么找到了 BIOS 里?

其实,有个地方我说得并不严谨,CPU 并不是去内存(RAM)中去找,而是把 0xFFFF0 作为 CPU 的地址线信号传输出去,去这个地址线对应的位置处找。

你可能又问,难道 CPU 的地址线连接的设备还不仅仅是内存?没错,CPU 地址线连接的有 RAM(也就是我们常说的内存),有 ROM(也就是我们上图中的 BIOS),还有一些外设的 IO 端口,叫做 Memory-Mapped IO,我们暂时涉及不到。

图片

所以,刚刚的 0xFFFF0 就指向了 BIOS 程序所在的 ROM 区域。当然,如果你不管它到底指向了哪也没问题,反正是一行代码,就从这里开始往后执行就好。

读取硬盘启动区(第一扇区)

若想了解 BIOS 程序所做的事情,我们得先了解一下启动区。

启动区一定在第一扇区,但第一扇区并不一定是启动区。

那什么是启动区呢?启动区的定义非常简单,只要硬盘中的 0 盘 0 道 1 扇区(第一扇区)的 512 个字节的最后两个字节分别是 0x55 和 0xaa,那么 BIOS 就会认为它是个启动区。

对于我们理解操作系统而言,此时的 BIOS 仅仅就是个代码搬运工,把 512 字节的二进制数据从硬盘搬运到了内存中而已。作为操作系统的开发人员,我们仅需要把操作系统最开始的那段代码编译出来,并存储在硬盘的 0 盘 0 道 1 扇区即可。之后,BIOS 会帮我们把它放到内存里,并跳转过去执行。

而 Linux-0.11 最开始的代码就是这个用汇编语言写的 bootsect.s,位于 boot 文件夹下。

图片

通过编译,这个 bootsect.s 会被编译成二进制文件,存放在启动区的第一扇区。

图片

加载到内存 0x7c00 位置并跳转

当我们把操作系统代码编译好后存放在硬盘的启动区中,开机后,BIOS 程序就会将代码搬运到内存的 0x7c00 位置,而 CPU 也会从这个位置开始,一条一条指令不断地往后执行下去。

那你猜为什么要将操作系统代码搬运到 0x7c00 呢?

不为啥,一开始的开发者就这么规定的,而后面不论是谁开发的操作系统,只要是用 BIOS 这种启动方式,统统都要假定自己将会被搬运到内存 0x7c00 这个位置,否则运行就会出错。

那 BIOS 程序是咋写的,才能使其完成从硬盘加载到内存这个目标呢?其实,BIOS 程序除了这个工作之外,还有很多计算机自检程序,那些代码可不见得比操作系统代码简单,不过这就不是我们研究的重点了。况且,把硬盘中的数据加载到内存也不是啥难事。

之后看操作系统代码的时候就知道了,刚开始的时候会有非常多这样的操作,因为 BIOS 只帮我们把启动区的这 512 字节加载到内存,可是仍在硬盘其他扇区的操作系统代码就得我们自己来处理了,所以你很快就会看到这个过程。

好了,这回应该梳理清楚了吧?注意,这块的东西你无需想太细,因为这并不是我们了解操作系统所该关注的重点。

那我们的梦幻之旅,就从用汇编语言写成的 bootsect.s 这个文件的前两行代码开始讲起吧!因为它会被编译并存储在启动区,然后搬运到内存 0x7c00,之后也会成为 CPU 执行的第一个指令,代码如下:

mov ax,0x07c0
mov ds,ax

这两行代码是用汇编语言写的,含义是把 0x07c0 这个值复制到 ax 寄存器里,再将 ax 寄存器里的值复制到 ds 寄存器里。这一番折腾的结果就是让 ds 这个寄存器里的值变成了 0x07c0。

图片

ds 是一个 16 位的段寄存器,具体表示数据段寄存器,在内存寻址时充当段基址的作用。这是什么意思呢?

就是当我们之后用汇编语言写一个内存地址时,实际上仅仅是写了偏移地址,比如这行代码:

mov ax, [0x0001]

实际上相当于如下代码:

mov ax, [ds:0x0001]

ds 是默认加上的,表示在 ds 这个段基址处,往后再偏移 0x0001 单位,将这个位置的内存数据复制到 ax 寄存器中。

形象地比喻一下就是,你和朋友商量去哪玩比较好,你说天安门、南锣鼓巷、颐和园等,实际上都是偏移地址,省略了北京市这个基址。

当然,你完全可以每次都加上北京这个前缀,比如北京天安门、北京南锣鼓巷。如果你事先和朋友说好,我说的地方都是北京市里的,之后你就不用每次都带着北京市这个前缀了,是不是更方便?

那 ds 这个数据段寄存器的作用就是这样,方便了描述一个内存地址时,可以省略一个基址,没什么其他特别之处。

ds : 0x0001
北京市 : 南锣鼓巷

我们再看看,为什么这个 ds 寄存器的数值要赋值为 0x07c0?这里是有历史因素的,x86 为了让自己在 16 位的实模式下,能访问到 20 位的地址线,所以要把段基址先左移四位。 0x07c0 左移四位就是 0x7c00,这刚好就和这段代码被 BIOS 加载到的内存地址 0x7c00 一样了。

也就是说,在之后写的代码里,访问的数据的内存地址都先默认加上 0x7c00,然后再去内存中寻址。

为什么统一加上 0x7c00 这个数呢?这很好解释,BIOS 规定死了把操作系统代码加载到内存 0x7c00,那么里面的各种数据自然就全都被偏移了这么多,所以把数据段寄存器 ds 设置为这个值,方便了以后通过这种基址的方式访问内存里的数据。

图片

总结时刻

好,这一讲我们就到这里,虽然最核心的代码就两行,是不是已经破除了你对源码学习的恐惧呢?

简单总结一下,这一讲我们学习的代码主要完成了这两步操作:第一步,BIOS 将操作系统代码加载到内存 0x7c00;第二步,通过 mov 指令将默认的数据段寄存器 ds 的值改为 0x07c0,方便以后的基址寻址方式。

好,我是闪客。后面的世界越来越精彩,欲知后事如何,且听下回分解。