《LINUX内核设计的艺术》读书笔记(一)

本系列文章主要针对《Linux内核程序设计的艺术》一书进行归纳总结,包括一些个人理解。

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

一、启动BIOS,准备16位实模式下的中断向量表和中断服务程序

计算机加电的时候,操作系统程序并没有加载到计算机内存中,因此必须先把操作系统从磁盘加载到内存中。在使用操作系统时,打开文件、加载到内存、加载到什么位置这些动作通常是通过操作系统执行,程序员只需调用响应的API即可。但是开机的时候操作系统不在内存中,所以必须有可以控制加载操作系统的一套“东西”才行,那套东西就是BIOS。

1.1 加电后执行BIOS程序

BIOS是一套固化在主板ROM芯片的程序。不同的主板有不同的BIOS程序,不过基本大同小异。我们选用的BIOS程序大小为8K,处于0xFE000~0xFFFFF的地址段上。 BIOS对操作系统的加载可以说是一个基本是纯硬件的过程。BIOS加电后把CS寄存器赋值为0xFFFF,IP寄存器赋值为0x0000,因此CS:IP指向0xFFFF0,也就是指向BIOS地址段中。如果此地址没有任何可执行的程序,那么计算机会就此死机,永远停留在这这个位置。

1.2 建立中断向量表和中断服务程序

BIOS启动后会做检查内存、显卡等操作。之后BIOS会在内存0x00000的位置建立大小为1K的中断向量表,在紧挨着的0x00400~0x004FF大小为256个字节的位置建立BIOS的数据段,在0x0E2CE的位置建立大小为56K的中断服务程序,中断服务程序与前面建立的中断向量表相对应。

注:在16位实模式下,中断向量表的一个表项占4个字节,前两字节作为CS寄存器的值,后两个字节作为IP寄存器的值,CS与IP合起来组成CS:IP指向对应地址处的中断服务程序。本文中建立了1K大小的中断向量表,因此最多可以有256个中断向量表项。


二、加载bootsect、setup、system模块

2.1 加载bootsect

之后BIOS程序执行int0x19中断,该中断执行一个固定的程序:把0号磁头对应磁盘0号盘面0号磁道1号扇区,把第一扇区共512个字节的内容复制到内存0x07C00的位置。第一扇区被称为启动扇区(bootsector)。至此把操作系统的第一部分加载到内存中,计算机可以执行操作系统写好的指令。

2.2 加载setup

2.2.1 复制bootsect
bootsect程序自身将自己复制到0x90000的位置,然后通过段间跳转指令跳转到复制后的位置继续执行。0x07C00这个位置属于固定的加载位置,所有操作系统都要先加在这个位置。而linux为了自身的需要,会把程序复制到linux所希望的地方中。至此,操作系统不再需要依赖BIOS,可以按照操作系统自身的需要安排代码的位置。

之后bootsect程序改变DS、ES、SS寄存器,与CS一样指向0x90000的位置,并把SP寄存器设为0xFF00,即栈顶为0x9FF00,之后的程序代码便具备执行push、pop的基础。

2.2.2 加载setup到内存中
借助BIOS的int0x13中断,可以将磁盘中指定位置的内容加载到内存中,与int0x19中断不同,int0x19是加载磁盘固定位置的内容到固定内存位置,int0x13则是用户传递参数指定需要加载的扇区和内容;另外int0x19中断是BIOS自身调用中断服务程序,而int0x13则是bootsect调用中断服务程序。执行int0x13中断后,计算机加载磁盘第2个扇区开始的4个扇区的内容到0x90200的位置(与0x90000相距512个字节的位置),该位置紧挨着bootsect。

2.3 加载system模块

加载system模块与加载setup模块基本相同,只不过是system模块的大小有240个扇区,最终system模块会加载到0x10000开始的120KB的范围。


三、向32位模式转变,为main函数的调用做准备

3.1 执行setup

执行 bootsect代码加载完 setup、system模块后,程序跳转到setup模块继续执行。setup会光标位置、显示页面数据、硬盘参数表等数据放在0x90000~0x901FC的位置,这会覆盖掉部分bootsect的程序。因此bootsect已经没有什么作用,不会影响后面的程序执行。在linux启动阶段,存在很多这样的情况:新的程序覆盖内存中不会再用到的程序位置,把内存的使用发挥到极致。

3.2 移动system模块到0x00000

在后面执行main函数时,操作系统将调用32位保护模式下的中断服务程序,因此需要替换掉系统加载时的中断向量表,重建新的中断服务体系。首先要做的是把CPU的标志寄存器(EFLAGS)的IF位置0,那么就实现了关中断。关中断的原因在于复制system到0x00000位置的过程中,会覆盖原来的中断向量表,如果此时产生中断,那么很可能无法找到中断服务程序的正确位置,从而导致系统崩溃。 为何system模块不直接加载到0x00000的位置,而要先放在0x10000呢?因为加载 system模块过后,还有一些操作需要用到中断服务,因此不能这么快覆盖掉。

3.3 设置中断描述符表和全局描述符表

全局描述附表(GDT)是系统中唯一存放段寄存器内容的数组。在保护模式下的段寻址通过在该表中进行寻找。表中存放了每个任务的局部描述符表(LDT)的地址和任务状态段(TSS)地址,用于完成进程中各段的寻址、现场保护与现场恢复。
GDT基地址寄存器(GDTR)是用于指向GDT用的,因为GDT可以存放在内存中的任何位置,因此需要标识GDT的入口位置。
中断描述符表(IDT)是保存保护模式下所有中断服务程序的入口地址,类似实模式下的中断向量表。
IDT基地址寄存器(IDTR)作用与GDTR类似。

setup程序会设置GDTR和IDTR,并建立GDT。

3.4 打开A20,实现32位寻址

16位模式时,只使用A0~A19地址线,在32位模式下,使用A0~A31地址线

3.5 setup程序对可编程中断控制器8259A进行重新编程,为执行head.s做准备

在保护模式下,int0x00~int0x1F保留作为内部中断和异常中断,所以需要对8259A进行重新编程,把原来对应的中断号重新分布。在保护模式下,IRQ0x00~IRQ0x0F的中断号对应int0x20~int0x2F。之后setup程序将CR0寄存器的第0位(PE)置1,设定处理器工作方式为保护模式。

之后执行jmpl  0,8这条指令,跳转到head.s的位置执行。jmpl是无条件段间跳转指令,“0”表示段内偏移,“8”在32位保护模式下表示段选择符位置。"8"写成二进制表示为"1000",其中后两位表示特权级;从后数起第三位为0表示GDT,为1表示LDT;“1”表示所选表项,即选择第一项,即选择GDT的第一项。根据开始setup的初始化,GDT第二项段基址为0x00000000,最终组成的段基址和段内偏移组成地址

3.6 执行head.s

3.6.1 设置各段寄存器
head.s是由汇编代码编写而成,与以c语言编写的内核代码链接成为system模块,其中head.s在system模块的头部,即0x00000的位置。head.s为系统转为保护模式工作作准备。CS寄存器在实模式下为代码段基址,而在保护模式下为代码段选择符。通过前面的命令jmpi 0, 8可以把CS与GDT的第一项关联起来。之后为DS、ES、FS、GS寄存器赋值为0x10,与前面的"8"意义相同,实际等于上述4个寄存器指向GDT的第二项。同时设置好内核栈,然后设置好中断描述符表。

3.6.2 重建新的GDT
现在需要废除原GDT建立新的GDT,因为原GDT在setup上面,以后setup代码区域会被覆盖,因此需要head.s新建一个GDT。

3.6.3 建立内存分页机制
到了进入main函数的最后阶段,此时开始创建分页机制。首先将页目录表和4个页表放在物理内存的起始位置,从内存其实位置开始的5个页空间全部清零,为初始化页目录和页表做准备,这5个页空间将覆盖原来head自身所占的内存。然后设置页目录前四项分别指向前四个页表。在第4张页表的最后一项设置为指向寻址空间的最后一项。页表设置完毕后,设置页目录机制寄存器CR3,使之指向页目录表,再将CR0最高位置1,即打开分页机制控制位。

建立分页机制之后,跳转到操作系统的main函数继续执行。



附录:

CR0寄存器:0号32位控制寄存器,存放系统控制标志。第0位为PE标志,置1时CPU工作在保护模式,置0时为实模式。第32位为PG位,当置1时打开分页机制控制位。
CR3寄存器:3号32位控制寄存器,高20位存放页目录的基地址。当CR0中的PG位置1时,CPU使用CR3指向的页目录和页表进行虚拟地址到物理地址的映射。

发布了39 篇原创文章 · 获赞 5 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/sadoshi/article/details/27202679
今日推荐