



























按下电源键,转瞬之间字符瀑布奔涌或 Logo 悄然浮现,Linux 随之亮相。看似魔法,实则是一连串微小代码与CPU之间的精密握手。下文循着这场握手的轨迹,一路追踪到 Linux 内核首行 C 代码的登场。
电源稳定后,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 内部的一个小槽位,用来暂存当前正在使用的数值。诸如 CS 与 IP 就是寄存器名:CS 表示“代码段(code segment)”,标记当前指令的所在“邻域”;IP 表示“指令指针(instruction pointer)”,标记下一条指令的位置。
固件(firmware)是烧在主板上的一个小型引导程序。
0x55AA,就认定“这货能启动”。于是 BIOS 把这扇区搬到内存 0x7C00,然后跳过去继续执行。扇区容量极小,通常只够再拉一段更大的加载器进来。引导加载器是把操作系统“请进场”的门童。GRUB 是 PC 上的常见选择。它读取自身配置,显示菜单(如果你配置了),并将 Linux 内核加载到内存中。内核文件实际上包含两部分:
GRUB 还会往一个叫 setup header 的小结构里填好关键信息:内核被摆在哪、命令行丢在哪、有没有 initrd 等。填完便直接跳去 setup 程序继续干活。
在 Linux 做任何有趣的事前,setup 代码需要创建一个可预测的工作环境:
CS(代码段)、DS(数据段)与 SS(栈段)。同时清除一个叫“方向标志”的 CPU 位,让拷贝指令向前移动。SS 指定栈所用的段,SP 指向当前栈顶。earlyprintk,setup 代码还会编程串口以打印非常早期的消息——当图形尚未就绪时尤为有用。e820,它返回一份简单的可用与保留范围列表。内核会用这份列表来避免踩到固件的脚趾。完成这些后,setup 代码调用它的第一个 C 函数,名字就叫 main。此时我们仍在这个古老而小巧的实模式中……
现代 Linux 在 PC 上运行于 long mode(64 位,x86_64)。你无法从实模式(real mode) 直接跳到 long mode,路径是:real mode → protected mode → long mode。本部分解释这条路径与相关术语。
保护模式是为摆脱 1980 年代限制而引入的 32 位世界,它增加两件核心工具:
setup 代码先把“吵闹”部分关掉:用一条指令禁用可屏蔽中断(maskable interrupts),让老式 PIC 芯片安静,以确保硬件中断暂时完全被阻断;打开 A20 line(历史性开关),避免地址在 1MB 处环回;重置数学协处理器,让浮点状态干净。
随后加载一个仅含必需项的迷你 GDT 与迷你 IDT。最终在 CR0 中设置 PE 位(Protected Mode Enable),并执行一次 far jump。这个跳转会从 GDT 重新加载代码段,锁定进入 protected mode;同时重载数据段与栈段,并修正栈指针以匹配新的扁平世界。
我们现在处于 32 位的 protected mode。
小知识:控制寄存器(control registers)
Linux 想要的是 64 位,也就是 long mode。还需要两件事:
32 位序幕会建立一套小型页表,表达“在这片区域,虚拟地址等于物理地址”。这叫 identity map(同址映射),足以让分页安全地开启。
为此,代码在 CR4 中启用 PAE,使得使用更大的页表项;构建覆盖低端内存的最小页表,用 2MB 页快速铺设;将顶层页表地址写入 CR3,分页就绪。
最后在 EFER 中设置 LME 位,并通过一次 far return 跳入以 64 位语法编写的标签。long mode 现已激活。段仍旧“扁平”,但地址与寄存器都变为 64 位宽。
为什么要如此小心?在一个活着的系统里切换模式像是在行进中换轮胎。代码先屏蔽打断、准备最小所需的表、再翻转关键位,最后才重新允许中断。稳妥的顺序可以避免半切换的奇怪状态。
我们已有 64 位 CPU、Paging 已开启,内存中放着一个压缩的内核。现在由一个小小的 64 位 stub 来做实际工作:如有需要先挪开自己、解包内核、若内核不在默认位置则修正地址,最后跳转。
stub 首先搞清楚它到底运行在何处。早期代码在链接时好像自己位于地址 0,运行时再计算真实基址。如果解压后的内核计划目的地会与 stub 重叠,它会先把自己复制到安全位置。
它清零自己的 BSS,让全局状态从干净开始。
它加载一份极简 IDT,仅有两个处理程序:一个处理 page fault(页故障),一个处理 NMI(不可屏蔽中断)。页故障发生在 CPU 试图使用的虚拟地址没有对应映射时。在我们早期的 identity map 世界里,这个小小的页故障处理器可以即时补上缺失的映射并继续运行。NMI 处理器则确保在我们尚在“拉起系统”的阶段,突发的不可屏蔽中断不会让机器崩溃。
同时它还为接下来会触及的区域建立同址映射,包括内核的未来驻留区、由引导加载器填充的 boot parameters(启动参数)页,以及命令行缓冲区。
一个常被命名为 extract_kernel 的 C 函数接管。它先划出一小块堆作为临时缓冲;打印那句经典的提示;随后用内核构建时选择的算法进行解压。gzip、xz、zstd、lzo 等都通过同一个包装器接入。
字节解出后,解压器读取内核的 ELF(Executable and Linkable Format)头。ELF 既是文件格式也是地图:哪些是代码、哪些是数据、每块应该确切放到哪里。解压器按图将每个分块拷贝到其归属位置。
如果内核被加载到与其构建时不同的地址,解压器会应用 relocations(重定位)。重定位是对包含地址的指令或指针做的小修正。解压器遍历修正列表,把每个位置补丁到我们实际使用的地址空间中的正确指向。
当一切就绪,解压器返回“真正内核”的入口地址,并跳转过去,同时传入指向启动参数的指针。从那一刻起,你已经进入完整内核。遇到的第一个函数是 start_kernel,大型初始化随即开始。
你可能在内核日志中看到过 kASLR(Kernel Address Space Layout Randomization,内核地址空间布局随机化)。核心思想非常直接:如果攻击者不知道内核在内存中的确切位置,某些利用会变得更困难。
在启动早期,若启用了 kASLR,解压器会随机选择两个“基址”:
它如何在不“踩雷”的前提下做选择?
memmap= 选项,所保留的区间也会被纳入其中。如果没有合适的区域可用,代码会回退到默认地址并打印一个小警告;如果你在命令行传了 nokaslr,则会按设计跳过随机化步骤。
0x 作为前缀的 16 进制数。0x10 是 16,0x100000 是 1MB。十六进制与位存储对齐良好,因此底层代码常用。physical = segment * 16 + offset。SS 选择其段,SP 指向当前栈顶。https://www.0xkato.xyz/feed.xmlhttps://www.0xkato.xyz/https://www.0xkato.xyz/abouthttps://x.com/0xkato此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。