主题
第 1 部分 — 从电源按钮到内核的第一次呼吸
您按下电源按钮。一秒钟后,一堵文字墙滚动而过,或者一个徽标淡入,最终出现了 Linux。中间发生的事情不是魔法。这是微小程序和非常字面意思的 CPU 之间的仔细握手。这部分遵循握手,直到 Linux 内核内的第一行 C 代码运行。
第一个指令
当电源稳定时,CPU 会将自身重置为一种称为真实模式的微小老式模式。真实模式可以追溯到最初的 8086 芯片。规则故意很简单。内存地址由 CPU 保存在称为寄存器的特殊快速存储中的两个值构建。您可以像这样组合线段和偏移量:
physical_address = (segment << 4) + offset如果您看到像 , 这样的数字,那就是十六进制。十六进制是以 16 为底。我们写在前面是为了明确这一点。 每天计数是 16。 是 1 兆字节。十六进制与硬件存储位的方式非常吻合,这就是为什么你在低级代码中随处可见它的原因。0xFFFFFFF0``0x``0x10``0x100000
复位后,CPU 立即跳转到一个称为复位向量的特殊地址,位于 。将其视为一个永久书签,上面写着“从这里开始”。该地址几乎没有空间,因此制造商在那里进行了一次远(长)跳跃,将控制权传递给主板上的固件。0xFFFFFFF0
小解释器:注册寄存器是 CPU 内部的一个小插槽。它包含 CPU 现在使用的数字。CS 和 IP 等名称是寄存器名称。CS 的意思是“代码段”,它标记指令的当前邻域。IP 的意思是“指令指针”,它标记接下来是哪条指令。
BIOS 和 UEFI
固件是内置于电路板中的小型入门程序。
BIOS 代表基本输入输出系统。这是旧的风格。BIOS 执行称为 POST 的快速健康检查,查看启动顺序,并尝试每个设备。如果它找到一个磁盘,其第一个 512 字节扇区以标记字节结尾,则 ,它会将该设备视为可启动设备。BIOS 将该扇区复制到内存并跳转到那里。该扇区很小,因此它通常只知道如何加载下一个更大的部分。0x55``0xAA``0x7C00
UEFI 是现代替代品。它仍然启动机器,但它直接理解文件系统,并且可以加载更大的引导程序,而无需旧的“第一扇区”舞蹈。UEFI 还将更丰富的信息传递给作系统。不同的路径,相同的目标:手动控制可以加载 Linux 的启动程序。
认识引导加载程序
引导加载程序是使作系统就位的引座器。GRUB 是 PC 上的热门选择。它读取其配置,显示一个菜单(如果您安装了菜单),并将 Linux 内核加载到内存中。Linux 内核文件实际上包含两件事:
- 一个仍然在真实模式下运行的小型安装程序
- 稍后将解压的较大压缩内核
GRUB 还用有用的事实填充了一个称为 setup 标头的小结构:它放置内核的位置,命令行所在的位置,initrd 在哪里(如果有的话)。然后它跳入设置程序。
设置程序打造安全室
在 Linux 可以做任何有趣的事情之前,安装代码会创建一个可预测的工作区。
它排列段寄存器,以便内存副本每次都以相同的方式运行。您将在此处看到的名称是 CS 代表代码、DS 代表数据和 SS 代表堆栈。它还清除称为“方向标志”的单个 CPU 位,以便复制指令通过内存向前移动。
它创建了一个堆栈。堆栈是一个后进先出的工作台,函数在其中存储临时值。SS 表示堆栈使用哪个段。SP 是指向堆栈当前顶部的指针。
它清除了一个名为 BSS 的区域。BSS 是必须从零开始的全局变量所在的位置。C 代码假设 BSS 为零。安装程序在整个跨度上写零以遵守该承诺。
如果您传递了内核命令行,安装代码还会对串行端口进行编程,以便它可以打印非常早期的消息。当图形尚未准备好时,这很有用。earlyprintk
最后,安装程序询问固件“我们真正有多少可用 RAM,漏洞在哪里。在旧的 BIOS 上,这是一个人们经常昵称为 e820 的电话,它返回可用和保留范围的简单列表。内核将使用该列表来避免踩到固件的脚趾。
完成此作后,安装代码调用其第一个 C 函数,字面意思是 .在这一点上,我们仍然处于小旧的真实模式。下一份工作是离开它。main
小解释器:打断中断是一种硬件或软件“对不起”,它暂停 CPU 正在做的事情,并为紧急情况运行一个小型处理程序。计时器滴答是中断。按键是中断。这里有两种口味。可屏蔽中断遵循您的规则,并且可以暂时阻止,这样它们就不会在微妙时刻触发。不可屏蔽的中断(通常称为 NMI)总是切入,因为它们通常会报告严重的硬件问题。我们将在切换模式时控制两者,因此中途没有什么让我们感到惊讶的。
第 2 部分 — 离开实模式,逐步完成 32 位土地,到达 64 位
PC 上的现代 Linux 以长模式运行,即 x64_86 的 64 位模式。您不能直接从真实模式跳到那里。路径是实模到保护模式,然后是保护模式到长模式。这部分涵盖了这条路径并解释了途中的词汇。
保护模式,没有行话的阴霾
保护模式是为突破 32 年代的限制而引入的 1980 位世界。它增加了两个中心思想。
全局描述符表 (GDT) 是段描述的简短列表。描述说“这个部分从这里开始,涵盖这么多,并被允许做这些事情。Linux 让这一点变得简单。它使用平面模型,这意味着基数为零,大小覆盖整个 32 位空间。当一切都平坦时,地址看起来又像普通数字。
中断描述符表 (IDT) 是紧急呼叫的“电话号码”目录。如果中断到达,CPU 会查找 IDT 中的条目并跳转到其中列出的处理程序。在切换过程中,我们加载一个很小的占位符 IDT,因为无论如何我们都会阻止中断。一旦真正的内核负责,功能齐全的 IDT 就会稍后到达。
小心的开关
设置代码首先关闭嘈杂的部分。它通过单个指令禁用可屏蔽中断。它使旧的 PIC 芯片静音,因此硬件中断暂时被完全阻止。它开通了 A20 线。这是一个历史怪癖。早期的 PC 使地址换行为 1 兆字节。打开 A20 会删除该换行,因此较高的地址会按预期工作。它重置数学协处理器,使浮点状态干净。
然后它加载一个很小的 GDT,其中只有我们现在需要的东西和一个很小的 IDT。最后,它在名为 CR0 的控制寄存器中设置一个名为 PE 的单位,并执行远跳转。该跳转会从 GDT 重新加载代码段并锁定在保护模式下。它重新加载数据和堆栈段,并修复堆栈指针以匹配新的平面世界。
我们现在处于 32 位保护模式。
微型解释器:控制寄存器CPU 有一些用于开关的特殊寄存器。CR0 打开保护模式。CR3 保存页表顶部的地址,我们稍后将需要该地址。CR4 支持一组扩展功能,例如更大的页表条目。
为什么我们还没有完成
Linux 想要 64 位。这就是长模式。需要两件事。
分页必须打开。分页是虚拟地址和物理地址之间的转换器。程序使用虚拟地址。硬件读取和写入物理内存。页表在称为页面的固定大小块中映射到另一个。在 PC 上,普通页面为 4 KB。还有更大的页面。在启动初期,内核使用 2 兆字节的页面来快速描述内存不足。
必须将名为 EFER 的专用寄存器中名为 LME 的单个位设置为允许长模式。EFER 是一个特定于模型的寄存器,这是一种奇特的说法,即“用于某些 CPU 功能的寄存器”。
构建足够的分页
32 位序言构建了一小组页表,上面写着“对于这个区域,虚拟等于物理”。这称为身份图。安全地打开分页就足够了。
为了实现这一点,代码在 CR4 中启用了 PAE,因此使用了更大的条目。它构建了一组最小的表,这些表覆盖了 2 MB 块中的低内存。它将顶表的地址写入 CR3。寻呼现已准备就绪。
最后,它在 EFER 中设置 LME,并执行远返回到写为 64 位代码的标签中。长模式现已激活。段仍然是“平面的”,但地址和寄存器是 64 位宽的。
为什么要额外照顾在实时系统运行时切换模式就像在滚动时更换汽车轮胎一样。该代码阻止中断,准备所需的最少表,翻转位,然后才邀请中断回来。缓慢而稳定可防止奇怪的半开关状态。
第 3 部分 — 解压真正的内核,修复地址,以及为什么 Linux 有时会自行移动
我们有一个 64 位 CPU,开启分页功能,内存中有一个压缩内核。现在,这个小的 64 位存根可以完成实际工作:如果需要,让开,解压缩内核,如果内核不在其默认位置,则修复地址,然后跳转。
扫清道路并设置安全网
存根首先确定它实际运行的位置。早期代码被链接,就好像它位于地址零一样,然后在运行时计算其真实基数。如果解压缩内核的计划目标与存根重叠,它会将自身复制到安全位置。
它清除自己的 BSS,以便全局状态从干净开始。
它加载一个带有两个处理程序的最小 IDT。一个用于页面错误,一个用于 NMI。当 CPU 找不到它刚刚尝试使用的虚拟地址的映射时,就会发生页面错误。在我们早期的身份映射世界中,微小的页面错误处理程序可以动态添加缺失的映射并继续。NMI 处理程序就在那里,因此当我们仍在启动时,不可屏蔽的中断不会使机器崩溃。
它还为接下来将接触的区域构建身份映射。这包括内核的未来主页、引导加载程序填写的小引导参数页面和命令行缓冲区。
解压缩 Linux...
一个通常命名的 C 函数接管。它为临时缓冲区留出一个小堆,打印经典行,并使用构建内核的任何算法解压内核。gzip、xz、zstd、lzo 等都插入到同一个包装器中。extract_kernel
当字节出来时,解压缩器读取内核的 ELF 标头。ELF 是可执行和可链接格式的缩写,既是一种文件格式,也是一种映射。它说明哪些块是代码,哪些是数据,以及每个块想要存留的确切位置。解压缩器将每个块复制到它所属的位置。
如果内核的加载地址与其构建时的地址不同,解压缩器会应用重定位。重定位是一种小型修复,用于调整包含地址的指针或指令。解压缩器会遍历这些列表并修补每个位置,以便它指向我们实际使用的地址空间中的正确位置。
当一切就绪后,解压缩器返回真实内核的入口点并跳转到那里,传递指向引导参数的指针。从那一刻起,你就进入了完整的内核。您遇到的第一个函数是 ,大初始化开始了。start_kernel
为什么内核有时会故意移动自己
您可能会在内核日志中看到提到的 kASLR。这代表内核地址空间布局随机化。这个想法很简单。如果攻击者不知道内核在内存中的位置,某些攻击就会变得更加困难。
在启动初期,如果启用了 kASLR,解压缩器会随机选择两个“基数”:
- 物理底座,即字节位于 RAM 中的位置
- 虚拟基,即内核在设置完整分页后将使用的起始虚拟地址
它如何在不破坏任何东西的情况下选择
它建立了一个请勿触摸列表。这包括解压缩器本身、压缩映像、初始内存磁盘、启动参数页面和命令行缓冲区。它还可以包括您在命令行上使用选项保留的范围。memmap=
它从固件扫描内存映射以查找大的可用区域。对于每个空闲区域,它计算了有多少个大小合适的对齐“插槽”可以容纳。它使用它拥有的最佳早期熵源绘制一个随机数。在现代 CPU 上,这可能是硬件随机指令。它将数量减少到插槽总数并选择匹配的插槽。这成为物理基础。虚拟基地的选择方式相同,但在内核的虚拟地址窗口内。
如果不存在合适的地址,代码将回退到默认地址并打印一个小警告。如果传递命令行,则设计上会跳过随机化步骤。nokaslr
可以添加书签的快速词汇表
**十六进制。**以 16 为基数的数字 . 是 16 岁。 是 1 兆字节。十六进制干净地映射到位,这就是低级代码使用它的原因。0x``0x10``0x100000
**注册。**CPU 内部的一个小插槽,现在有一个数字。示例:CS、DS、SS、IP、SP。
**分段和偏移。**用于构建实模地址的两部分。物理地址等于分段乘以 16 加上偏移量。
**BIOS。**较旧的固件,用于启动计算机、检查硬件并将第一个引导扇区加载到内存中。
**UEFI的。**现代固件,可理解文件系统并直接加载更大的启动程序。
**引导加载程序。**将内核放入内存并将有关系统的事实传递给它的引座器。GRUB 是一种常见的。
**叠。**后进先出的功能工作台。SS 选择其段。SP 指向当前顶部。
**BSS。**必须从零开始的全局变量存在的区域。内核安装代码在 C 运行之前清除它。
**中断。**从硬件或软件中快速“对不起”。CPU 暂停,运行一个小型处理程序,然后恢复。可屏蔽中断可以暂时被阻止。NMI 不能。
**GDT。**全局描述符表。区段描述符的简短列表。Linux 将其设置为简单的平面模型。
**IDT。**中断描述符表。中断处理程序的目录。早期启动使用最小的启动。完整内核稍后会安装真正的内核。
**A20线。**在旧 PC 上必须打开以正确地址超过 1 兆字节的历史交换机。
**保护模式。**引入 GDT 和 IDT 并允许分页的 32 位模式。
**长模式。**x64_86上的 64 位模式。需要分页并在 EFER 寄存器中设置名为 LME 的位。
**寻呼。**从虚拟地址到物理内存的转换器。使用页表实现。
**页表。**将虚拟页面映射到物理页面的数据结构。早期启动使用标识映射。普通页面为 4 KB。早期启动通常使用 2 MB 页面来快速覆盖地面。
**CR0、CR3、CR4。**控制寄存器。CR0 打开保护模式。CR3 指向页表的顶部。CR4 支持扩展功能,例如 PAE。
**埃弗。**一个特定于模型的寄存器,它保存长模式启用以及其他位。
**精灵。**内核的磁盘格式,带有内容所属位置的内置映射。
**搬迁。**当代码加载到与构建时不同的基础时,会调整地址的修复。
**kASLR。**在启动时随机化内核基地址,使利用更加困难。