上电前:BIOS chip / boot ROM
BIOS chip 是主板的芯片组的一块芯片,主要有两个值得关注的性质:首先,它(或其中一部分)内存映射到处理器的地址空间,使得处理器可以用和访问内存同样的方式访问 BIOS chip。CPU通过将指令指针指向 BIOS chip 中的代码,执行。其次,BIOS chip 存储了 CPU 上电后最先执行的指令。一个典型的 BIOS 包含 flash 描述符(BIOS chip 的内容表)、BIOS 区(被执行的第一条指令)、Intel ME(Management Engine)和以太网口。BIOS chip 在系统的多个部分间共享,而非在为 CPU 独占。
上电后
现代 Intel 芯片含有 Intel Management Engine。一旦上电,先启动 Intel ME。它执行自身的初始化:
- 读取 BIOS 的 flash 描述符,找到 Intel ME 区
- 读取代码并设置数据
按下电源键后,CPU 启动。在多核系统中,往往先启动一个 CPU(Bootstrap Processor a.k.a BSP)。刚启动时,CPU 进入 16-bit 实模式,指令指针指向 0xffff:0000(CS:IP的复位向量)。
0xf_fff0 作为 CPU 复位地址,指向 boot ROM 的 BIOS 区。BIOS 区中的固件被称为启动固件,例如 UEFI 实现,legacy 实现。
启动固件要做的第一件事是切换到保护模式,,开启分段并区分分段权限。然而启动固件仅包含一个段,被称为 flat mode。
早期初始化
在开始时,DRAM 还不可用。DRAM 初始化是启动固件的一个主要目标。但是在初始化 DRAM 前需要做些准备。
打微代码(Intel CPU 转译为内部 RISC 的转译表)补丁使 CPU 功能正确。Intel 为不同型号的 CPU 持续发布微代码补丁。启动固件在启动过程的最开始打补丁,接下来初始化南桥(I/O Controller Hub / Peripheral Controller Hub)部分。例如 ICH 包含看门狗定时器的话,在初始化过程中需要关闭。
当然这些初始化都由启动固件中的代码完成。大多数我们熟悉的代码需要依赖于栈,但当 DRAM 没有初始化,内存尚且不可用。因此,此时的代码不能用到栈,不管是直接写 x86 汇编(如 coreboot 那样)还是写 C 并使用专用的 ROMCC 编译器生成无堆栈代码。这种限制使得 ROMCC 不能编译出我们想执行的所有代码,我们需要这些代码尽快建立栈。
因此下一步是设置 cache as RAM。启动固件通过配置 CPU cache 使得它可以暂时用作内存,使得固件可以运行需要堆栈支持的程序,但是仍受限于栈空间大小和可用内存大小。
内存初始化和 Intel FSP(Firmware Support Package)
Intel 平台上,内存初始化由 Intel FSP 完成。Intel FSP 由 Intel 以二进制形式提供,完成了许多配置 Intel 处理器启动的复杂工作,包括内存初始化。基本上,它具备一组三级 API。启动固件与 FSP 的交互通过配置好参数和返回地址并跳转到 FSP 对应段。 FSP 段根据传递的参数执行后返回到启动固件。
- TempRamInit(): 在这一阶段,对 RAM 做部分初始化并手动控制返回到启动固件。启动固件发起一些动作后再进入下一阶段。这是因为下一阶段进行芯片组和内存初始化比较耗时。启动固件可以同时进行启动硬盘等耗时操作。
- FspInitEntry(): 这一阶段后 DRAM 才真正被设置好,同时 FSP 也会初始化芯片组的其它部分,如 PCH 和 CPU。初始化完成后,重新返回到启动固件。在初始化完成的内存支持下,进入 FSP 下一阶段前,启动固件完成接下来的其它初始化,被称为“有内存后的初始化”。
- NotifyPhase(): 这一阶段执行的操作与平台有关,但常常包含 PCI 设备枚举。
后内存初始化 After Memory Init
一旦 DRAM 配置好,启动固件会通过内存别名(“memory aliasing”)将自己复制到 DRAM 中,1MB 以内的读写访问被重定向到 DRAM 内。
其后,一些平台特定的初始化,如 GPIO 配置,重新打开(在前内存初始化关闭的)南桥看门狗定时器,配置本地先进可编程中断控制器(Local Advanced Programmable Interrupt Controller,LAPIC)作为启动中断的基础。LAPIC 存在于每个处理器中(而非处理器核中),它决定了每个中断如何分配给特定处理器核。I/O APIC(IOxAPIC)在南桥中,所有处理器共享一个IOxAPIC。功能上,一个可编程中断控制器在实模式作为一个包含 256 个中断向量(中断处理函数入口地址)的中断向量表使用。在保护模式时,则作为中断描述符表。
接下来,启动固件根据平台和固件差异设置不同的定时器。Programmable Interrupt Timer(PIT)是系统定时器,位于南桥的 IRQ0。High Precision Event Timer(HPET)也在南桥,往往由操作系统初始化,作为精度更高的时钟。南桥中的 Real Time Clock(RTC)是记录真实世界时间的定时器。在配置好所有定时器后,启动固件进一步初始化高速缓存,对不同地址段配置不同的缓存特性。
其它处理器配置,I/O 设备 和 PCI
最后,BSP 运行 CPUID 指令,找到同一封装中的其它处理器核(Application Processors,AP)。然后用它的 LAPIC 向它发送SIPI 中断请求。每个 SIPI 指向收到中断请求的 AP 应该开始执行的物理地址,在开始执行时,每个 AP 都是从实模式开始,因此 SIPI 地址须小于实模式最大可寻址范围 1 MB。完成初始化后,每个 AP 很快执行 HLT 指令进入停止(halt)状态,等待 BSP 中接下来的指令实行。然而,在 OS 得到控制权前,AP 应该处于”waiting-for-SIPI“态。BSP 通过发送一组处理器间中断来保证该状态。
接下来配置 I/O 设备,如 Embedded COntroller 和 Super I/O 以及 PCI。PCI 初始化一般分为两步:
- 枚举所有 PCI 设备
- 为每个 PCI 设备分配相关资源
PCI 是一个层次化总线协议,每一层的叶节点为 PCI 设备或连接下层 PCI 总线的 PCI 总线桥。CPU 通过读写 PCI 寄存器组与 PCI 通信。PCI 需要的资源是内存地址段、I/O 地址段和 IRQ 分配。CPU 通过读写 PCI 设备的基地址找出地址范围和类型(Memory-mapped 或 I/O)。IRQ 则由主板设计决定。
在 PCI 枚举期间,启动固件也读取 Option ROM 寄存器。如果该寄存器不空,则内容为 Option ROM 的地址。Option ROM 一般物理上在 PCI 设备中。例如,网卡可能包含 Option ROM 保存 iPXE 固件。Option ROM 读取完成后,它被加载到内存中执行。
将控制权转给操作系统加载器
在交出控制权给下一阶段加载器(OS loader 如 GRUB2 / LILO),启动固件在内存中设置一些操作系统的信息。该信息包含 Advanced Configuration and Power Interface(ACPI)表和内存映射表(memory map)。内存映射表高速操作系统不同的地址范围和目的。这些区域包括 OS 可用的地址空间,ACPI 相关的地址范围,保留(不能被操作系统使用),IOAPIC(由IOAPIC使用),LAPIC(由LAPIC使用)。启动固件也配置 System Management Mode(SMM)中断的响应程序,SMM 是 Intel CPU 的工作模式,如 实模式、保护模式,以及长模式(64位)。SMM 中断有多种触发源,如 CPU 达到特定的温度水平。此外,启动固件锁定部分寄存器和 CPU 功能以免被操作系统改变。
实际的控制转换一般是到 jmp 到特定的内存地址。操作系统加载器会基于自身配置执行,并将控制转交给操作系统。以 Linux 为例,系统通常在被加载前被打包为一个大的 zImage 文件。