参考用书:《Linux内核设计的艺术》
从开机加电到执行main函数之前,主要分为以下三个部分:
整体思维导图:
补充知识:
- ROM:只读存储器,断电之后仍能保存信息
- 0x00400 = 4*(16^2)字节 = 4*256字节 = 1024字节 = 1KB
理解: bootsect中sect是section的缩写,代表软盘。整个bootsect程序就是将内核程序从软盘加载到主机内存的过程。
过程:
冷知识:0x07C00来自Intel第一台个人电脑8088芯片,为了保持兼容,以后的CPU都保留此地址
BIOS将引导程序bootsect载入内存后,现在需要将第二批、第三批程序陆续加载到内存中。
1.bootsect对内存的规划
为了实现上述操作,bootsect首要工作就是先规划内存。如图,bootsect程序对后续操作涉及的内存位置进行了设置:
2.复制bootsect
接下来,bootsect将自身复制至内存0x90000(INITSEG)处:
过程:一遍执行,一边复制
目的:复制完后,就能根据自己的需要规划内存,程序可以执行更复杂的数据运算类指令了
3.将setup程序加载到内存中
通过BIOS提供的int 0x13中断向量指向的中断服务程序,将软盘第二扇区开始的4个扇区(即setup.s对应程序)加载到内存中(SETUPSEG)。
此时,操作系统已经从软盘中加载了5个扇区的代码。bootsect程序执行完后,setup程序就要开始工作了。
1.bootsect载入系统模块
接下来,bootsect程序进行第三批程序的载入工作。首先,bootsect借助BIOS中断int 0x13,将240个扇区的system系统模块加载进内存。至此,整个操作系统的代码已全部加载至内存。bootsect还需要再确认一下根设备号,然后其工作就结束了!
补充:Linux0.11要求系统必须存在一个根文件系统,这里的文件系统☞配套文件系统格式的设备,如一张格式化好的软盘
2.setup程序提取机器系统数据
setup程序现在开始执行。首先,它利用BIOS中断服务程序从设备上提取内核所需要运行的机器系统数据(光标位置、显示页面等),并加载在内存中。
注意:BIOS提取的机器系统数据将覆盖bootsect程序所在部分区域,这提高了内存的利用率。
到此为止,内核已全部加载完成。接下来,系统将通过已加载到内存的代码,实现从实模式到保护模式的转变,使得Linux真正成为“现代”操作系统!
本节,操作系统执行的操作包括打开32位的寻址空间、打开保护模式、建立保护模式下的中断响应机制等与保护模式相关的工作、建立内存分页机制,最后最好调用main函数的准备。
首先关闭中断,即将CPU的标志寄存器(EFLAGS)的中断允许标志(IF)置0。
补充:
- EFLAGS相当于总开关
- 这里的关闭中断并不意味着没有中断了,其实是仍会存在中断,只是不再响应处理而已
接下来,setup程序将位于0x10000的内核程序复制至内存起始地址0x00000处,将BIOS中断向量表和BIOS数据区完全覆盖。
setup程序对中断描述符表寄存器(IDTR)和全局描述符表寄存器(GDTR)进行初始化设置。
补充知识:
GDT:全局描述符表,是存放段寄存器内容(段描述符)的数组,可以理解为进程总目录表
GDTR:GDT基地址寄存器,是GDT的入口
IDT:中断描述符表,保护模式下所有中断服务程序的入口地址,相当于实模式下的中断向量表
IDTR:IDT基地址寄存器,是IDT的入口
寻址:CPU能使用多大空间的内存
打开A20,意味CPU可以进行32位寻址,最大寻址空间为4GB,内存条范围由00xFFFFF扩展为00xFFFFFFFF。
2^32 = 4*2^30 = 4GB;2^32 = 16^8 = 0xFFFFFFFF
1.setup程序对可编程中断控制器重新编程
若不对其重新编程,一些Intel保留作为内部的中断和异常中断将被覆盖。
2.设置CPU为保护模式
setup程序将CR0寄存器第0位(PE)置1,即设置CPU为保护模式。
3.跳转到head程序
通过jmpi 0, 8
,从setup跳转到head程序。需要把这里的8
看成二进制1000
:
1.head程序的加载
head.s先汇编成目标代码,c语言内核程序编译成目标代码,然后链接成system模块。
2.head程序创建了内核分页机制
在0x000000创建页目录表、页表、页表缓冲区、GDT、IDT,并将head执行完的代码所占内存空间覆盖。这也意味着head将自己废弃,main函数开始执行。
1._pg_dir标识内核分页机制完成后的内核起始位置,head程序从这里建立页目录表,为分页机制做准备。
2.head正式执行,将CS的用法转为保护模式(CS作为代码段选择符),jump 0,8
使CS和GDT第2项关联,并使代码段基址指向0x000000。
3.段选择子指向内核代码段:DS、ES、FS和GS(都是段选择子)的值都置为0x10
,这里的0x10
也看成二进制00010000
,其中:
4.对栈的设置:SS转为栈段选择符,栈顶指针成为32位的esp。
注意:栈顶增长方向由高地址向低地址
5.设置IDT:先让所有中断描述符默认指向ignore_int这个位置,然后对IDT寄存器的值进行设置。
6.废除已有的GDT,并在内核新位置重建 ——> 段限长增加了一倍,变为16MB。这里再次对一些段选择符进行重新设置,包括DS、ES等。
7.检验A20地址线、数学协处理器。
8.将L6标号和main函数入口地址压栈,栈顶位main函数地址,这使得head执行完,能通过ret直接执行main函数。
9.创建分页机制:
首先,将页目录表和4个页表放在物理内存起始位置,此步骤覆盖了head程序自身内存空间(注意:这4个页表都是内核专属页表,将来每个用户进程都有他们的专属页表)。然后,设置页目录表的前4项,分别指向后4个表。然后,将CR3指向页目录表(CR3是物理地址!),启动分页机制开关PG标志置位。然后认定页目录表在内存的起始位置,这个位置是内核通过分页机制能够实现线性地址等于物理地址的唯一起始位置。
页目录表、页表都占1页(1 页 4KB,1 项 4B)
1 个页表有 1K 项,1 项对应一页覆盖的物理地址(4KB)
1 个页目录表覆盖 1K1K4KB = 4GB物理地址
10.ret,通过跳入main函数执行,将压入的main函数在执行入口地址弹出给EIP。