首页 > 代码库 > 电脑从开机加电到操作系统main函数之前执行的过程

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

总的来说在操作系统加电启动之后到main函数执行之前操作系统经历了以下3个大步骤
1.启动BIOS。这个时候位于实模式下,加载中断向量和中断服务程序
2.加载操作系统内核并为保护模式做准备。这个时候操作系统一共加载了3部分代码:引导程序bootsect,内核代码setup,内核代码system模块
3.从实模式转换为32位保护模式。这个过程要做大量重建工作,并且持续工作到操作系统main函数的执行过程。细说的话,主要包括打开32位寻址空间,打开保护模式,建立保护模式下的中断相应机制与保护模式配套的相关工作,建立内存分页机制。

名词说明:
实模式:20位的存储器地址空间,可以直接软件访问BIOS及周边硬件,没有硬件支持的分页机制和实时多任务概念。从80286开始,所有的80x86CPU开机状态都是实模式。
中断向量表:实模式下用于记录所有中断号对应的中断服务程序的内存地址。
中断服务程序:用于指示中断发生后该怎么办的程序。
SS:栈基址寄存器,SP:栈顶指针,两者一起构成栈在内存中的位置。压栈方式为高地址向地地址。
根文件系统设备:linux 0.11使用Minux OS的文件系统管理方式,要求系统必须存在一个根文件,其他文件系统挂在其上。因此Linux 0.11在启动时需要两部分数据,即系统内核镜像和根文件系统。
GDT(全局描述符表):在系统中唯一的存放段寄存器内容的数组,配合程序进行保护模式下的段寻址。其可理解为所有进程的总目录表,其中存放每一个task局部描述符表(LDT)地址和任务状态段(TSS)地址,完成进程中各段的寻址,现场保护与现场恢复。
IDT(中段描述符表):保存保护模式下所有中断服务程序的入口地址,类似于实模式下的中断向量表。
GDTR,IDTR:分别为以上描述符表的基地址寄存器
CR0寄存器:0号32为控制寄存器,存放系统控制标志。第0位为PE标志,置1时CPU工作在保护模式下,置0时为实模式。
设置段寄存器指令:该组指令的功能是把内存单元的一个“低字”传送给指令的指定16寄存器,然后把“高字”传给相应的段寄存器。命令格式:LDS/LES/LFS/LGS/LSS Mem, Reg

说明:我学的是linux 0.11内核,目前有的内核版本是3.6,虽然差好多,但是适合学习。
注意:开始阶段的BIOS与操作系统无关


1.启动BIOS,准备实模式下的中断向量表和中断服务程序
电脑启动后,CPU逻辑电路被设计为只能运行内存中的程序,没有能力直接运行存在于软盘或硬盘中的操作系统,如果想要运行,必须要加载到内存(RAM)中。
BIOS是如何启动的:
CPU硬件逻辑设计为在加电瞬间强行将CS值置为0XF000,IP为0XFFF0,这样CS:IP就指向0XFFFF0这个位置,这个位置正是BIOS程序的入口地址。
BIOS在内存中加载中断向量表和中断服务程序
    BIOS程序被固化在计算机主机板上的一块很小的ROM芯片里。现在CS:IP已经指向了0XFFFF0这个位置,意味着BIOS开始启动。随着BIOS程序的执行,屏幕上会显示显卡的信息,内存的信息……说明BIOS程序在检测显卡,内存……期间,有一项对启动操作系统至关重要的工作,那就是BIOS在内存中建立中断向量表和中断服务程序。
    BIOS程序在内存最开始的位置(0x00000)用1KB的内存空间(0x00000~0x003FF)构建中断向量表,在紧挨着它的位置用256KB的内存空间构建BIOS数据区(0x00400~0x004FF),并在大约57KB以后得位置(0x0e05b)加载了8KB左右的与中断向量表相应的若干中断服务程序。
    中断向量表有256个中断向量,每个中断向量占4个字节,其中两个字节是CS值,两个字节是IP值。每个中断向量都指向一个具体的中断服务程序。

2.加载操作系统内核并为保护模式做准备
现在开始计算机要分三批逐次加载OS的内核代码。分别是引导程序bootsect,内核代码setup,内核代码system模块
加载引导程序bootsect
    首先对CPU发送int 0x19中断,使CPU运行int 0x19中断对应的中断服务程序,这个中断服务程序的作用就是把软盘第一个扇区的程序加载到内存的指定位置。总结来说就是:找到软盘并加载第一扇区。这个第一扇区的内容就是bootsect,第一扇区称为启动扇区。
    至此,第一批代码bootsect已经加载进内存了,下面的工作就是执行bootsect把软盘的第二,三批代码加载入内存。
加载第二部分程序setup
    bootsect首先对内存进行规划:包括之后将要加载的setup程序的扇区数,被加载到的位置,启动扇区被BIOS加载的位置等等。总之就是规划好之后内存分布,方便之后使用。
    bootsect接着复制自身代码。从内存0x07c00复制到内存0x90000处。这个时候说明OS开始根据自身的需求安排内存了。
    最后,bootsect将setup加载至内存。其中加载过程需要借助BIOS提供的int 0x13中断向量指向的中断服务程序来完成。该程序将软盘第二个扇区开始的4个扇区,即setup.s对应的程序加载至内存的SETUPSEG(0x90200)处。
加载第三部内核代码--system模块
    加载第三批代码仍然用int 0x13中断向量。
    这次加载的扇区数是240个,为了让用户觉得不是机器故障,Linus在屏幕上设计了打出一行字符串:"Loading system ... "
    加载完毕后再确定一下根设备号。

3.从实模式转换为32位保护模式
    至此,3部分代码已经加载完毕,开始转变进入保护模式。
关中断并将system移动到内存地址起始位置0x00000
    关中断是为了不响应外部中断,直到保护模式下的中断服务体系被构建完毕才会打开中断。
    回顾一下,0x00000这个位置原来存放着由BIOS建立的中断向量表级BIOS数据区。这个复制会覆盖原区域。这样的好处是:
        a)废除BIOS的中断向量表。
        b)内存空间回收
        c)让内核代码占据内存物理地址最开始的有利位置
设置中断描述附表和全局描述符表
    设置并初始化这两个表
打开A20,实现32位寻址
    A20为第21根地址线,实模式下启用前20根(A0~A19),保护模式将开启A20~A31,标志32位寻址。
为保护模式下执行head.s做准备
    为了建立保护模式下的中断机制,setup程序将对可编程中断控制器8259A进行重新编程,重新映射中断号。
    接着执行代码:
    ...
    mov ax,#0x0001   ! protected mode bit
    lmsw ax
    jmpi 0,8                 ! jmp offset 0 of segment 8 (cs)
    ...
    其中8代表1000,最后两位表示内核特权级,11则表示用户权级;第三位0表示GDT,1则表示LDT;1表示选择表中的第1项(从0开始),这个用来确定为代码段(cs)的段基址和段限长等信息。示意图如下:
    


head.s开始执行
    head.s的加载方式与之前的bootsect,setup有所不同,大致过程是:先将head.s汇编成目标代码,将用C语言编写的内核程序编译成目标代码,然后链接成system模块。也就是在内存既有内核程序,又有head程序。两者是紧挨着的。
    
     head程序除了做一些调用main之外的工作之外,还主要用程序的自身代码在程序自身所在的内存空间创建了内存分页机制。
    head程序将L6标号和main函数入口地址压栈,栈顶为main函数地址,目的是使head程序执行完后通过ret指令就可以直接执行main函数。正常来说main函数是不应该退出的,但如果main函数异常退出,就会返回这里标号L6处继续执行,防止系统崩溃。
    这些压栈操作完成后,head执行setup_paging:去执行,开始创建分页机制。先要将页目录表和4个页表放在物理内存的起始位置。
    4个页表分别映射0x0000到0xFFF000的所有页面,一个页面大小为4KB
    
    head程序设置完的内存分布示意图
 

至此,开机到main函数执行之前的过程结束。


参考:Linux内核设计的艺术

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