【操作系统】从开机加电到执行main函数之前的过程

参考用书:《Linux内核设计的艺术》

引言

从开机加电到执行main函数之前,主要分为以下三个部分:

  1. 启动BIOS,准备实模式下的中断向量表和中断服务程序
  2. 从启动盘加载操作系统程序到内存:
    • bootsect程序(扇区 -> 主机内存)
    • setup程序(设置内核运行所需的机器系统数据)
    • head程序(保护模式,内存分页)
  3. 为执行32位的main函数做过渡

整体思维导图:

启动BIOS

  1. CPU的硬件都设置为加电进入实模式
  2. BIOS程序被固化在ROM中,由硬件方式执行:加电瞬间,CS:IP指向BIOS程序的入口地址(0xFFFF0)
  3. BIOS检测显卡、内存······,并且在内存中建立中断向量表和中断服务程序

补充知识:

  • ROM:只读存储器,断电之后仍能保存信息
  • 0x00400 = 4*(16^2)字节 = 4*256字节 = 1024字节 = 1KB

加载操作系统内核程序

加载第一部分内核代码——引导程序bootsect

理解: bootsect中sect是section的缩写,代表软盘。整个bootsect程序就是将内核程序从软盘加载到主机内存的过程。
过程

  1. CPU接收到一个int 0x19中断
  2. 对应中断服务程序把软盘中第一扇区的程序(512B)加载到内存的指定位置(0x07C00)

【操作系统】从开机加电到执行main函数之前的过程_第1张图片

冷知识:0x07C00来自Intel第一台个人电脑8088芯片,为了保持兼容,以后的CPU都保留此地址

加载第二部分内核代码——setup

BIOS将引导程序bootsect载入内存后,现在需要将第二批、第三批程序陆续加载到内存中。

1.bootsect对内存的规划
为了实现上述操作,bootsect首要工作就是先规划内存。如图,bootsect程序对后续操作涉及的内存位置进行了设置:
【操作系统】从开机加电到执行main函数之前的过程_第2张图片

2.复制bootsect
接下来,bootsect将自身复制至内存0x90000(INITSEG)处:
【操作系统】从开机加电到执行main函数之前的过程_第3张图片

过程:一遍执行,一边复制
目的:复制完后,就能根据自己的需要规划内存,程序可以执行更复杂的数据运算类指令了

3.将setup程序加载到内存中
通过BIOS提供的int 0x13中断向量指向的中断服务程序,将软盘第二扇区开始的4个扇区(即setup.s对应程序)加载到内存中(SETUPSEG)。
【操作系统】从开机加电到执行main函数之前的过程_第4张图片

此时,操作系统已经从软盘中加载了5个扇区的代码。bootsect程序执行完后,setup程序就要开始工作了。

加载第三部分内核代码——system模块

1.bootsect载入系统模块
接下来,bootsect程序进行第三批程序的载入工作。首先,bootsect借助BIOS中断int 0x13,将240个扇区的system系统模块加载进内存。至此,整个操作系统的代码已全部加载至内存。bootsect还需要再确认一下根设备号,然后其工作就结束了!

补充:Linux0.11要求系统必须存在一个根文件系统,这里的文件系统☞配套文件系统格式的设备,如一张格式化好的软盘

2.setup程序提取机器系统数据
setup程序现在开始执行。首先,它利用BIOS中断服务程序从设备上提取内核所需要运行的机器系统数据(光标位置、显示页面等),并加载在内存中。
注意:BIOS提取的机器系统数据将覆盖bootsect程序所在部分区域,这提高了内存的利用率。
到此为止,内核已全部加载完成。接下来,系统将通过已加载到内存的代码,实现从实模式到保护模式的转变,使得Linux真正成为“现代”操作系统!

向32位模式转变

本节,操作系统执行的操作包括打开32位的寻址空间、打开保护模式、建立保护模式下的中断响应机制等与保护模式相关的工作、建立内存分页机制,最后最好调用main函数的准备。

关闭中断,移动system

首先关闭中断,即将CPU的标志寄存器(EFLAGS)的中断允许标志(IF)置0。

补充:

  • EFLAGS相当于总开关
  • 这里的关闭中断并不意味着没有中断了,其实是仍会存在中断,只是不再响应处理而已

接下来,setup程序将位于0x10000的内核程序复制至内存起始地址0x00000处,将BIOS中断向量表和BIOS数据区完全覆盖。

设置中断描述符表和全集描述符表

setup程序对中断描述符表寄存器(IDTR)和全局描述符表寄存器(GDTR)进行初始化设置

【操作系统】从开机加电到执行main函数之前的过程_第5张图片

补充知识:
GDT:全局描述符表,是存放段寄存器内容(段描述符)的数组,可以理解为进程总目录表
GDTR:GDT基地址寄存器,是GDT的入口
IDT:中断描述符表,保护模式下所有中断服务程序的入口地址,相当于实模式下的中断向量表
IDTR:IDT基地址寄存器,是IDT的入口

打开A20,实现32位寻址

寻址:CPU能使用多大空间的内存

打开A20,意味CPU可以进行32位寻址,最大寻址空间为4GB,内存条范围由00xFFFFF扩展为00xFFFFFFFF。

2^32 = 4*2^30 = 4GB;2^32 = 16^8 = 0xFFFFFFFF

为保护模式下执行head.s做准备

1.setup程序对可编程中断控制器重新编程
若不对其重新编程,一些Intel保留作为内部的中断和异常中断将被覆盖。

2.设置CPU为保护模式
setup程序将CR0寄存器第0位(PE)置1,即设置CPU为保护模式。

3.跳转到head程序
通过jmpi 0, 8,从setup跳转到head程序。需要把这里的8看成二进制1000

  • 0:段内偏移
  • 8:段选择符
    • 二进制1000
      • 最后两位(00):内核特权级
      • 倒第三位(0):代表GDT
      • 第一位(1):GDT项号为第2项(从0开始)

head.s开始执行

执行过程的整体策略

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,其中:

  • 最后两位(00):内存特权级;
  • 倒数第三位(0):代表GDT;
  • 第4、5位两位(10):GDT的2项(从0开始),即第3项。

重要:理解每一位代表的东西
【操作系统】从开机加电到执行main函数之前的过程_第6张图片

4.对栈的设置:SS转为栈段选择符,栈顶指针成为32位的esp。

注意:栈顶增长方向由高地址向低地址

5.设置IDT:先让所有中断描述符默认指向ignore_int这个位置,然后对IDT寄存器的值进行设置。

  • IDT的一部分 ——> GDT表项 ——> 基址
  • 另一部分 —解析—>偏移+特权等信息

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。

你可能感兴趣的:(学校课程,#,操作系统,linux)