Linux0.11源码趣读

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

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

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

04|操作系统怎么把自己从硬盘搬运到内存?

讲述:闪客 大小:8.64MB 时长:00:09:01
00:00
1.0×

你好,我是闪客。

上一讲,咱们说过了为访问内存,操作系统做的最最基础的准备工作。这一讲,我们要讨论的问题是,操作系统是怎么把在硬盘中的自己搬运到内存中来的。

图片

如上图所示,此时操作系统用短短几行代码,将数据段寄存器 ds代码段寄存器 cs 设置为了 0x9000,方便之后程序访问代码和数据。并且,将栈顶地址 ss:sp 设置在了离代码的位置 0x9000 足够遥远的 0x9FF00,保证栈向下发展不会轻易覆盖掉已有的代码。

简单说,就是设置了如何访问数据的数据段,如何访问代码的代码段,以及如何访问栈的栈顶指针,也即初步做了一次内存规划。从 CPU 的角度看,访问内存就这么三块地方而已。

图片

做好这些基础工作后,接下来又到了新一翻的“折腾”。上一讲结尾说过,我们目前仅仅把硬盘中最开始的 512 字节加载到内存中了,但操作系统还有很多代码仍然在硬盘的其他扇区里,不能抛下他们不管呀。

把剩下的操作系统代码从硬盘请到内存

所以下一步,自然是把仍然在硬盘里的操作系统代码请到内存中来。

我们接着往下看代码:

load_setup:
    mov dx,#0x0000      ; drive 0, head 0
    mov cx,#0x0002      ; sector 2, track 0
    mov bx,#0x0200      ; address = 512, in 0x9000
    mov ax,#0x0200+4    ; service 2, nr of sectors
    int 0x13            ; read it
    jnc ok_load_setup       ; ok - continue
    mov dx,#0x0000
    mov ax,#0x0000      ; reset the diskette
    int 0x13
    jmp load_setup

ok_load_setup:
    ...

这里有两个 int 指令我们还没见过。

注意这个 int 是汇编指令,可不是高级语言的整型变量哟。int 0x13 表示发起 0x13 号中断,这条指令上面的各种 mov 指令,用来给 dx、cx、bx、ax 赋值,这四个寄存器都是作为这个中断程序的参数,这叫通过寄存器来传参。与之相对应的另一种传参方式是通过栈传递,在C语言中应用很广。

中断你可以简单理解为一种通知机制,用来打断当前 CPU 正在执行的指令流,转而去执行相应的中断处理程序的过程。更详细的原理和中断分类,你有兴趣的话可以课后自学。

好,我们回到课程主线上,发起中断后,CPU 就会通过这个中断号 0x13,去寻找对应的中断处理程序的入口地址,并跳转过去执行,逻辑上就相当于执行了一个函数

而 0x13 号中断的处理程序,是 BIOS 提前给我们写好的,具体就是读取磁盘的相关功能的函数。之后真正进入操作系统内核后,中断处理程序还需要我们自己重新写。后续章节里,你会不断看到各个模块注册自己的中断处理程序,所以不要急。此时为了方便,就先用 BIOS 提前写好的中断处理程序就行了。

可见即便是操作系统的源码,有时也需要去调用现成的函数方便自己工作,并不是造轮子的人就非得完全从头开始造。

回到正题,Linux 在此处用这个 0x13 号中断干了什么呢?让我们从代码里找到答案:

load_setup:
    mov dx,#0x0000      ; drive 0, head 0
    mov cx,#0x0002      ; sector 2, track 0
    mov bx,#0x0200      ; address = 512, in 0x9000
    mov ax,#0x0200+4    ; service 2, nr of sectors
    int 0x13            ; read it
    ...

本段代码的注释已经写的很明确了,直接说最终的作用吧——从硬盘的第 2 个扇区开始,把数据加载到内存 0x90200 处,共加载 4 个扇区。图示其实就是这样:
图片

图片里主要是为了清晰表达意思,比例可能不那么严谨,这个不必纠结。另外,其实这里加载的应该是软盘,不过软盘现在已经见不到了,我们当做硬盘来分析即可,它们的逻辑结构都是一样的。

再往后的 jnc 和 jmp,表示成功和失败分别跳转到哪个标签处,相当于我们高级语言的 if else。

load_setup:
    ...
    jnc ok_load_setup       ; ok - continue
    ...
    jmp load_setup

ok_load_setup:
    ...

为了保证主流程的顺畅,具体汇编指令的含义就不展开了,你可以自行查看相关手册。

可以看到,如果复制成功,就跳转到 ok_load_setup 这个标签;如果失败,则会不断重复执行这段代码,也就是重试。

因此我们先不需要关心重试逻辑了,直接看成功后跳转的 ok_load_setup 这个标签后的代码:

ok_load_setup:
    ...
    mov ax,#0x1000
    mov es,ax       ; segment of 0x10000
    call read_it
    ...
    jmpi 0,0x9020

这段代码中,我省略了很多非主逻辑的代码,比如在屏幕上输出 Loading system … 这个字符串。

剩下的核心代码就都写在这里了,就这么几行,其作用是把从硬盘第 6 个扇区开始往后的 240 个扇区,加载到内存 0x10000 处,和之前的从硬盘复制到内存是一个道理。

至此,整个操作系统的全部代码,就已经全部从硬盘加载到内存中了。然后这些代码,又通过一个熟悉的段间跳转指令 jmpi 0,0x9020,跳转到 0x90200 处,就是硬盘第二个扇区开始处的内容。

那这里的内容是什么呢?就是我们要阅读的第二个操作系统源代码文件 setup.s。

聊聊操作系统的编译过程

不过先不急,进入第二个源代码文件之前,我们先简单复盘一下整个操作系统的编译过程。

整个编译过程,就是通过 Makefilebuild.c 配合完成的,最终达到这样一个效果:

1.把 bootsect.s 编译成 bootsect 放在硬盘的 1 扇区;

2.把 setup.s 编译成 setup 放在硬盘的 2~5 扇区;

3.把剩下的全部代码(head.s 作为开头,与各种 .c 和其他 .s 等文件一起)编译并链接成 system,放在硬盘的随后 240 个扇区。

所以整个路径如下图所示:

图片

熟悉 gcc 编译过程的朋友应该知道,这里的 bootsect.s、setup.s、head.s 以及各种 .c 和其他 .s 文件都是源代码文件,写着人类看得懂的 C 语言和汇编语言的代码。而 bootsect、setup 以及 system 是最终编译成的二进制文件,里面是只有机器能看得懂的机器码。

这里的细节就不再展开,不了解这部分的同学可以去看 gcc 的编译过程。

所以 0x90200 处的代码,也就是我们即将跳转到的内存地址处的代码,就是从硬盘第二个扇区加载过来的。而第二个扇区的最开始处,也就是 setup 二进制文件的内容,是由 setup.s 源代码文件编译后形成的。

所以我们接下来跟随着 CPU 的脚步,从 setup.s 文件的第一行代码开始往后阅读就好,更多细节我们下讲再说。

挪来挪去的真讨厌

好了,到目前为止,你是不是觉得,这操作系统从硬盘到内存挪来挪去的真乱?而且前面编译放在硬盘的位置,和将硬盘数据加载到内存的位置,以及后面代码写死的跳转地址,它们竟然如此地强耦合?那万一整错了咋办。

比如加载到内存 a 处,然后却跳转到了 b 处;或者编译到了硬盘的第二扇区的代码,手一抖加载了硬盘第三扇区的数据到内存,这不全乱套了么?

没错,就是这样。在操作系统刚刚开始建立的时候,那是完全自己安排前前后后的关系,一个字节都不能偏,就是这么强耦合。

操作系统代码的编写者,需要处处小心翼翼,大脑时刻保持清醒,规划好自己写的代码被编译并存储在硬盘的哪个位置,接着又会被加载到内存的哪个位置,并且让 CPU 随后跳转到那个位置,不能错乱。

但这也是很有好处的,那就是在这个阶段,你完完全全知道每一步跳转,每一步数据访问都是怎么设计和规划的。

我们写高级语言的时候,完全不知道底层帮我们做了多少工作,又是怎么做的。虽然这让我们少了关心底层细节的烦恼,但当我们遇到问题或者想知道底层原理的时候,就显得很讨厌了。所以珍惜这个揭秘操作系统工作内幕的阶段吧!

总结时刻

好了,这一讲的内容就结束了。这也标志着我们走完了第一个操作系统源码文件 bootsect.s,开始向下一个文件 setup.s 进发了!

这一讲我们看到了操作系统如何把自己从硬盘加载到内存,顺便又研究了 Linux 0.11 整个编译和加载的简要过程。你可以对照后面这张图复习消化一下。现在,操作系统的代码已经完全从硬盘被搬到内存中了!

图片

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