从开机加电到执行main函数之前的过程

    我们平时将计算机打开,要经过几十秒,才能进入到我们熟悉的图形(命令行)界面。那么,在这几十秒之内,计算机在干什么呢?计算机做了那些事情呢?下面让我们一探究竟。 

    从开机到main()的执行分三步完成,其目的是 实现从启动盘加载操作系统程序,完成执行main()所需要的准备工作
  1. 启动BIOS,准备实模式下的中断向量表和中断服务程序;
  2. 从启动盘加载操作系统到内存,加载操作系统程序的工作就是利用第一步中准备的中断服务程序实现的;
  3. 为执行32位的main()做过渡工作。

注:实模式(Real Mode)是Intel 80286和之后的80x86兼容的CPU的操作模式。实模式的特性是一个20位的存储器地址空间(即1MB的存储器可被寻址),可以直接通过软件的方式访问BIOS以及周边硬件,没有硬件支持的分页机制和实时多任务的概念。

    下面,我们就来说说第一步--启动BIOS,准备实模式下的中断向量表和中断服务程序。

    我们都知道计算机加电的一瞬间,内存中什么程序都没有。软盘(硬盘)里虽然有操作系统程序,但CPU的逻辑电路被设计为只能运行内存中的程序,它没有能力直接从软盘(硬盘)运行操作系统。也就是说,我们现在的任务是将软盘(硬盘)中的操作系统程序加载到内存中,以使其可以执行。

    问题是内存中什么也没有,加载操作系统的过程由谁来完成呢?答案是BIOS

    我们先来说说BIOS自身是怎样启动的。前面说过,内存中什么都没有,所以软件启动BIOS的方法是不行的,那就只能靠硬件方法来完成了。

    为了兼容8086,以及解决最开始的启动问题,Intel做了两件事来完成这项工作。

  • Intel将所有80x86系列的CPU的硬件都设计为加电即进入16位实模式状态运行。等启动结束后,会切换到保护模式下。
  • 将CPU硬件逻辑设计为加电瞬间强行将CS的值置为0xFFFF,IP的值置为0x0000,这样CS:IP就指向了0xFFFF0这个地址范围,即BIOS的地址范围。

                                                     启动BIOS在内存中的状态及初始执行位置

    这是一个纯硬件完成的动作
。如果这里有可执行代码,那么计算机将从这里的代码开始,沿着后续的程序一直执行下去;否则,就此死机。
    BIOS程序的入口地址恰恰就是0xFFFF0。即BIOS程序的第一条指令就设计在这个位置上。

注:BIOS(Basic Input Output System),直译过来后中文名称就是"基本输入输出系统"。其实,它是一组 固化到计算机内主板上一个 ROM芯片上的程序,它保存着计算机最重要的基本输入输出的程序、系统设置信息、开机后自检程序和系统自启动程序。 其主要功能是为计算机提供 最底层的、最直接的 硬件设置和控制。

    从上图可以看到,我们选用的BIOS所占的地址段为0xFE000~0xFFFFF,大小为8KB。CPU可以对ROM直接寻址, BIOS程序的地址0xFE000~0xFFFFF是ROM的硬件地址。现在CS:IP已经指向0xFFFF0这个位置,BIOS就此可以启动了。BIOS会执行开机后的自检程序,这期间,有一项对启动操作系统直观重要的工作,那就是在内存中建立中断向量表和中断服务程序。
   
   
                                  BIOS在内存中加载中断向量表和中断服务程序

    从图中可以看到,BIOS程序在内存最开始的位置(物理地址0x00000),占用1KB的内存空间(0x00000~0x003FF)构建中断向量表,并在紧挨着它的位置用256字节的内存空间构建BIOS数据区(0x00400~0x004FF),在大约56KB以后的位置(0x0E2CE)加载了8KB左右的与中断向量表相应的若干中断服务程序。


    下面我们来说一下从开机到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程序所在的部分区域。由于这些数据是要留用的的,因此在它们失去使用价值之前,一定不能被覆盖掉。

    到此为止,操作系统内核程序的加载工作已经完成。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值