编程笔记

lifelong learning & practice makes perfect

译|Linux 启动过程:从按下电源到内核

Part 1 — 从按下电源按钮到内核的第一步

按下电源键,转瞬之间字符瀑布奔涌或 Logo 悄然浮现,Linux 随之亮相。看似魔法,实则是一连串微小代码与CPU之间的精密握手。下文循着这场握手的轨迹,一路追踪到 Linux 内核首行 C 代码的登场。

最初的指令(The very first instruction)

电源稳定后,CPU 会把自己拉回一个迷你而古老的模式——实模式(real mode)。它源自最早的 8086 芯片,规则被故意设计得极其简单:内存地址由寄存器里的一对值拼成,即段(segment)与偏移(offset):

physical_address = (segment << 4) + offset

你会看到类似 0xFFFFFFF0 的数字,这是十六进制(hex),以 0x 作为前缀。0x10 是 16(十进制),0x100000 是 1MB。十六进制与硬件的位存储非常契合,因此在底层代码中随处可见。

复位后,CPU 跳转到一个特殊地址——复位向量(reset vector)0xFFFFFFF0。你可以将其理解为一个永久书签:“从这里开始”。这个地址的空间极其有限,所以主板厂商通常在那里放一个“远跳转(far jump)”,把控制权交给主板上的固件。

小知识:寄存器(register)是 CPU 内部的一个小槽位,用来暂存当前正在使用的数值。诸如 CSIP 就是寄存器名:CS 表示“代码段(code segment)”,标记当前指令的所在“邻域”;IP 表示“指令指针(instruction pointer)”,标记下一条指令的位置。

BIOS 与 UEFI

固件(firmware)是烧在主板上的一个小型引导程序。

  • BIOS(Basic Input Output System)属于“老派”做法。一通上电自检(POST)后,它按预设顺序挨个试探设备;只要发现某块磁盘的头 512 字节最后躺着签名 0x55AA,就认定“这货能启动”。于是 BIOS 把这扇区搬到内存 0x7C00,然后跳过去继续执行。扇区容量极小,通常只够再拉一段更大的加载器进来。
  • UEFI 是现代替代者。它同样负责启动机器,但它可以直接理解文件系统,并能加载更大的引导程序,无需旧式“首扇区”舞步。UEFI 还能向操作系统传递更丰富的信息。路径不同,目标一致:把控制权交给能够加载 Linux 的引导程序。

引导加载器(Meet the bootloader)

引导加载器是把操作系统“请进场”的门童。GRUB 是 PC 上的常见选择。它读取自身配置,显示菜单(如果你配置了),并将 Linux 内核加载到内存中。内核文件实际上包含两部分:

  • 一个仍在实模式下运行的小型 setup 程序;
  • 更大的压缩内核,稍后会被解压。

GRUB 还会往一个叫 setup header 的小结构里填好关键信息:内核被摆在哪、命令行丢在哪、有没有 initrd 等。填完便直接跳去 setup 程序继续干活。

setup 程序建立“安全工作区”(The setup program makes a safe room)

在 Linux 做任何有趣的事前,setup 代码需要创建一个可预测的工作环境:

  • 对齐段寄存器,使得内存拷贝行为每次都一致。这里你会看到 CS(代码段)、DS(数据段)与 SS(栈段)。同时清除一个叫“方向标志”的 CPU 位,让拷贝指令向前移动。
  • 建立栈。栈是一个“后进先出”的工作台,函数在此存放临时值。SS 指定栈所用的段,SP 指向当前栈顶。
  • 清零 BSS。BSS 区域用于存放需要从零开始的全局变量。C 代码假定 BSS 为零,setup 程序会把这个跨度全部写成零以兑现承诺。
  • 如果你在内核命令行传了 earlyprintk,setup 代码还会编程串口以打印非常早期的消息——当图形尚未就绪时尤为有用。
  • 最后,setup 程序向固件询问“我们到底有多少可用 RAM,以及空洞在哪里”。在传统 BIOS 上,这个调用常被昵称为 e820,它返回一份简单的可用与保留范围列表。内核会用这份列表来避免踩到固件的脚趾。

完成这些后,setup 代码调用它的第一个 C 函数,名字就叫 main。此时我们仍在这个古老而小巧的实模式中……

Part 2 — 离开实模式,踏过 32 位,抵达 64 位

现代 Linux 在 PC 上运行于 long mode(64 位,x86_64)。你无法从实模式(real mode) 直接跳到 long mode,路径是:real mode → protected mode → long mode。本部分解释这条路径与相关术语。

Protected mode(保护模式),尽量不拗口

保护模式是为摆脱 1980 年代限制而引入的 32 位世界,它增加两件核心工具:

  • GDT(Global Descriptor Table,全局描述符表):一张短短的段描述列表。每一项描述“该段从哪里开始、覆盖多大范围、允许做什么”。Linux 保持简单,采用扁平模型(flat model):基址为 0,大小覆盖整个 32 位空间。扁平后,地址看起来又像普通数字。
  • IDT(Interrupt Descriptor Table,中断描述符表):一份“紧急呼叫”的目录。若有中断到来,CPU 在 IDT 中查找并跳到登记的处理程序。切换过程中我们先加载一个极小的占位 IDT,因为即将屏蔽中断;真正的、功能完整的 IDT 在进入“真实内核”后才安装。

谨慎的切换(The careful switch)

setup 代码先把“吵闹”部分关掉:用一条指令禁用可屏蔽中断(maskable interrupts),让老式 PIC 芯片安静,以确保硬件中断暂时完全被阻断;打开 A20 line(历史性开关),避免地址在 1MB 处环回;重置数学协处理器,让浮点状态干净。

随后加载一个仅含必需项的迷你 GDT 与迷你 IDT。最终在 CR0 中设置 PE 位(Protected Mode Enable),并执行一次 far jump。这个跳转会从 GDT 重新加载代码段,锁定进入 protected mode;同时重载数据段与栈段,并修正栈指针以匹配新的扁平世界。

我们现在处于 32 位的 protected mode。

小知识:控制寄存器(control registers)

  • CR0:打开 protected mode 的总开关。
  • CR3:指向页表顶层的地址,我们马上会用到。
  • CR4:启用扩展特性,例如更大的页表项(包括 PAE)。

为什么还没有结束

Linux 想要的是 64 位,也就是 long mode。还需要两件事:

  • 必须开启 Paging(分页)。分页是虚拟地址与物理地址之间的翻译器。程序使用虚拟地址,硬件读写物理内存。页表以固定大小的页(典型 4KB)进行映射。早期启动时,内核常用 2MB 大页来快速描述低端内存。
  • 在 EFER(一个 model-specific register,模型特定寄存器)中设置名为 LME 的位,以允许 long mode。

构建“恰到好处”的分页

32 位序幕会建立一套小型页表,表达“在这片区域,虚拟地址等于物理地址”。这叫 identity map(同址映射),足以让分页安全地开启。

为此,代码在 CR4 中启用 PAE,使得使用更大的页表项;构建覆盖低端内存的最小页表,用 2MB 页快速铺设;将顶层页表地址写入 CR3,分页就绪。

最后在 EFER 中设置 LME 位,并通过一次 far return 跳入以 64 位语法编写的标签。long mode 现已激活。段仍旧“扁平”,但地址与寄存器都变为 64 位宽。

为什么要如此小心?在一个活着的系统里切换模式像是在行进中换轮胎。代码先屏蔽打断、准备最小所需的表、再翻转关键位,最后才重新允许中断。稳妥的顺序可以避免半切换的奇怪状态。

Part 3 — 解包真正的内核、修正地址,以及内核为何会“主动搬家”

我们已有 64 位 CPU、Paging 已开启,内存中放着一个压缩的内核。现在由一个小小的 64 位 stub 来做实际工作:如有需要先挪开自己、解包内核、若内核不在默认位置则修正地址,最后跳转。

清出路径并设好安全网

stub 首先搞清楚它到底运行在何处。早期代码在链接时好像自己位于地址 0,运行时再计算真实基址。如果解压后的内核计划目的地会与 stub 重叠,它会先把自己复制到安全位置。

它清零自己的 BSS,让全局状态从干净开始。

它加载一份极简 IDT,仅有两个处理程序:一个处理 page fault(页故障),一个处理 NMI(不可屏蔽中断)。页故障发生在 CPU 试图使用的虚拟地址没有对应映射时。在我们早期的 identity map 世界里,这个小小的页故障处理器可以即时补上缺失的映射并继续运行。NMI 处理器则确保在我们尚在“拉起系统”的阶段,突发的不可屏蔽中断不会让机器崩溃。

同时它还为接下来会触及的区域建立同址映射,包括内核的未来驻留区、由引导加载器填充的 boot parameters(启动参数)页,以及命令行缓冲区。

解压 Linux…

一个常被命名为 extract_kernel 的 C 函数接管。它先划出一小块堆作为临时缓冲;打印那句经典的提示;随后用内核构建时选择的算法进行解压。gzipxzzstdlzo 等都通过同一个包装器接入。

字节解出后,解压器读取内核的 ELF(Executable and Linkable Format)头。ELF 既是文件格式也是地图:哪些是代码、哪些是数据、每块应该确切放到哪里。解压器按图将每个分块拷贝到其归属位置。

如果内核被加载到与其构建时不同的地址,解压器会应用 relocations(重定位)。重定位是对包含地址的指令或指针做的小修正。解压器遍历修正列表,把每个位置补丁到我们实际使用的地址空间中的正确指向。

当一切就绪,解压器返回“真正内核”的入口地址,并跳转过去,同时传入指向启动参数的指针。从那一刻起,你已经进入完整内核。遇到的第一个函数是 start_kernel,大型初始化随即开始。

为什么内核会主动“搬家”(kASLR)

你可能在内核日志中看到过 kASLR(Kernel Address Space Layout Randomization,内核地址空间布局随机化)。核心思想非常直接:如果攻击者不知道内核在内存中的确切位置,某些利用会变得更困难。

在启动早期,若启用了 kASLR,解压器会随机选择两个“基址”:

  • 物理基址:内核字节最终驻留的 RAM 物理地址;
  • 虚拟基址:当完整分页机制建立后,内核使用的起始虚拟地址。

它如何在不“踩雷”的前提下做选择?

  • 先建立一个“勿触列表”,包括解压器自身、压缩镜像、初始内存盘(initrd/initramfs)、启动参数页与命令行缓冲区;如果你在命令行传了 memmap= 选项,所保留的区间也会被纳入其中。
  • 扫描固件提供的内存映射,寻找足够大的空闲区域;对每个空闲区域,计算合适尺寸且对齐的“插槽”数;
  • 使用启动早期可用的熵源生成随机数(在现代 CPU 上可能是硬件随机指令);将随机数映射到插槽总数,挑选对应的插槽,作为物理基址;
  • 虚拟基址以相同方式选择,但限制在内核的虚拟地址窗口范围内。

如果没有合适的区域可用,代码会回退到默认地址并打印一个小警告;如果你在命令行传了 nokaslr,则会按设计跳过随机化步骤。


术语速览

  • 十六进制(Hexadecimal):以 0x 作为前缀的 16 进制数。0x10 是 16,0x100000 是 1MB。十六进制与位存储对齐良好,因此底层代码常用。
  • 寄存器(Register):CPU 内部用于“当下”存数的微小槽位,如 CS、DS、SS、IP、SP。
  • 段与偏移(Segment/Offset):在实模式中用于构造物理地址的两部分,公式为:physical = segment * 16 + offset
  • BIOS:较早的固件风格,负责开机、自检,并把第一个引导扇区加载到内存。
  • UEFI:现代固件,理解文件系统,可直接加载更大的引导程序。
  • 引导加载器(Bootloader):把内核放入内存并向其传递系统事实的“门童”,GRUB 很常见。
  • 栈(Stack):函数使用的“后进先出”工作台。SS 选择其段,SP 指向当前栈顶。
  • BSS:用于存放必须从零开始的全局变量区域。C 运行前,内核 setup 代码会清零该区域。
  • 中断(Interrupt):来自硬件或软件的快速“打断”。CPU 暂停、运行小处理程序后恢复。可屏蔽中断允许暂时阻断,NMI 不可。
  • GDT(全局描述符表):段描述符的短表。Linux 在早期设置为简单的扁平模型。
  • IDT(中断描述符表):中断处理程序的目录。早期启动使用一个极简版本,完整内核稍后安装真正的表。
  • A20 线(A20 line):在老式 PC 上必须打开的历史性开关,否则 1MB 以上寻址不正确。
  • 保护模式(Protected mode):32 位模式,引入 GDT/IDT,并允许分页。
  • 长模式(Long mode):x86_64 的 64 位模式,要求启用分页,并在 EFER 寄存器中设置 LME 位。
  • 分页(Paging):将虚拟地址翻译到物理内存的机制,通过页表实现。
  • 页表(Page tables):映射虚拟页到物理页的数据结构。早期启动常用“同址映射(identity map)”。常规页大小为 4KB,早期启动经常用 2MB 大页以快速覆盖地址空间。
  • CR0、CR3、CR4:控制寄存器。CR0 打开保护模式;CR3 指向页表顶层;CR4 启用扩展特性如 PAE。
  • EFER:模型特定寄存器,包含 Long Mode Enable(LME)等位。
  • ELF:内核的磁盘格式,内置“该放哪里”的结构化映射。
  • 重定位(Relocation):当代码被加载到不同于其构建基址的位置时用于修正地址的“补丁”。
  • kASLR:在启动时随机化内核基址以提高利用难度。

作者

  • 订阅 Feed:https://www.0xkato.xyz/feed.xml
  • 网站首页:https://www.0xkato.xyz/
  • 关于作者:https://www.0xkato.xyz/about
  • X(Twitter):https://x.com/0xkato

原文

欢迎关注我的其它发布渠道