3. 实模式启动代码分析:header.S 与 setup.bin

当 BIOS 将引导扇区加载到 0x7C00 并跳转执行后,内核的启动流程进入实模式阶段。这一阶段的核心代码位于 arch/x86/boot/header.S 和 arch/x86/boot/setup.bin(由 setup.S 及多个辅助文件编译链接而成)。本章将深入分析这两部分代码在实模式下的执行逻辑、内存布局以及关键的初始化操作。

3.1 header.S:引导扇区与内核头部

header.S 是内核镜像的最前端,它既包含引导扇区代码,也定义了内核的 实模式头部(Real-Mode Header)。这个头部是引导加载程序(如 GRUB、LILO 或直接 BIOS 调用)识别和加载内核的关键数据结构。

3.1.1 实模式头部结构

内核头部位于镜像文件偏移 0x1F1 处,其结构定义在 arch/x86/boot/header.S 中。以下是关键字段及其含义:

偏移(十六进制) 字段名 大小(字节) 描述
0x1F1 setup_sects 1 setup.bin 占用的扇区数(默认 4)
0x1F2 root_flags 2 根文件系统挂载标志
0x1F4 syssize 4 压缩内核镜像的大小(以 16 字节为单位)
0x1F8 ram_size 2 扩展内存大小(已废弃)
0x1FA vid_mode 2 视频模式选择
0x1FC root_dev 2 根文件系统设备号
0x1FE boot_flag 2 引导扇区标志(0xAA55)
0x200 jump 2 跳转到 setup 代码的指令
0x202 header 4 头部魔数(HDRS)
关键点: 引导扇区标志 0xAA55 位于偏移 0x1FE,这是 BIOS 识别可引导设备的硬性要求。内核头部魔数 HDRS(0x53726448)用于引导加载程序验证内核镜像的完整性。

3.1.2 引导扇区代码流程

当 BIOS 将第一个扇区(512 字节)加载到 0x7C00 后,执行从 _start 标号开始。以下是核心代码片段及注释:

// arch/x86/boot/header.S (简化)
.section ".bstext", "ax"

.globl _start
_start:
    // 1. 设置段寄存器
    movw    %cs, %ax
    movw    %ax, %ds
    movw    %ax, %es
    movw    %ax, %ss
    movw    $0x7C00, %sp      // 栈指针指向当前代码段底部

    // 2. 读取 setup.bin 到 0x7E00
    movw    $0x7E00, %bx      // 目标地址
    movb    setup_sects, %al  // 要读取的扇区数
    movb    $0x02, %ah        // BIOS 读扇区功能号
    int     $0x13             // 调用 BIOS 中断

    // 3. 跳转到 setup 代码
    ljmp    $0x0000, $0x7E00  // 远跳转到 setup.bin 入口
实模式地址计算: 在实模式下,物理地址 = 段寄存器值 × 16 + 偏移。因此 0x0000:0x7C00 对应物理地址 0x7C00,0x0000:0x7E00 对应 0x7E00。

3.2 setup.bin:实模式初始化核心

setup.bin 是内核在实模式下的主要执行体,它负责收集硬件信息、设置基本环境,并为切换到保护模式做准备。其入口点位于 arch/x86/boot/setup.S 中的 start_of_setup 标号。

3.2.1 内存布局与加载地址

在实模式阶段,内核各部分在内存中的布局如下:

实模式启动阶段内存布局 0x00000000 - 0x000003FF:BIOS 中断向量表 (IVT) 0x00000400 - 0x000004FF:BIOS 数据区 (BDA) 0x00000500 - 0x00007BFF:空闲内存 / 引导加载程序使用 0x00007C00 - 0x00007DFF:引导扇区 (header.S 第一部分) 0x00007E00 - 0x00007FFF:setup.bin (实模式初始化代码) 0x00008000 - 0x00008FFF:临时栈 (向下增长) 0x00010000 (64KB) 及以上:压缩内核镜像 (bzImage) 地址递增 →

3.2.2 初始化操作流程

setup.bin 的执行流程可以分为以下几个关键步骤:

  1. 设置栈和段寄存器:将栈指针 SP 设置为 0x8000(向下增长),确保有足够的栈空间供后续调用。
  2. 检查内核签名:验证 0x7C00 处的引导扇区标志 0xAA55 和内核头部魔数 HDRS。
  3. 收集硬件信息:通过 BIOS 中断获取内存大小(int 0x15, e820h)、磁盘参数、视频模式等。
  4. 设置视频模式:根据 vid_mode 字段或用户指定的模式初始化显示。
  5. 移动内核到低地址:将压缩内核从加载地址(通常 0x10000 以上)移动到 0x10000(64KB)处,为解压做准备。
  6. 进入保护模式:关闭中断、加载全局描述符表(GDT)、设置控制寄存器 CR0 的保护模式位,然后远跳转到 32 位代码。
注意事项: 在实模式下,中断是开启的。但在进入保护模式前,必须使用 cli 指令关闭中断,并确保 IDT(中断描述符表)在保护模式下正确设置,否则会导致三重故障(Triple Fault)并重启系统。

3.2.3 关键代码分析:内存探测

内存探测是 setup.bin 最重要的任务之一。以下是通过 int 0x15, e820h 获取内存布局的典型代码:

// arch/x86/boot/memory.c (简化)
int detect_memory_e820(void) {
    struct biosregs ireg, oreg;
    struct e820entry *desc = &e820_table;
    int count = 0;

    initregs(&ireg);
    ireg.ax  = 0xE820;
    ireg.cx  = sizeof(*desc);
    ireg.edx = SMAP;          // 签名: 0x534D4150 ('SMAP')
    ireg.di  = (size_t)desc;

    do {
        intcall(0x15, &ireg, &oreg);
        ireg.ebx = oreg.ebx;  // 连续调用索引
        desc++;
        count++;
    } while (ireg.ebx && count < 128);

    return count;
}
E820 返回值: 每次调用返回一个 e820entry 结构,包含内存基地址、长度和类型(1 = 可用内存,2 = 保留,3 = ACPI 可回收,4 = ACPI NVS)。内核利用这些信息构建最终的物理内存映射。

3.3 实模式与保护模式的切换

setup.bin 的最后一步是切换到保护模式。这一过程涉及以下关键操作:

  • 加载 GDT:使用 lgdt 指令加载临时全局描述符表,其中包含代码段和数据段描述符。
  • 设置 CR0:将 CR0 寄存器的第 0 位(PE 位)置 1,启用保护模式。
  • 远跳转刷新流水线:执行 ljmp 指令跳转到 32 位代码段,同时刷新 CPU 指令预取队列。
// arch/x86/boot/pmjump.S (简化)
.section ".text32", "ax"

.code32
jmpl    *%eax                 // 跳转到 32 位内核入口 (startup_32)
段选择子: 在保护模式下,段寄存器不再直接存储基地址,而是存储段选择子(Selector)。GDT 中的每个描述符定义了段的基地址、限长和访问权限。内核通常使用 __BOOT_CS(代码段)和 __BOOT_DS(数据段)作为初始选择子。

3.4 本章小结

实模式启动代码是 Linux 内核启动流程的基石。header.S 提供了引导扇区和内核头部,确保 BIOS 和引导加载程序能够正确识别和加载内核。setup.bin 则负责在实模式下完成硬件探测、环境初始化,并最终将控制权移交给保护模式下的 32 位内核。理解这一阶段的内存布局和初始化操作,对于后续分析内核解压、重定位以及内存管理至关重要。