操作系统(1) 系统的启动

700 阅读9分钟

按下电源之前

在断开电源的情况下计算机内部并不是所有组件都停止工作,比如你有没有考虑过为什么一台没有联网的电脑断开电源一段时间后再启动时间还是准确的?这是因为主板上的RTC(Real-time clock)芯片由主板上的电池供电在断开外部电源的情况下也能工作。类似的组件还有CMOS(Complementary Metal Oxide Semiconductor),它是主板上的一块可读写的RAM(Random Access Memory)芯片,用来保存BIOS(Basic I/O System)的设置信息比如启动顺序,正是因为这些组件在断开外部电源状态下的继续工作才使得我们的计算机不至于一断电就回到出厂状态。

接通电源

按下电源后主板上各种组件严格按照电源时序苏醒,任何一个组件苏醒失败都将导致计算机的启动失败。时序方案有多种,根据主板和芯片组等硬件设备的特点而定。例如笔记本常用EC(Embed Controller)方案,台式机常用SIO(Super I/O)方案。

时序方案是非常复杂的,以下以EC方案为例仅做顺序上的简要描述

  1. EC在关机状态下保持运行,等待用户开机
  2. 用户按下电源键,EC接收到PWRSW#信号
  3. EC以PM_PWRBTN#通知南桥,南桥回应SLP_S5#,SLP_S4#,SLP_S3#三个信号
  4. EC收到SLP_S3#这个标志性的信号后通过PCON#通知ATX电源发出各路电压(+12V、+5V等)给主板各个组件供电
  5. 供电正常的话电源发出PWROK#信号由EC转交南北桥(从Intel5开始北桥已被整合进CPU中)
  6. 组件间的一系列互动...
  7. 最后CPU接收到CPU_RST#信号被唤醒(该信号由北桥发出,但北桥被并入CPU后是否还是如此本人没有查询到相关资料)

以下章节内容适用于32位X86平台非UEFI启动模式

第一条指令

硬件上设定CPU被唤醒后就处于实模式(CR0寄存器的PE标志位为0,对应保护模式的1),实模式是早期8086CPU的唯一工作模式,该模式下地址总线为20位,最大寻址能力为220=1M2^{20}=1M,现在的CPU为了向前兼容默认被唤醒也是处于实模式完成一系列操作后才切换到保护模式

CPU被唤醒后就取第一条指令执行,但此时内存是空的,所以第一条指令不在内存中,在BIOS所处的ROM中例如EPROM(Erasable Programmable Read Only Memory),CPU根据地址取指执行,那么第一条指令的地址是什么?有两种说法:

  1. 地址为0xFFFF0
  2. 地址为0xFFFFFFF0

其实两种说法都对,只是适用对象不同,Intel定义前者适用于Intel 8088,808616位处理器时代,后者适用于更新的处理器。

图片来源:member82 20161030162658345.png

第一条指令的地址是由硬件设定在通电后赋值给相应的寄存器:

  1. 地址0xFFFF0, CS=0xFFFF, IP=0x0000, 寻址方式: CS左移4位+IP
  2. 地址0xFFFFFFF0, CS=0xF000, 段基址地址=0xFFFF0000, IP=0xFFF0, 寻址方式: 段基址地址+IP

为什么同样CPU处于实模式寻址方式却不一样?这还得看Intel的定义

图片来源:member82 2.png

也就是说对于32位CPU在通电之后CS寄存器的初始值发生改变后才使用第一种寻址方式,否则用第二种寻址方式。 确定地址后还有一个问题,对于计算机来说内存条里的是内存,ROM里的也是内存,这些内存都有自己的地址,比如内存条有0x100地址,ROM里也有0x100地址,那么CPU拿着个地址到底要去哪里呢?其实CPU眼里地址是统一管理的:

图片来源: initroot

4.png

将不同的物理内存对应不同的一段连续地址空间,CPU在地址空间寻址而不是进入具体的内存设备中寻址。 如上图以32位CPU为例(最大寻址能力4GB),BIOS所在EPROM被编址到可寻址的内存最高处0xFFFFFFFF(4GB)向下扩展和映射到0xFFFFF(1MB)处向下扩展,此时CPU根据0xFFFFFFF0取到的指令就是BIOS指令,该指令是一个长跳转指令(此时会改变CS寄存器的值,回顾上面两个寻址方式)跳转到F000:E05B

当CPU第一条指令的地址为0xFFFF0时该地址存放的指令也是跳转到F000:E05B的跳转指令

为什么非得跳转呢?在16位CPU时代BIOS被编址到1MB内存地址空间的最高64KB中,位于0xFFFF0处的跳转指令指示CPU跳转到BIOS主程序处开始执行。在32位CPU时代如果BIOS还是编址到相同的位置就把内存隔断了,所以把BIOS编址到4GB内存最高高处,映射到1MB内存低处,通过0xFFFFFFF0处的跳转指令跳转到相同地址,回到早期CPU的控制流保证向前兼容(如果是64位CPU会怎样呢?)

BIOS登场

BIOS作为一个基本输入输出系统对计算机的启动起着至关重要的作用,其代码被写入到主板上的ROM中,CPU被唤醒后首先执行BIOS的代码。

BIOS首先执行硬件自检(Power-On Self-Test),检查包括显卡,主板等,有时我们开机会听到蜂鸣声就是检测到硬件异常BIOS发出警报然后启动就失败了

自检完成后BIOS就会把下一阶段的程序载入进来由它对系统的启动进行引导,毕竟不同的系统有很多BIOS不可能为所有系统做适配,所以让系统的编写者自己写引导程序,BIOS只负责把引导程序载入进来

BIOS会根据设定好的启动顺序到存储介质中寻找下一阶段的程序,启动顺序的信息一般存储在CMOS中,不过我们也可以在启动的时候进入BIOS中设置

图片来源: 阮一峰

6.jpg

如上图装过系统的同学肯定不陌生,我们都会在这里设置选择制作好的U盘启动盘,然后进入到PE里就可以装系统了。

BIOS会根据这个顺序读取存储介质中(一般都是硬盘)第一个扇区512字节的内容,如果最后两个字节的值是0x550xAA则表明该扇区是引导扇区,否则按顺序查找下一个存储介质。找到引扇区后将其读入内存0x7C00处(为什么是这里?),这些数据就是下一阶段的启动程序,然后设置CS=0x07C0IP=0x0000让CPU到该处取指执行

Bootset

被BIOS读入内存的那部分数据其实主要是被称作Bootset.s的汇编程序,从它开始就是属于操作系统的部分了

Bootset会将自己从0x7C00处开始的256个字节搬到0x90000处(为什么要搬?),简单讲就是根据Linux协议规定它就应该在那一片

图片来源: 李志军 image.png

搬完自己后Bootset就将操作系统的另一个模块Setup搬进内存和自己挨着(int 0x13是BIOS读磁盘的中断)

图片来源: 李志军 image.png

搬完Setup后Bootset就通过中断在屏幕上显示字符,例如我们开机会看到的 "系统正在启动"之类的字样

图片来源: 李志军 image.png

最后Bootset还要把最后一个模块System搬进内存和Setup挨着,真是个勤劳的搬运工,最后的内存布局如下:

图片来源: 李志军 image.png

上图中System模块中间有个表示磁道的标志,因为System有可能很大要跨磁道才能放得下,Bootset干完苦力活后就该把控制权交给Setup

Setup

Setup要做的就是为操作系统收集硬件数据存到内存中,然后再当一个搬运工把System模块搬到内存0地址处

图片来源: 李志军

image.png

图片来源: 李志军

image.png

为什么不在Bootset中就直接把System搬过去?因为BootsetSystem是从磁盘到内存,需要磁盘中断依赖BIOS放在0地址处的中断向量表SetupSystem是从内存到内存不需要中断,并且Setup后面会创建新的IDT(Interrupt Descriptor Table)GDT(Global Descriptor Table),这两张临时表用于保护模式下的地址翻译和中断。

图片来源: 李志军 image.png

搬完SystemSetup就把CRO寄存器最后一位置为1使系统进入保护模式,该模式下寻址是根据CSGDT获得基址加上IP,最后Setup通过jmpi 0 8(保护模式下该指令才能正确跳转)跳转到0地址处控制权移交System

图片来源: 李志军

image.png

System

System中第一个执行的是Head.s,如果说Setup是进入保护模式,那么Head就是保护模式下的初始化

Head做一下设置系统栈、重新建立IDTGDT等初始化工作

图片来源: 李志军 image.png

Head会将Main函数需要的参数地址等数据放入栈中,设置完页表后就通过ret指令跳转到Main.c中执行(此处就是C语言代码了)

图片来源: 李志军

image.png

Main主要的工作也是各种初始化包括内存、时钟、缓冲区等等,初始化完成后我们就可以进入系统了。Main是一个死循环函数,所以我们的系统永远不会退出。

图片来源: 李志军

image.png

参考资料