下面我们来说一下从开机到main()的执行过程中的第二步--加载操作系统内核程序并为保护模式做准备。
在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移动后,其实就有两个bootsect模块在内存中,一个是刚刚移动到的新位置(0x9000),另一个是之前代码的位置(0x07c0)。此刻,代码段寄存器CS仍指向0x07c0,而我们需要从0x9000这个位置开始继续往下执行,0x07c0处的代码在后来会被覆盖掉的。
注:
之前将bootsect模块加载在位置0x07c0处,是因为“约定”和“定位识别”的需要,而现在将模块移动到0x9000处,说明操作系统开始根据自己的需要开始安排内存了。
代码需要开始从0x9000处开始执行,那么具体是怎么实现的呢?这是一段写的非常巧妙的代码:
- rep
- movw
- jmpi go, INITSEG
- go:mov ax, cs
这里用了jmpi段间跳转命令,跳转到标号为go,地址为INITSEG的地方。那么程序从go开始往下执行。这两句巧妙地实现了“
到新位置后接着原来的执行顺序继续执行下去”的目的。
由于位置的改变,接下来,就是修改寄存器的值。
- 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.
SS与SP是对栈寄存器的设置,说明从此以后程序可以执行更为复杂一些的数据运算类指令了。
注:
栈操作的方向:高地址到低地址。
做完了内存规划,终于可以加载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的中断服务程序则不然,它可以根据设计者的意图,把指定扇区的代码加载到内存的指定位置。
这样的话,使用 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:
传参完毕后,产生 int 0x13中断,执行中断服务程序,将软盘从第二扇区开始的4个扇区,即setup对应的程序加载至内存0x9020处。
注:
前面提到的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内核设计的艺术》,图片是从网上找的。
阅读(539) | 评论(0) | 转发(0) |