这本书里提到的操作系统绝大部分是指Linux 0.11
1.1
从开机到main函数执行分三步
- 启动BIOS,准备实模式下的中断向量表和中断服务程序
- 启动盘加载操作系统到内存
- 为执行32位的main函数做过渡
实模式:20位存储器地址空间,1MB的存储器可寻址,可直接通过软件方式访问BIOS及周边硬件,没有硬件支持的分页机制和实时多任务的概念。
Intel 80x86 CPU可以分别在16位实模式和32位保护模式下运行,加电即进入16位实模式状态运行,加电瞬间强行将CS的值置为0xFFFF,IP的值置为0x0000,CS:IP指向0xFFFF0,即BIOS的地址范围。
IP/EIP:指令指针寄存器,记录将要执行的指令在代码段内的偏移地址,与CS组合即为将要执行的指令的内存地址。实模式为绝对地址,指令指针为16位,即IP;保护模式为线性地址,指令指针为32位,即EIP。
CS:代码段寄存器,指向CPU当前执行代码在内存中所在的区域。
如果0xFFFF0上没有可执行代码,计算机就此死机。BIOS固化在一片ROM里,断电仍能保存信息。
中断向量表中有256个中断向量,每个中断向量占4个字节,其中两个是CS,两个是IP,每个中断向量指向一个具体的中断服务程序。
1.2
Linux 0.11,1991年,分三批加载操作系统代码(bootsect、setup、system)。计算机完成自检等操作后,CPU接收到int 0x19中断(BIOS提供),对应的中断服务程序是将软盘(是的,那时还是软盘)的第一个扇区中的程序加载到内存,这个就是Linux 0.11的引导程序,bootsect(代码写在bootsect.s中),作用是把软盘中的操作系统载入内存。
操作系统把最开始执行的程序`定位`在启动扇区,BIOS从启动扇区把代码加载到0x07C00(BOOTSEG),不管从这个扇区中加载的是什么
bootsect首先规划了内存,确保将要载入内存的代码和已经载入内存的代码及数据互不覆盖,并且各自有足够的内存空间。然后将自身(512B)从0x07C00处复制到内存0x90000(INITSEG)。之后bootsect执行int 0x13中断,将setup.s对应的程序加载至内存的SETUPSEG处,将约240个扇区的system模块加载至SYSSEG处往后的120KB空间中。
这时bootsect把根设备号保存在root_dev中,作为机器系统数据之一。Linux 0.11使用Minix操作系统的文件系统管理方式,要求系统必须存在一个根文件系统上,其他文件系统挂接其上。Linux 0.11的启动需要两部分数据,系统内核镜像和根文件系统,这里的文件系统指的是有配套文件系统格式的设备,而不是操作系统内核中的文件系统代码。
下面程序跳转到0x90200处,setup开始执行,首先利用BIOS提供的中断服务程序从设备上提取内核运行所需的机器系统数据,包括光标位置和显示页面等。机器系统数据将覆盖bootsect所在的绝大部分区域(512字节中覆盖了510字节,2字节未覆盖)。
1.3
接下来操作系统要使计算机在32位保护模式下工作。首先,操作系统将CPU的标志寄存器(EFLAGS)中的中断允许标志位(IF)置0,操作系统不再响应BIOS中断,为接下来操作系统进入保护模式做准备。接着setup将位于0x10000的内核程序拷贝至内存地址起始位置0x00000处,覆盖BIOS的中断向量表和数据区。
接下来,setup自身提供的数据信息对IDTR和GDTR进行初始化。
GDT:全局描述符表,系统中唯一存放段寄存器内容(段描述符)的数组,配合程序进行保护模式下的段寻址。GDT可以理解为所有进程的总目录表,存放着每一个任务的局部描述符表(LDT)地址和任务状态段(TSS)地址,用于完成进程中各段的寻址、现场保护与现场恢复。
GDTR:GDT可以存放在内存的任何位置,当程序通过段寄存器引用一个段描述符时,需要取得GDT的入口,GDTR标识的即为此入口。操作系统完成对GDT的初始化后,可以用LGDT指令将GDT基地址加载至GDTR。
IDT:中断描述符表,保存保护模式下所有中断服务程序的入口地址
IDTR:IDT基地址寄存器,保存IDT的起始地址
接下来,打开A20地址线,CPU可以进行32位寻址,最大寻址空间为4GB。
setup对可编程中断控制器8259A重新编程,因为保护模式下和实模式的中断号不同。将CR0寄存器的第0位(PE)置1,即设定了保护模式,后续执行哪里的程序要根据GDT表来决定。
head程序属于system模块的一部分,在main函数之前执行。head创建了页目录表,页表,缓冲区,GDT,IDT,并将自身已执行过的代码所占的内存空间覆盖。
实模式下,CS本身是代码段基址,保护模式下,CS本身是代码段选择符。
head程序现在要对中断描述符表进行设置,废除已有的GDT,并在内核中的新位置重新创建全局描述符表。原本的GDT所在位置是setup里设置的,将来会被缓冲区覆盖。段限长也从原来的8MB变为现在的16MB。
head程序将L6标号和main函数入口地址压栈,栈顶为main函数地址,目的是使head程序执行完后通过ret指令可以直接执行main函数,如果main函数退出,就会返回到L6标号处继续执行,并产生死循环。
接下来head程序跳到setup_paging,开始创建分页机制。首先将页目录表和4个页表放在物理内存的起始位置,从内存起始位置开始的5页空间内容全部清零(每页4KB),重新设置。