在BSD内核启动之前:AMD64上的第一部分

2020-11-21 23:15:12

系统初始化是鲜为人知的利基领域之一,确切的细节在不同的平台,固件,CPU架构和操作系统之间存在很大差异,这使得学习这一切变得很困难。在系统启动阶段或如果操作系统无法启动,则很少与负责启动的代码有关。在大多数情况下,这是由于其他因素引起的,例如引导媒体或BIOS配置。但是,了解早期初始化过程可能有助于调试或熟悉新的平台或硬件。

在本文中,我将逐步完成早期的内核初始化过程,以定义该术语的含义。系统初始化是一个广泛的主题,从平台的硬件设计一直到操作系统的典型功能(例如处理I /)。 O操作。不可能在文章范围内充分涵盖整个主题。在第一部分中,我将介绍著名的AMD64:64位平台。我将重点介绍初始化过程中非常有趣的部分-内核的早期初始化。稍后,我将其与ARM64进行比较。在这两种情况下,我都将在NetBSD的上下文中讨论该主题,该操作系统以其可移植性而闻名。

CPU的起始点称为复位向量:CPU引导,然后提取并执行位置0xFFFFFFF0处的第一个物理地址。引导加载程序必须始终在最后的前16个字节中包含到初始化代码的跳转。 CPU处于实模式的一种变体,称为实模式。带段的16位寻址最多可寻址1 MiB内存。复位后,CS描述符缓存基字段包含一个特殊的固定32位值:0xFFFF0000。(在实模式下,用户只能更改低16位CS的位;上半部分(也称为基数)在重置和隐藏时设置)。使用此技术,指令指针相对于物理内存的最后64 KiB片段进行寻址,通常将其连接到只读闪存内存,平台固件(BIOS / UEFI)的一部分所在的位置。

BIOS(基本输入/输出系统)是用于旧版平台初始化固件以及操作系统与平台之间的接口的术语,主要用于与IBM PC兼容的计算机,例如个人计算机或服务器类型的计算机。 UEFI(统一可扩展固件接口)是一个通用规范,而不是特定的实现,并且与BIOS相似,它定义了操作系统和平台固件之间的接口。 UEFI的目标是取代传统的接口,并且被设计为通用的,可以应用于PC或服务器以及嵌入式设备。此新标准旨在克服旧标准(例如16位处理器模式)的局限性。 1 MB的可寻址空间,或可以从中引导操作系统的最大硬盘驱动器大小。它还提供了安全启动或UEFI运行时服务等新功能。描述UEFI及其与BIOS的区别不在本文讨论范围之内,但重要的是要知道基于BIOS和UEFI的固件都将执行平台初始化,并且UEFI和BIOS的加载方式有所不同。较新的标准允许更高级的功能,例如BIOS在引导扇区上运行的GPT分区布局。对于本文,我们将从基于主启动记录(MBR)的旧启动过程开始。如果需要,将来可以扩展UEFI的主题。

当CPU复位后启动时,大多数平台硬件尚未准备就绪:尚未检测和初始化作为DIMM模块连接的系统内存,定时器和中断尚未准备好,PCI总线也未运行。进行初始化,这是平台固件的基本作用。好奇的读者可以在最小化引导加载程序的英特尔®架构中找到有关初始化过程的详细说明,这里我仅指出关键功能。一开始,固件初始化代码需要初始化CPU,而平台芯片组只能初始化然后准备好要工作的存储器。在称为后存储器初始化的阶段中操作存储器之后,固件会将自身从慢速闪存复制到系统DRAM。初始化代码只有在将软件环境准备为堆栈或CPU模式后才能开始执行。当CPU跳转到DRAM中小于1MB的内存地址(此内存区域历来是为此目的保留的)时,它还有很多事情要做在最新阶段,将初始化IO设备并枚举PCI总线。完成此操作后,初始化代码将搜索要引导的旧版操作系统,将MBR扇区从磁盘加载到内存中并执行它。

BIOS从硬盘开始加载第一个扇区,称为MBR(512字节)。该区域必须以魔术数字(也称为签名)0xAA55结尾。该扇区包含的指令必须将更多的扇区加载到内存中以执行更高级的引导程序,原因很简单:大小和512个字节可以容纳多少指令。只有这样,我们才能拥有更复杂的程序查找并执行操作系统的内核。在描述执行内核并使之在长模式下运行的过程之前,我们需要了解典型UNIX内核的起点。

两种最常见的可执行文件格式是ELF(可执行文件和可链接格式)和PE(便携式可执行文件)。在UNIX环境中,ELF是程序二进制文件的典型格式,而PE在Windows上被广泛使用,这并不奇怪。向读者表明NetBSD内核也是ELF可执行文件。

我们习惯于认为程序以某种主要功能开始。我们中那些研究过库或执行流程的人可以回忆起一个较低级别的_start函数,该函数在程序加载到内存时被调用。在ELF可执行文件中,该程序实际上从文件头内定义的入口点(入口点地址)开始。我们可以使用内核二进制文件上的readelf程序轻松地验证此声明:

$ readelf -h ./netbsdELF标头:Magic:7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 00类:ELF64数据:2的补码,小端版本:1(当前)OS / ABI:UNIX -System V ABI版本:0类型:EXEC(可执行文件)机器:Advanced Micro Devices X86-64版本:0x1入口点地址:0xffffffff80209000 PDPT-> PD-> PT。在我们填充它们之前,必须将它们擦除,在完成清理之后,我们到达了为页表设计的内存段的末尾。因此,我们可以从PT(L1)一直到PML4(L4)填充它们。内核的某些部分(例如内核堆栈或内核代码)必须存在并映射到内存中,因此,基于已知的内存映射,我们需要如下图所示,将64位虚拟地址分解为页表:

在页表被映射之后,我们可以启用PAE(它们在控制寄存器中表示为标志)。为此,我们需要在EFER中设置LM位(寄存器中的第9位)。这不会将CPU转换为Long Mode,而是要执行跳转指令(这是在Intel CPU上的两种模式之间进行切换的一般方法)。在将CPU切换为Long Mode之前,我们需要将控制寄存器3指向PML4顶部条目的地址。现在,我们准备启用分页。在将正确的标志写入CR0之后,为了使其生效,我们需要执行跳转指令。

切换之后,CPU处于长模式的一种变体(称为兼容模式),我们需要再执行一次操作。要进行切换,我们需要加载准备好的全局描述符表(GDT)并执行长跳转。代码段和描述符在平面64位模式中仍然存在,因为它们建立了处理器执行特权级别以及操作模式(请参阅《 AMD64体系结构程序员手册》第2卷的4.8.1-4.8.2)。我们加载GDT,将准备好的代码段设置为该代码段并执行跳远。

经过漫长的跳跃,我们终于进入了长距离模式!现在,只需几个步骤就可以调用main,但是我们将在本文的下一部分中讨论它们。

[5]通过在OsDev上搜索,可以更详细地了解大多数主题。