开机加电到main执行:


BIOS运行


按下开机键之后计算机便开始运行。CPU一开始在16位的实模式下运行,寻址空间为20位,也就是1MB。CPU执行的是内存中的指令,这时内存是空的,没有可执行的指令。所以BIOS会首先将自身程序加载到物理内存的特定位置,386之前的机器为0xFFFF0的位置,该位置是配件厂商协商好的。然后CS和IP寄存器会分别被强制设置为0xF000、0xFFF0,也就是CPU下一条的指令地址就是0xFFFF0。


BIOS程序会检测硬件信息,包括内存、硬盘等,并会在物理内存的开始处构建实模式的中断向量表(256个,每个中断向量占用4个字节,两个是CS,两个是IP,总共占用1KB的内存空间)、紧接着的是256字节的BIOS数据区、大约57KB开始处的中断服务程序(8KB左右)。这时候的中断服务程序只是适用于实模式,会在后面加载操作系统时被擦除。现在所做的工作都是硬件上设计好的,和机器当前安装的操作系统没有一点关系。




加载并执行bootsect


现在BIOS要开始加载硬盘上的引导程序,开始准备执行操作系统。BIOS产生0x19中断,CPU通过BIOS构建的中断向量表执行对应的中断服务程序,该程序是把启动盘的第一扇区加载到物理内存0x07C00处(这个操作也是也是硬件厂商设定的,硬件商不知道用户将来要安装什么操作系统,只能做一个统一处理将其加载到特定位置然后调至新的引导程序执行)。该扇区就是操作系统的引导程序(安装Windows是Windows的引导程序,安装Ubuntu是grub引导程序,linux 0.11是软盘上的bootsect.s)。现在内存中开始有了操作系统的代码。


bootsect.s只有512字节,能做的功能很少,所以它需要加载一些更长的的程序:setup和system程序。bootsect首先规划内存,定义各个参数和后续操作要涉及的内存位置;然后将自身程序 复制到0x90000处,并跳转到新的代码处继续执行,CS被设置为新的地址0x9000;接下来设置启动阶段要使用的堆栈,SP设置为0xFF00,SS设置为0x9000.堆栈从0x9FF00处开始向下增长,有了堆栈就可以进行一些复杂的指令操作。


bootsect通过int 0x13产生中断,执行0x13中断服务程序:磁盘服务程序。之前加载bootsect的0x19中断也是加载磁盘的操作,但是是固定加载磁盘的第一个扇区。但是0x13中断可以根据参数加载指定扇区和长度的磁盘数据。0x13中断服务程序将setup程序加载到内存,setup是从软盘第二个扇区开始的占用4个扇区的程序,加载到内存的位置是0x90200,紧挨着bootsect程序。这样bootsect执行完毕后就能马上执行setup。


bootsect继续执行,再次通过0x13中断从磁盘加载system程序至内存0x10000处(system程序共占用240个扇区,120KB,是Linux0.11操作系统的所有代码和数据)。bootsect最后检测磁盘的类型和大小等信息确定根文件系统设备,并存储到root_dev中。该变量在之后linux的启动过程中被多次使用。至此,bootsect执行完毕,此时内存分布如下所示:


bios加电 bios加电启动_加载



执行setup


现在setup程序开始执行,它首先利用BIOS的中断服务程序提起硬件的一些参数,并存放在0x90000-0x901FC处,覆盖bootsect代码区域。接下来setup到向32保护模式转变,为main函数的执行做好准备,setup做的主要工作有:


1.关中断并将system模块从0x10000处移动到内存的起始位置,覆盖了由BIOS建立的中断向量表。此前的中断向量表都是通过BIOS设定的,现在操作系统开始运行要建立自己的中断服务程序。


2.setup程序初始化中断描述符表(IDT,一个中断描述符位64位)寄存器和全局描述符表(GDT)寄存器。注:32位和16位模式下的中断机制不同,16位使用中断向量表,固定在内存的起始位置,32位使用IDT,位置由IDTR记录。


3.打开A20,可以进行32位寻址(实模式下寻址空间是20位,1MB,打开A20之后虽然线性地址空间是4GB,寻址空间变为16M)。


4.为保护模式下执行head.s做准备(head.s是system从开始处也就是内存0x00000开始,占用25KB+184bytes的一段代码,为main构建执行环境,紧挨着head.s的是main程序);对可编程中断控制器重新编程(因为保护模式下,0x00-0x1F被Intel保留,需要对其重新编程);然后将CPU设置保护模式,跳转至head处执行。


5.head程序将寄存器从实模式转变到保护模式,简单初始化IDT,重建保护模式下的中断服务,在main中会具体设置IDT。设置GDT和GDTR,并将main函数地址入栈(这样下一次返回弹出的IP就指向main的入口地址了),然后去创建分页机制。


head程序在创建分页机制之前已经执行了一大段代码,前面有一大段的废弃空间,head将在此处构建页目录表和页表。将内存起始处的5个页的物理空间(20KB)清零,第一个空间存储页目录表,后四个存储页表(每个表能覆盖4MB的内存空间,所以内核能访问的内存空间只有16MB)。首先设置页目录表(也就是填充页目录的前四项,分别指向4个页表),然后从第四个页表的末端开始从后往前填充4个页表,这四个页表是内核专属的页表,另外每个进程都有自己的页表,两者寻址不同。至此,内存开始的25KB184B的空间(head的空间)依次有页目录表,四个页表,缓冲区1KB,184Bytes剩余代码空间,IDT2KB,GDT2KB。然后将页目录表基址寄存器CR3指向页目录表,也就是0x00000处,并启动分页机制开关PG标志位,这样内核就能够通过CR3寄存器访问页目录表,从而访问16MB的内存空间。最后通过ret模拟返回,堆栈弹出main的执行地址准备执行main函数。