【Linux 0.11】第六章 引导启动程序

499 阅读11分钟

赵炯;《Linux 内核完全注释 0.11 修正版 V3.0》

本章代码主要是 boot/ 下的三个汇编代码文件 —— bootsect.s、setup.s、head.s

1. 总体功能

Linux 内核 + 文件系统 = 可用的操作系统

Linux 操作系统启动的主要执行流程:

  • Power ON,80x86 CPU 进入实模式,从 0xFFFF0 处(ROM-BIOS)开始执行程序代码;
  • 系统检测,在物理地址 0 处初始化中断向量;
  • 将引导扇区读入 0x7C00,并在这里执行代码,加载 setup.s 和 system 模块(为何不一步到位加载到起始位置?setup.s 程序会使用到 BIOS 设置的中断来获取及其参数),给出根文件系统位置,在系统加载期间将显示信息 "Loading...",将控制权交给 setup.s。(bootsect.s)

image.png

  • 识别主机的特性、VGA 卡的类型、将 system 模块移动到内存起始,开启保护模式,跳转到 head.s(system 模块头部)(setup.s)
  • IDT、GDT、LDT 被加载,处理器和协处理器确认,分页工作设置完毕。(head.s)
  • 调用 init/main.c 中的 main() 程序。

image.png

2. bootsect.s 程序

该程序是磁盘引导块程序,驻留在磁盘的第一个扇区中。

  1. 在上电后,BIOS 会将其加载到内存的 0x7C00;
  2. 将自己移动到内存绝对地址 0x90000 开始处;
  3. 加载 setup 模块至 0x90200;
  4. 读取硬盘参数表中内容;
  5. 显示 Loading system....;
  6. 加载 system 模块至 0x10000;
  7. 确定根文件系统的设备号;
  8. 跳转到 setup 程序;

image.png

system 模块后面的区域可用来存放一个基本的根文件系统。

程序中涉及的硬盘设备命名方式如下(设备号=主设备号*256+次设备号),其它设备的主设备号分别为:1-内存、2-磁盘、3-硬盘、4-ttyx、5-tty、6-并行口、7-非命名管道。

image.png

从硬盘启动系统

如果需要从硬盘启动系统,通常需要使用其他多操作系统引导程序(Shoelace、LILO、Grub)来引导系统加载,bootsect.s 的任务会由这些程序来完成。

从硬盘启动的基本流程:系统上电后,可启动硬盘第 1 个扇区(主引导记录 MBR-Master Boot Record)会被加载到内存 0x7C00 处并开始执行,该程序会首先把自己向下移动到 0x600 处,然后根据 MBR 中分区表信息所指明活动分区中的第 1 个扇区加载到内存 0x7C00 处,然后开始执行之。

3. setup.s

  • 利用 ROM BIOS 中断读取机器系统数据,并将这些数据保存到 0x90000 开始的位置,所取得的参数和保留的内存位置如下:

image.png

  • setup 程序将 system 模块从 0x10000-0x8ffff,整块向下移动到内存绝对地址 0x00000 处;
  • 加载中断描述符表寄存器 idtr 和 gdtr;
  • 开启 A20;
  • 设置两个中断控制芯片 8259A;
  • 进入 32 位保护模式;
  • 跳转到位于 system 模块最前面部分的 head.s 程序;

本程序临时设置了中断描述符表和全局描述符表,并在 GDT 中设置了当前内核代码段的描述符和数据段描述符。

image.png

段描述符存放在描述符表中。描述符表有两类:全局描述符表和局部描述符表。处理器通过 GDTR 和 LDTR 寄存器来定位 GDT 表和当前的 LDT 表。这两个寄存器以线性地址的方式保存了描述符表的基地址和表的长度。指令 lgdt 和 sgdt 用于访问 GDTR 寄存器;指令 lldt 和 sldt 用于访问 LDTR 寄存器。lgdt 使用内存中一个 6 字节操作数来加载 GDTR 寄存器,头两个字节代表描述符表的长度,后 4 个字节是描述符表的基地址,访问 LDTR 寄存器的指令 lldt 所使用的操作数却是一个 2 字节的操作数,表示全局描述符表 GDT 中一个描述符项的选择符。

image.png

索引值用于选择指定描述符表中 8192 个描述符中的一个,处理器将该索引值乘上 8,并加上描述符表的基地址即可访问表中指定的段描述符。表指示器 TI 用于指定选择符所引用的描述符表。值为 0 表示指定 GDT 表,值 1 表示当前的 LDT 表。在进入保护模式之前,我们必须首先设置好将要用到的段描述符表,然后使用指令 lgdt 把描述符表的基地址告知 CPU(存入 gdtr 寄存器),再将机器状态字的保护模式标志置位即可进入 32 位保护运行模式。

3.3 其它信息

3.3.1 当前内存映像

在 setup.s 程序执行结束后,系统模块 system 被移动到物理地址 0x0000 开始处,而从位置 0x90000 开始处则存放了内核将会使用的一些系统基本参数,如图:

在这里插入图片描述

3.3.2 BIOS 视频中断 0x10

在这里插入图片描述

3.3.3 硬盘基本参数表

中断向量表中,int 0x41 的中断向量位置存放的并不是中断程序的地址,而是第一个硬盘的基本参数表。对于 100% 兼容的 BIOS 来说,这里存放着硬盘参数表阵列的首地址 F000H:E401H,第二个硬盘的基本参数表入口地址存于 int 0x46 中断向量位置处。

在这里插入图片描述

3.3.4 A20 地址线问题

为了兼容 1MB 内存。

3.3.5 8259A 中断控制器的编程方法

1. 8259A 芯片工作原理

级联的两片 8259A 可编程控制器(PIC)芯片,共可管理 15 级中断向量。其中从芯片的 INT 引脚连接到主芯片的 IR2 引脚上。主芯片的端口基地址是 0x20,从芯片是 0xA0。

在这里插入图片描述

  • 中断请求寄存器 IRR 用来保存中断请求输入引脚上所有请求服务中断级,寄存器的 8 个比特位 D7-D0 分别对应引脚的 IR7-IR0;
  • 中断屏蔽器 IMR 用于保存被屏蔽的中断请求项对应的比特位,寄存器的 8 位也是对应 8 个中断级,哪个比特位被置 1 就屏蔽哪一级中断请求。对高优先级输入项的屏蔽并不会影响低优先级中断请求项的输入;
  • 优先级解释器 PR 用于确定 IRR 中所设置比特位的优先级,选通最高优先级的中断请求到正在服务寄存器 ISR 中;
  • ISR 中保存着正在接受服务的中断请求。控制逻辑方框中的寄存器用于接受 CPU 产生的两类命令。

在 8259A 可以正常操作之前,必须首先设置初始化命令 ICW 寄存器组的内容,而在其工作过程中,则可以使用写入操作命令字 OCW 寄存器组来随时设置和管理 8259A 的工作方式。A0 线用于选择操作的寄存器。当 A0=0 时,芯片端口地址是 0x20 和 0xA0。当 A0=1 时,端口就是 0x21 和 0xA1。

来自各个设备的中断请求线分别连接到 8259A 的 IR0-IR7 中断请求引脚上。当这些引脚上有一个或多个中断请求信号到来时,中断请求寄存器 IRR 中响应的比特位会被置位锁存。此时若中断屏蔽寄存器 IMR 中对应位被置位,则相应的中断请求就不会送到优先级解析器中。对于未屏蔽的中断请求被送到优先级解析器之后,优先级最高的中断请求会被选出。此时 8259A 就会向 CPU 一个 INT 信号,而 CPU 则会在执行完当前的一条指令之后向 8259A 发送一个 INTA 来响应中断信号。8259A 在收到这个响应信号之后就会把所选出的最高优先级中断请求保存到正在服务寄存器 ISR 中,即 ISR 中对应中断请求级的比特位被置位。与此同时,中断请求寄存器 IRR 中的对应比特位被复位,表示该中断请求开始正被处理中。

此后,CPU 会向 8259A 发出第 2 个 INTA 脉冲信号,该信号用于通知 8259A 送出中断号。因此在该脉冲信号期间 8259A 就会把一个代表中断号的 8 位数据发送到数据总线上供 CPU 读取。

到此为止,CPU 中断周期结束。如果 8259A 使用的是自动结束中断 AEOI 方式,那么在第 2 个 INTA 脉冲信号的结尾处正在服务寄存器 ISR 中的当前服务中断比特位就会被复位。否则的话,若 8259A 处于非自动结束方式,那么在中断服务程序结束时程序就需要向 8259A 发送一个结束中断(EOI)命令以复位 ISR 中的比特位。如果中断请求来自接联的第 2 个 8259A 芯片,那么就需要向两个芯片都发送 EOI 命令。此后,8259A 就会去判断下一个最高优先级的中断,并重复上述处理过程。

...

4. head.s 程序

4.1 功能描述

head.s 程序在被编译生成目标文件后会与内核其他程序一起被链接成 system 模块,位于 system 模块的最前面开始部分,这也就是为什么称其为头部程序的原因。system 模块将被放置在磁盘上 setup 模块之后开始的扇区中,即从磁盘上第 6 个扇区开始放置。一般情况下, Linux0.11 内核的 system 模块大约有 120KB 左右,因此在磁盘上大约占 240 个扇区。

从这里开始,内核完全都是在保护模式下运行。head.s 汇编程序与前面的语法格式不同,它采用的是 AT&T 的汇编语言格式,并且需要使用 GNU 的 gas 和 gld 进行编译连接。

这段程序实际上处于内存绝对地址 0 处开始的地方。这个程序的功能比较单一。首先是加载各个数据段寄存器,重新设置中断描述符表 idt,共 256 项,并使各个表项均指向一个只报错误的哑中断子程序 ignore_int。中断描述符表中每个描述符项也占 8 字节,如图:

在这里插入图片描述

其中,P 是段存在标志;DPL 是描述符的优先级。在 head.s 程序中,中断门描述符中段选择符设置为 0x0008,表示该中断处理子程序在内核代码中。偏移值被设置为 ignore_int 中断处理子程序在 head.s 程序中的偏移值。由于 head.s 程序被移动到从内存地址 0 开始处,因此该偏移值也就是中断处理子程序在内核代码段中的偏移值。由于内核代码段一直存在于内存中,并且特权级为 0,即 P=1,DPL=00。因此中断门描述符的字节 5 和 字节 4 的值应该是 0x8E00。

在设置好中断描述符表后,本程序又重新设置了全局段描述符 GDT,处理修改了段限长之外,与 setup.s 中的内容完全一样,主要目的是为了把 GDT 表放在内存内核代码比较合理的地方。

检测 A20 地址线是否开启、是否有数学协处理器芯片、设置分页机制、利用返回指令将预先放置在堆栈中的 /init/main.c 程序的入口地址弹出,去运行 main() 程序。

CPU 会首先检测段保护,然后再检测页保护。

4.2 其它信息

4.2.1 程序执行结束后的内存映像

在这里插入图片描述

4.2.2 Intel 32 位保护运行机制

32 位保护模式下的内存寻址需要拐个弯,经过描述符表中的描述符和内存页管理来确定。针对不同的使用方面,描述符表分为三种:全局描述符表、中断描述符表和局部描述符表。当 CPU 运行在保护模式下,某一时刻 GDT 和 IDT 分别只能有一个,分别由寄存器 GDTR 和 IDTR 指定它们的表基址。局部表可以有 0 个或最多 8191 个,这由 GDT 表中未用项数和所设计的具体体系来确定。在某一时刻,当前 LDT 表的基地址由 LDTR 寄存器的内容指定,并且 LDTR 的内容使用 GDT 中某个描述符来加载,即 LDT 也是由 GDT 中的描述符来指定。但是在某一个时刻同样也只有其中的一个被认为是活动的。一般对于每个任务使用一个 LDT。在运行时,程序可以使用 GDT 中的描述符以及当前任务的 LDT 中的描述符。对于 Linux0.11 内核来说同时可以有 64 个任务在执行,因此 GDT 表中最多有 64 个 LDT 表的描述符项存在。

中断描述符表 IDT 的结构与 GDT 类似,在 Linux 内核中它正好位于 GDT 表的前面。

在这里插入图片描述