在Linux-0.11源码中,有一个文件夹boot,其中存放了三个汇编文件,分别是bootsect.s, setup.s, head.s。我们就从这三个文件入手,来讲解加载操作系统内核程序。
(一)加载第一部分代码--引导程序(bootsect)
前面BIOS已经执行了一系列代码,计算机完成了自检等操作。计算机硬件体系结构的设计与BIOS联手操作,会让CPU接收到一个 int 0x19 中断,CPU接收到这个中断后,会立即在中断向量表中找到 int 0x19这个中断向量。此时,CPU指向 int 0x19这个中断向量所对应的中断服务程序的入口地址,这个中断服务程序的作用就是把软盘(硬盘)的第一个扇区中的程序(bootsect)加载到内存中的指定位置(注意这里只是加载第一个扇区的程序)。
注:
1. 中断向量表(Interrupt Vector Table):实模式中断机制的重要组成部分,表中记录所有中断号对应的中断服务程序的入口地址。
2. 中断服务程序(Interrupt Services):通过中断向量表的索引对中断进行响应服务,是一些具有特定功能的程序。
这个中断服务程序(即启动加载服务程序)将软驱0号磁头对应的盘面的0磁道1扇区的内容拷贝至内存0x07C00处。这个扇区里的内容就是Linux-0.11操作系统的引导程序,即启动扇区。这是非常关键的一步,从此计算机与软盘(硬盘)上的操作系统产生关联。至此,已经将bootsect.s的内容装入内存,现在的任务就是继续装入后续的代码setup.s和head.s。
注:
BIOS程序固化在主机板上的ROM中,是根据具体的主机板而不是根据具体的操作系统设计的。由于计算机可以安装不同的操作系统,为了能让操作系统和BIOS协调工作,必须建立统一的协调机制。现行的方案是“两头约定”和“定位识别”。操作系统的设计者“约定”必须把最开始执行的程序“定位”在启动扇区(即软盘中的0盘面0磁道1扇区),其余的程序可以依照操作系统的设计顺序加载在后续的扇区中。“定位识别”只从启动扇区把代码加载到内存的0x07c00这个位置,而不管启动扇区的内容是什么。
(二)加载第二部分代码--setup
现在就来将setup.s和head.s加载至内存。
我们平时编写程序时,不用去管程序的代码和数据放在内存的什么位置,因为操作系统和编译器会替我们完成。而现在,没有操作系统,没有编译器,只能靠操作系统的设计者来规划内存了。
所以,我们必须先弄清楚操作系统设计者是如何规划内存的。在实模式下,寻址范围是1MB,bootsect中设计了如下代码:
点击(此处)折叠或打开
- SETUPLEN = 4 !nr of setup sectors
- BOOTSEG = 0x07c0 !original address of boot sector
- INITSEG = 0x9000 !we move boot here-out of the way
- SETUPSEG = 0x9020 !setup starts here
- SYSSEG = 0x1000 !system loaded at 0x10000
- ENDSEG = SYSSEG + SYSSIZE !where to stop loading
接着,我们将bootsect启动代码(共512B)从内存位置0x07c0(BOOTSEG)复制到内存0x9000(INITSEG)处。这时,你可能和我有一样的疑问,为什么要移动代码呢???
因为当时system的模块长度不会超过0x80000字节大小(即512KB),所以bootsect程序把system模块读入物理地址0x10000开始处,这样也不会覆盖在0x90000处开始的bootsect和setup模块。后面setup程序将会把system模块移动到物理内存起始位置处(0x0000),这样system模块中代码的地址也即等于实际的物理地址,便于对内核代码和数据进行操作。
可能你又和我有一样问题了,既然都要移动到物理内存起始位置处,为什么不直接移动呢,而是要先移动到0x10000处,再移动到0x0000位置呢?
这是因为在随后执行的setup代码开始部分还需要利用ROM BIOS中的中断调用来获取机器的一些参数。当BIOS初始化时会在物理内存开始处放置一个大小为0x400字节(1KB)的中断向量表,因此需要在使用完BIOS的中断调用后才能将这个区域覆盖掉。
说了这么多,终于可以安心地移动bootsect模块了。代码如下:
点击(此处)折叠或打开
- mov ax, #BOOTSEG !source
- mov ds, ax
- mov ax, #INITSEG !destination
- mov es, ax
- mov cx, #256 !num of word that need move
- sub si, si !si be zero
- sub di, di !di be zero
- rep !mov word till 256
- movw
注:
之前将bootsect模块加载在位置0x07c0处,是因为“约定”和“定位识别”的需要,而现在将模块移动到0x9000处,说明操作系统开始根据自己的需要开始安排内存了。
代码需要开始从0x9000处开始执行,那么具体是怎么实现的呢?这是一段写的非常巧妙的代码:
点击(此处)折叠或打开
- rep
- movw
- jmpi go, INITSEG
- go:mov ax, cs
由于位置的改变,接下来,就是修改寄存器的值。
点击(此处)折叠或打开
- go:mov ax, cs
- mov ds, ax !ds = 0x9000
- mov es, ax !es = 0x9000
- !put stack at 0x9ff00
- mov ss, ax
- mov sp, #0xff00 !arbitrary value >> 512
- !load the setup-setctors directly after the bootblock.
- !Note that 'es' is already set up.
注:
栈操作的方向:高地址到低地址。
做完了内存规划,终于可以加载setup模块了。加载setup模块需要借助BIOS提供的 int 0x13中断向量所指向的中断服务程序来完成。(这也就是为什么之前移动模块bootsect时没有直接移动到0x0000处,而是移动到了0x1000处,此刻用到了BIOS中断向量表,而这个向量表就是存放在0x0000开始位置处的)。
int 0x13中断向量与 int 0x19不同:
- int 0x19中断向量所指向的启动加载服务程序是BIOS执行的,int 0x13的中断服务程序是LINUX操作系统自身的启动代码bootsect执行的。
- int 0x19的中断服务程序只负责把软盘的第一扇区的代码加载到0x07c00位置,int 0x13的中断服务程序则不然,它可以根据设计者的意图,把指定扇区的代码加载到内存的指定位置。
点击(此处)折叠或打开
- load_setup:
- mov dx, #0x0000 !drive 0, head0
- mov cx, #0x0002 !sector 2, track 0
- mov bx, #0x0200 !address = 512, in INITSEG
- mov ax, #0x0200+SETUPLEN !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
- j load_setup
- ok_load_setup:
注:
前面提到的SS:SP所指向的位置为0x9FF00,这与setup程序的实际位置还有很大的距离,即使setup加载进来后,系统仍有足够的内存空间用来执行数据压栈操作。由于在启动部分,要压栈的数据有限,所以不存在越界问题,我们不需要担心,这些都是操作系统设计者进行过精密测算的。
(三)加载第二部分代码--system
加载第三部分模块与加载setup模块一样,使用的都是 int 0x13中断向量,加载的过程大抵都一样,只是这次加载的扇区为240,是之前setup的60倍。由于加载时间会加长,所以需要对软盘(硬盘)进行更多的监控。Ok,我们第三部分的代码也已经完全加载入内存中。
虽然内核模块已经加载至内存中,但是这不能让LINUX系统运行起来。作为完整可运行的LINUX系统,还需要一个基本的文件系统支持,即根文件系统。LINUX 0.11内核仅支持MINIX的1.0文件系统。根文件系统通常是在另一个软盘上或者在另一个硬盘分区中。为了知道内核所需要的根文件系统在什么地方,bootsect代码给出了根文件系统所在的默认块设备号。所以,我们必须确认一下根设备号。
现在,bootsect程序的任务都已经完成!
下面通过执行“jmpi 0, SETUPSEG”这条语句跳转至0x90200处,即setup程序的位置处开始执行。它做的第一件事就是利用BIOS提供的中断服务程序从设备上提取内核运行所需的机器系统数据,并分别从向量0x41 和 0x46向量指的内存地址处获取硬盘参数表1和硬盘参数表2,并把他们存放在0x9000:0x0080和0x9000:0x0090处,这些数据被加载到内存0x90000~0x901FC位置处,在以后main函数执行时发挥重要作用。
注:
BIOS提取的机器系统数据将覆盖掉bootsect程序所在的部分区域。由于这些数据是要留用的的,因此在它们失去使用价值之前,一定不能被覆盖掉。
到此为止,操作系统内核程序的加载工作已经完成。
本文参考自《Linux内核设计的艺术》,图片是从网上找的。