系统是如何启动的

786 阅读9分钟

1. 炒菜与计算

在李志军老师的操作系统课程中,借用炒菜的过程十分形象地描绘了程序执行的过程。假设我们现在要做一份西红柿鸡蛋,首先从书架中找到《食谱》,找到对应的菜谱后我们将西红柿炒蛋的制作步骤一步步地记下来(将被执行程序从磁盘载入内存并设置程序计数器指向程序地址),并将西红柿和鸡蛋也准备好放在桌子上(载入数据),这些准备工作都做完之后,我们将从按照菜谱从第一步开始执行(按程序计数器执行程序流程),经过宽油、爆葱花、下鸡蛋等步骤之后,我们将制作好的菜肴盛好再摆到餐桌上(将计算结果输出),这一过程就算结束了。但是这也只是将操作系统的工作做了简化后的形象描述,我们会在下面进行更为详细的研究。

2. 启动

2.1 简单认识CPU

可以认为CPU通常由:寄存器+控制器+运算器+时钟组成,而程序员只需要了解寄存器即可,其余三个都不用太过关注。

2.1.1 程序计数器

其中程序计数器(PC)是我们了解CPU工作原理的入口,若将CPU的工作简单的理解为“取址执行”的话,那么PC中保存了下一条将要执行的指令的地址,决定了指令执行的顺序:

更进一步,在x86架构中,可以认为PC的功能是由指令指针IP(32位是EIP)和代码段寄存器CS实现的:

  • IP/EIP(Instruction Pointer):指令指针寄存器,存在于CPU中,记录将要执行的指令在代码段内的偏移地址,和CS组合即为将要执行的指令的内存地址。实模式为绝对地址,指令指针为16位,即IP;保护模式下为线性地址,指令指针为32位,即EIP。
  • CS(Code Segment Register):代码段寄存器,存在于CPU中,指向CPU当前执行代码在内存中的区域(定义了存放代码的存储器的起始地址)。

2.2 上电

我们都知道通过命令行或图形窗口手动启动一个程序后,会由内核将该程序从磁盘载入内存,再由程序计数器PC指导CPU一步步执行下去,但是当电脑上电后内存中应该是没有任何数据的,也没有任何程序甚至操作系统在运行,那么我们如何启动第一个引导程序?

在软件视角中这似乎是一个先有鸡还是先有蛋的问题,用户程序需要内核程序拉起,内核程序(操作系统)又是由BIOS拉起的,但是BIOS又是由谁启动的呢?如果软件不行,可能就需要在硬件层面解决了。

也为了解决最开始的启动问题,Intel将所有80x86系列的CPU,包括最新型号的CPU的硬件都设计为加电即进入16位实模式状态运行。将CPU上电后的硬件逻辑设计为加电后就将CS的值置为0xF000、IP的值置为0xFFF0,这样CS:IP就指向0xFFFF0这个地址位置。

这是一个硬件动作,出厂时就被设定死了,如果CS:IP指向的地址0xFFFF0处没有正确的程序我们的计算机就永远无法正确启动,但是BIOS程序的入口地址恰恰就是0xFFFF0(BIOS程序被固化在计算机主机板上的一块很小的ROM芯片里,也就是说0xFFFF0被映射到计算机主板上的BIOS ROM中)。

注1: 16位实模式状态:由于8086的CPU是16位,即它的所有寄存器和寄存器之间的数据总线都是16位的,而其外部数据总线却是20位的,那么如何通过16位的寄存器访问20位的寻址范围呢?答案是使用两个寄存器就可以了,也就是我们上面介绍的CS:IP,20位的地址有CS左移4位加上IP的16位得出。

注2:ROM(Read Only Memory):只读存储器。ROM有一个重要的特性,就是断电之后仍能保存信息,这一点和硬盘类似。

2.2.1 BIOS启动

通过上面的动作,CPU可以顺利地执行BIOS程序,在BIOS程序执行的过程中,主要做了以下几个事情:

  1. 检查RAM,键盘,显示器,磁盘等设备;
  2. 将磁盘0磁道0扇区(磁盘的第一个扇区,512字节,引导扇区,存放bootsect.s)读入0x07c00处;
  3. 设置CS=0x07c0,IP=0x0000(执行引导扇区的程序bootsect.s);

电脑加电后的启动程序执行过程

引导程序在磁盘上的布局示意图

2.2.2 bootsect.s

我们知道bootsect.s是一个汇编程序,也是系统第一个从磁盘加载的程序(第一个我们能够控制的程序),由于汇编代码日常较少直接编写,这里略过详细的代码解析,只列出了该程序都完成了哪些事情:

  1. 把0x07c0:0x0000开始的512个字节数据,拷贝到0x9000:0x0000(将bootsect.s移动到0x90000);
  2. 调用BIOS的0x13中断,将磁盘上的引导扇区后的4个扇区(setup.s)加载到0x90020(bootsect.s)后;
  3. 调用BIOS的0x10中断,输出显示系统正在加载的文字提示;
  4. 调用BIOS的0x13中断,加载操作系统的模块(约几百个扇区)至0x10000;

注:bootsect.s第一步就将本程序从0x07c00拷贝到0x90000,这也是无奈之举,因为后面setup.s执行时会将操作系统模块移动到0x00000,因为操作系统模块一般由数百个扇区构成,所以在移动时很可能会覆盖0x07c00范围的内存,导致后续启动程序被覆盖从而无法启动。

2.2.3 setup.s

  1. 调用0x15中断,获取计算机的设备信息:内存大小、显卡参数等;
  2. 将操作系统模块移动到0x00000,即内存的起始位置;
  3. 初始化GDT表并将计算机切换到保护模式;
  4. 跳转到内存的起始位置:0x00000;

这里需要解释下为什么要从16位实模式切换到32保护模式,在上面我们通过CS左移4位+IP的模式可以扩展为20位的寻址范围,即1MB,但是32位计算机的寻址空间最大支持4GB,所以20位只支持1MB的模式肯定不能再使用了。当我们将模式切换到32位保护模式时,只有寻址的计算方式发生了改变。

GDT(Global Descriptor Table,全局描述符表),在系统中唯一的存放段寄存器内容(段描述符)的数组,配合程序进行保护模式下的段寻址。它在操作系统的进程切换中具有重要意义,可理解为所有进程的总目录表,其中存放每一个任务(task)局部描述符表(LDT,LocalDescriptor Table)地址和任务状态段(TSS,Task Structure Segment)地址,完成进程中各段的寻址、现场保护与现场恢复。

GDTR(Global Descriptor Table Register,GDT基地址寄存器),GDT可以存放在内存的任何位置。当程序通过段寄存器引用一个段描述符时,需要取得GDT的入口,GDTR标识的即为此入口。在操作系统对GDT的初始化完成后,可以用LGDT(Load GDT)指令将GDT基地址加载至GDTR。

IDT(Interrupt Descriptor Table,中断描述符表),保存保护模式下所有中断服务程序的入口地址,类似于实模式下的中断向量表。

IDTR(Interrupt Descriptor Table Register,IDT基地址寄存器),保存IDT的起始地址。

2.2.4 head.s

从这里开始正式进入操作系统模块的代码,system模块里面既有内核程序,又有head程序。两者是紧挨着的。要点是,head程序在前,内核程序在后,所以head程序名字为“head”。head程序、以main函数开始的内核程序在system模块中的布局示意图:

head程序除了做一些调用main的准备工作之外,还做了一件对内核程序在内存中的布局及内核程序的正常运行有重大意义的事,就是用程序自身的代码在程序自身所在的内存空间创建了内核分页机制,即在0x000000的位置创建了页目录表、页表、缓冲区、GDT、IDT,并将head程序已经执行过的代码所占内存空间覆盖。这意味着head程序自己将自己废弃,main函数即将开始执行。

执行这些动作后,内存分布变为:

一切就绪后,就可以设置CS:IP跳转到我们熟悉的main函数,进一步初始化内存、IO等,完成操作系统启动。

2.3 总结

综上所述,计算机上电后主要经理了以下步骤:

  1. 首先进入16位实模式状态并且从硬件层面开始执行BIOS ROM上的BIOS程序;
  2. BIOS程序从磁盘的第一个扇区加载bootsect.s并执行;
  3. bootsect.s加载了第2~5个扇区上的setup.s程序和system模块,然后调用setup.s;
  4. setup.s将system模块移动到内存头部,并由16位实模式切换到32位保护模式,将寻址范围由1M扩展为4G,而后调用head.s;
  5. head.s属于system模块中的程序,主要负责初始化内存页表、GDT、IDT等,为执行c语言main程序创造环境;
  6. 启动main程序,完成操作系统的启动;

起初不少人会有一些困扰:Linux 0.11是由c语言编写的,为什么在启动main程序之前执行了bootsect.s、setup.s、head.s这些汇编程序呢?

首先要意识到我们正在讨论操作系统的启动,与应用程序的启动是有很大区别的,应用程序可以由操作系统从磁盘载入内存,再设置PC执行代码首地址进行执行,但是操作系统由谁启动呢,这仿佛又回到了先有鸡还是先有蛋的问题。

再者,我们通过硬件层面的BIOS进入16位实模式状态,在这种状态下是无法执行32位的c语言程序的,并且系统的内存、GDT、IDT等都需要进行设置才能顺利启动应用程序,所以才有了本文中简单介绍的内容。

参考资料

[1] 网易云课堂,哈尔滨工业大学《操作系统之应用》 李治军

[2] 计算机加电后操作系统启动过程: www.cnblogs.com/ronny/p/778…

[3] 《Linux 内核设计的艺术》(第2版)