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) |
| 当前使用状态 | 已废弃(仅用于极小内核) | 标准格式 |
2.2 实模式与保护模式入口点分析
bzImage内核映像包含两个主要的入口点:实模式入口(位于setup.S)和保护模式入口(位于compressed/head_64.S或head_32.S)。引导加载程序首先跳转到实模式入口,执行硬件初始化、切换CPU到保护模式,然后跳转到保护模式入口开始解压内核。
2.2.1 实模式入口点(start_of_setup)
实模式入口位于内核映像的偏移0x200处(即setup.S代码段的起始位置)。引导加载程序通过远跳转(jmp far)进入此地址。实模式代码执行以下关键步骤:
- 段寄存器初始化:设置DS、ES、SS等段寄存器为已知值。
- 堆栈设置:在实模式地址空间建立临时堆栈。
- 硬件探测:通过BIOS中断获取内存布局、显示模式等信息。
- 切换到保护模式:加载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)
startup_32(用于兼容32位引导)和startup_64(用于64位引导)。引导加载程序必须根据自身运行模式选择正确的入口。
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));
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字段,支持内核首选加载地址提示。