Linux0.11源码趣读

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

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

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

05|重要代码放在零地址处

讲述:闪客 大小:6.75MB 时长:00:07:03
00:00
1.0×

你好,我是闪客。

上一讲我们说到,操作系统已经完成了各种从硬盘到内存的加载,以及内存到内存的复制(你可以通过后面这张图来回忆)。

在这一讲中,操作系统将会对内存布局做最后一次大调整,让我们一起看看它是怎么折腾的吧!

图片

处处都是 BIOS 的调包侠

好,我们向下一个文件 setup.s 进发!现在程序跳转到了 0x90200 这个位置开始执行,这个位置处的代码就位于 setup.s的开头,代码如下:

start:
    mov ax,#0x9000  ; this is done in bootsect already, but...
    mov ds,ax
    mov ah,#0x03    ; read cursor pos
    xor bh,bh
    int 0x10        ; save it in known place, con_init fetches
    mov [0],dx      ; it from 0x90000.

这里又有个 int 指令。

前面的课程如果你有好好看过的话,一下就能猜出它要干嘛。还记不记得之前有个 int 0x13 表示触发 BIOS 提供的读磁盘中断程序?

这个 int 0x10 也是一样的,它也是触发 BIOS 提供的中断服务,具体来说是调用显示服务相关的中断处理程序,而 ah 寄存器被赋值为 0x03 表示显示服务里具体的读取光标位置功能这一个子服务。

具体 BIOS 提供了哪些中断服务,如何去调用和获取返回值,你可以自行寻找资料,这里就不再展开了。

这个 int 0x10 中断程序执行完毕并返回时,将会在 dx 寄存器里存储好光标的位置,具体说来其高八位 dh 存储了行号,低八位 dl 存储了列号

图片

这里说明一下:计算机在加电自检后会自动初始化到文字模式,在这种模式下,一屏幕可以显示 25 行,每行 80 个字符,也就是 80 列。

那下一步 mov [0],dx 就是把这个光标位置存储在 [0] 这个内存地址处。注意,前面我们说过,这个内存地址仅仅是偏移地址,还需要加上 ds 这个寄存器里存储的段基址,最终的内存地址是在 0x90000 处,这里存放着光标的位置,以便之后在初始化控制台的时候用到。

所以从这里也可以看出,这和我们平时调用一个方法没什么区别,只不过这里的寄存器的用法相当于入参和返回值,这里的 0x10 中断号相当于方法名

这里又应了之前说的一句话,操作系统内核的最开始也处处都是 BIOS 的调包侠,有现成的就用呗。

再接下来的几行代码,都和刚才的逻辑一样:调用一个 BIOS 中断获取点信息,然后存储在内存中某个位置。因此,后面的代码我们迅速浏览一下就好咯:

比如获取内存信息。
; Get memory size (extended mem, kB)
    mov ah,#0x88
    int 0x15
    mov [2],ax
获取显卡显示模式。
; Get video-card data:
    mov ah,#0x0f
    int 0x10
    mov [4],bx      ; bh = display page
    mov [6],ax      ; al = video mode, ah = window width
检查显示方式并取参数
; check for EGA/VGA and some config parameters
    mov ah,#0x12
    mov bl,#0x10
    int 0x10
    mov [8],ax
    mov [10],bx
    mov [12],cx
获取第一块硬盘的信息。
; Get hd0 data
    mov ax,#0x0000
    mov ds,ax
    lds si,[4*0x41]
    mov ax,#INITSEG
    mov es,ax
    mov di,#0x0080
    mov cx,#0x10
    rep
    movsb
获取第二块硬盘的信息。
; Get hd1 data
    mov ax,#0x0000
    mov ds,ax
    lds si,[4*0x46]
    mov ax,#INITSEG
    mov es,ax
    mov di,#0x0090
    mov cx,#0x10
    rep
    movsb

以上原理都是一样的。我们就没必要细琢磨了,对操作系统的理解作用不大,只需要知道最终存储在内存中的信息是什么,在什么位置就好了,之后会用到他们的。

图片

又是 rep 指令复制内存

由于之后很快就会用上C语言来编程,虽然汇编和C也可以用变量的形式传递数据,但这需要编译器在链接时做一些额外的工作。

这么多数据,有没有更方便的处理方式呢?方法就是双方共同约定一个内存地址,我往这里存,你从这里取,就完事了。这恐怕是最最原始和直观的变量传递的方式了。

把这些信息存储好之后,操作系统又要做什么呢?我们继续往下看。

cli         ; no interrupts allowed ;

就一行 cli,表示关闭中断的意思。

因为后面我们要覆盖掉原本BIOS写好的中断向量表,也就是破坏掉原有的表,写上我们自己的中断向量表,所以此时是不允许中断进来的。

继续往后看:

; first we move the system to it's rightful place
    mov ax,#0x0000
    cld         ; 'direction'=0, movs moves forward
do_move:
    mov es,ax       ; destination segment
    add ax,#0x1000
    cmp ax,#0x9000
    jz  end_move
    mov ds,ax       ; source segment
    sub di,di
    sub si,si
    mov cx,#0x8000
    rep movsw
    jmp do_move
; then we load the segment descriptors
end_move:
    ...

看到后面那个 rep movsw 熟不熟悉,一开始我们把操作系统代码从 0x7c00 移动到 0x90000 的时候就是用的这个指令,看图回忆一下。

图片

同前面的原理一样,也是做了个内存复制操作,最终的结果是,把内存地址 0x10000 处开始往后一直到 0x90000 的内容,统统复制到内存的最开始的 0 位置,大概就是这么个效果。

图片

洗牌后的内存布局

由于之前的各种加载和复制,导致内存看起来很乱,是时候进行一波取舍和整理了,让我们重新梳理一下此时的内存布局。

栈顶地址仍然是 0x9FF00 没有改变。

0x90000 开始往上的位置,原来是 bootsectsetup 程序的代码,而此时 bootsect 的代码现在已经被一些临时存放的数据,如内存、硬盘、显卡等信息,覆盖了一部分。

内存最开始的 00x80000 这 512K 被 system 模块给占用了,之前讲过,这个 system 模块就是除了 bootsect 和 setup 之外的全部程序(head.s 作为开头,main.c 和其他文件紧随其后)链接在一起的结果,可以理解为操作系统的全部代码

那么现在的内存布局就是这个样子:

图片

system 被放在了内存地址零位置处,之前的 bootsect 和现在所在的 setup,正逐步被其他数据所覆盖掉。

由此也可以看出,system 才是真正被视为重要的操作系统代码,其他的都是作为前期的铺垫,用完就被无情抛弃了。而 system 真正的大头要在第二部分才会展开讲解,所以为什么我把第一部分称为进入内核前的苦力活,这下知道了吧?

好了,记住上面的图就好了,这回是不是又重新清晰起来了?之前的什么 0x7c00,已经是过去式了,赶紧忘掉它,向前看!

接下来,就要进行有点技术含量的工作了,那就是模式的转换,需要从现在的 16 位的实模式转变为之后 32 位的保护模式,这是一项大工程!也是我认为的这趟操作系统源码旅程中,第一个颇为精彩的地方,请你做好准备!

总结时刻

这一讲,我们看到操作系统将 system 模块的代码被移动到了内存的零地址处,同时在 0x90000 的位置处存放了一些临时信息,如内存、硬盘、显卡等设备信息,供后面的程序使用。

此时,经过了一次又一次的折腾,内存布局总算被初步定下来了。

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