Chinaunix首页 | 论坛 | 博客
  • 博客访问: 22333
  • 博文数量: 15
  • 博客积分: 29
  • 博客等级: 民兵
  • 技术积分: 95
  • 用 户 组: 普通用户
  • 注册时间: 2012-03-08 16:15
文章分类
文章存档

2012年(15)

我的朋友

分类:

2012-04-28 10:49:21

    下面我们来说一下从开机到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中设计了如下代码:

点击(此处)折叠或打开

  1. SETUPLEN = 4                    !nr of setup sectors
  2. BOOTSEG = 0x07c0                !original address of boot sector
  3. INITSEG = 0x9000                !we move boot here-out of the way
  4. SETUPSEG = 0x9020               !setup starts here
  5. SYSSEG = 0x1000                 !system loaded at 0x10000
  6. 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模块了。代码如下:

点击(此处)折叠或打开

  1. mov ax, #BOOTSEG                 !source
  2. mov ds, ax
  3. mov ax, #INITSEG                 !destination
  4. mov es, ax
  5. mov cx, #256                     !num of word that need move
  6. sub si, si                       !si be zero
  7. sub di, di                       !di be zero
  8. rep                              !mov word till 256
  9. movw
    将bootsect移动后,其实就有两个bootsect模块在内存中,一个是刚刚移动到的新位置(0x9000),另一个是之前代码的位置(0x07c0)。此刻,代码段寄存器CS仍指向0x07c0,而我们需要从0x9000这个位置开始继续往下执行,0x07c0处的代码在后来会被覆盖掉的。

注:
之前将bootsect模块加载在位置0x07c0处,是因为“约定”和“定位识别”的需要,而现在将模块移动到0x9000处,说明操作系统开始根据自己的需要开始安排内存了。

    代码需要开始从0x9000处开始执行,那么具体是怎么实现的呢?这是一段写的非常巧妙的代码:

点击(此处)折叠或打开

  1. rep
  2. movw
  3. jmpi go, INITSEG
  4. go:mov ax, cs
    这里用了jmpi段间跳转命令,跳转到标号为go,地址为INITSEG的地方。那么程序从go开始往下执行。这两句巧妙地实现了“到新位置后接着原来的执行顺序继续执行下去”的目的。
    由于位置的改变,接下来,就是修改寄存器的值。

点击(此处)折叠或打开

  1. go:mov ax, cs
  2. mov ds, ax                !ds = 0x9000
  3. mov es, ax                !es = 0x9000

  4. !put stack at 0x9ff00
  5. mov ss, ax
  6. mov sp, #0xff00           !arbitrary value >> 512

  7. !load the setup-setctors directly after the bootblock.
  8. !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中断向量时,就要事先将指定的扇区和加载的内存位置等信息传递给服务程序,即传参。

点击(此处)折叠或打开

  1. load_setup:
  2. mov dx, #0x0000                  !drive 0, head0
  3. mov cx, #0x0002                  !sector 2, track 0
  4. mov bx, #0x0200                  !address = 512, in INITSEG
  5. mov ax, #0x0200+SETUPLEN         !service 2, nr of sectors
  6. int 0x13                         !read it
  7. jnc ok_load_setup                !ok-continue
  8. mov dx, #0x0000
  9. mov ax, #0x0000                  !reset the diskette
  10. int 0x13
  11. j load_setup
  12. 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内核设计的艺术》,图片是从网上找的。
阅读(702) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~