2. 启动协议与入口点:bzImage与zImage格式区别

Linux内核的启动过程始于引导加载程序(Bootloader)将内核映像从存储介质加载到内存,并跳转到入口点执行。x86架构下,内核映像主要分为zImage(压缩内核映像)和bzImage(大内核映像)两种格式。理解两者的区别、实模式与保护模式入口点的设计,以及启动参数的传递约定,是分析内核解压与重定位阶段内存布局的基础。

2.1 bzImage与zImage格式对比

zImage是早期内核使用的格式,其最大限制是内核映像(包括解压后的代码)必须位于内存低端1MB范围内(即实模式地址空间)。随着内核体积增长,这一限制成为瓶颈。bzImage(Big zImage)突破了这一限制,允许内核映像位于内存任意位置,通常被加载到16MB以下的内存区域。

对比维度 zImage bzImage
最大映像大小 约512KB(受限于低端内存) 无严格上限(通常可达数MB)
加载地址 必须位于0x10000(64KB)以下 通常加载到0x100000(1MB)附近
实模式入口 映像起始偏移0x00 映像起始偏移0x00(setup.S)
保护模式入口 解压后位于0x1000 解压后位于0x100000(1MB)
引导协议版本 早期协议(≤2.00) 现代协议(≥2.00)
当前使用状态 已废弃(仅用于极小内核) 标准格式
关键点:bzImage的“b”代表“big”,但并非指映像文件更大,而是指内核在内存中的加载地址突破了1MB限制。现代所有发行版内核均使用bzImage格式。

2.2 实模式与保护模式入口点分析

bzImage内核映像包含两个主要的入口点:实模式入口(位于setup.S)和保护模式入口(位于compressed/head_64.S或head_32.S)。引导加载程序首先跳转到实模式入口,执行硬件初始化、切换CPU到保护模式,然后跳转到保护模式入口开始解压内核。

2.2.1 实模式入口点(start_of_setup)

实模式入口位于内核映像的偏移0x200处(即setup.S代码段的起始位置)。引导加载程序通过远跳转(jmp far)进入此地址。实模式代码执行以下关键步骤:

  1. 段寄存器初始化:设置DS、ES、SS等段寄存器为已知值。
  2. 堆栈设置:在实模式地址空间建立临时堆栈。
  3. 硬件探测:通过BIOS中断获取内存布局、显示模式等信息。
  4. 切换到保护模式:加载GDT、设置CR0寄存器,跳转到32位或64位保护模式代码。
入口地址计算:若内核映像被加载到物理地址0x100000(1MB),则实模式入口地址为0x100000 + 0x200 = 0x100200。引导加载程序需将控制权转交至此地址。

2.2.2 保护模式入口点(startup_32 / startup_64)

保护模式入口位于内核映像的压缩部分(vmlinux.bin.gz的头部)。实模式代码完成切换后,通过远跳转进入保护模式入口。该入口负责:

  • 解压内核:调用解压算法(gzip、LZ4、XZ等)将vmlinux解压到目标地址。
  • 重定位:如果内核配置了可重定位(CONFIG_RELOCATABLE),则根据加载地址调整符号表。
  • 跳转到内核主入口:解压完成后,跳转到start_kernel函数。
/* 保护模式入口点示例(arch/x86/boot/compressed/head_64.S) */
SYM_FUNC_START(startup_32)
    cld
    cli
    /* 初始化页表 */
    leal    (BP_scratch - startup_32)(%ebp), %eax
    /* ... 解压与重定位逻辑 ... */
    jmp     *%eax   /* 跳转到解压后的内核 */
SYM_FUNC_END(startup_32)
注意:在64位系统上,保护模式入口分为startup_32(用于兼容32位引导)和startup_64(用于64位引导)。引导加载程序必须根据自身运行模式选择正确的入口。
bzImage入口点与执行流程 引导加载程序 实模式入口 保护模式入口 实模式执行步骤 1. 段寄存器初始化 2. 堆栈设置 3. 硬件探测(BIOS) 4. 切换到保护模式 保护模式执行步骤 1. 解压内核映像 2. 重定位(如需要) 3. 跳转到start_kernel

2.3 启动参数传递约定

引导加载程序与内核之间通过启动协议(Boot Protocol)传递参数。这些参数包括内存布局、命令行、视频模式等信息。协议定义在Documentation/x86/boot.txt中,核心数据结构是struct boot_params。

2.3.1 boot_params结构体

boot_params位于实模式代码段(setup.S)的固定偏移处,引导加载程序在跳转到内核前填充该结构。关键字段如下:

字段 偏移 大小 描述
hdr.type_of_loader 0x210 1字节 引导加载程序类型ID
hdr.loadflags 0x211 1字节 加载标志(如LOADED_HIGH)
hdr.heap_end_ptr 0x224 2字节 实模式堆栈结束指针
hdr.cmd_line_ptr 0x228 4字节 命令行字符串的物理地址
e820_map 0x2D0 可变 系统内存布局(E820表)
screen_info 0x00 64字节 显示模式信息

2.3.2 命令行传递

引导加载程序将内核命令行字符串放置在内存中,并将物理地址写入hdr.cmd_line_ptr。命令行最大长度通常为2048字节(COMMAND_LINE_SIZE)。内核在解压后从该地址读取命令行参数。

/* 引导加载程序设置命令行的示例(伪代码) */
char *cmdline = "root=/dev/sda1 console=tty0";
unsigned long cmdline_addr = 0x100000; /* 示例地址 */

/* 将命令行复制到约定地址 */
memcpy((void *)cmdline_addr, cmdline, strlen(cmdline) + 1);

/* 填充boot_params */
boot_params.hdr.cmd_line_ptr = cmdline_addr;
boot_params.hdr.cmdline_size = strlen(cmdline) + 1;
协议版本检查:内核通过hdr.version字段(偏移0x206)检查引导加载程序支持的协议版本。现代内核要求协议版本≥2.00,否则可能拒绝启动。

2.3.3 内存布局传递(E820表)

引导加载程序通过BIOS中断INT 0x15, E820h获取系统物理内存布局,并将结果填充到boot_params.e820_map中。内核在解压阶段使用此信息确定可用内存区域,避免覆盖关键数据。

/* E820表项结构 */
struct e820_entry {
    unsigned long long addr;   /* 起始地址 */
    unsigned long long size;   /* 长度 */
    unsigned int type;         /* 类型(1=可用,2=保留,3=ACPI等) */
} __attribute__((packed));
启动参数传递流程 引导加载程序 填充boot_params boot_params 内存中固定偏移 内核解压代码 读取参数并执行 传递的关键参数 • 命令行地址 (cmd_line_ptr) → 内核启动参数 • E820内存映射 (e820_map) → 物理内存布局 • 视频模式 (screen_info) → 显示输出设置 • 加载标志 (loadflags) → 指示内核是否已加载到高端内存

2.4 启动协议版本演进

Linux内核启动协议经历了多个版本迭代,每个版本引入新的特性或修改约定。以下列出关键版本变化:

  • 协议 1.00(1992年):初始版本,仅支持zImage格式,无命令行传递。
  • 协议 2.00(1996年):引入bzImage格式,支持命令行传递(cmd_line_ptr)。
  • 协议 2.03(1999年):增加E820内存映射支持,替代旧的E801/88内存查询。
  • 协议 2.07(2003年):增加loadflags中的LOADED_HIGH标志,指示内核已加载到高端内存。
  • 协议 2.12(2012年):增加pref_address字段,支持内核首选加载地址提示。
兼容性:现代内核(≥5.x)要求引导加载程序至少支持协议2.00。GRUB 2、Syslinux等主流引导加载程序均支持最新协议版本。

2.5 本章小结