概述
arch/x86/boot/header.S
是Linux内核启动过程中的关键文件,它定义了内核镜像的头部结构和早期启动代码。这个文件就像内核的"身份证"和"启动说明书",告诉引导加载器如何正确加载和启动内核。
第一阶段:引导加载器解析内核头部
1.1 引导加载器如何找到并解析 header.S
启动流程中 header.S 的位置:
BIOS/UEFI → 引导加载器(GRUB) → header.S头部解析 → 跳转到_start → 执行start_of_setup
1.2 GRUB 解析过程
GRUB 加载过程:
// grub-core/loader/i386/linux.c
grub_linux_boot() {
// 读取内核头部
grub_file_read(file, &lh, sizeof(lh));
// 检查启动标志
if (lh.boot_flag != grub_cpu_to_le16_compile_time(0xAA55))
return grub_error(GRUB_ERR_BAD_OS, "invalid magic number");
// 检查头部签名
if (lh.header != grub_cpu_to_le32_compile_time(0x53726448)) /* "HdrS" */
return grub_error(GRUB_ERR_BAD_OS, "no setup signature found");
}
1.3 内核头部结构(引导加载器读取的部分)
// 传统启动头部 - 引导加载器首先读取这部分
.section ".header", "a"
.globl sentinel
sentinel: .byte 0xff, 0xff /* 哨兵值,检测损坏的引导器 */
.globl hdr
hdr:
.byte setup_sects - 1 /* 设置代码扇区数 */
root_flags: .word ROOT_RDONLY /* 根文件系统标志 */
syssize: .long ZO__edata / 16 /* 系统大小 */
ram_size: .word 0 /* 已废弃的内存大小字段 */
vid_mode: .word SVGA_MODE /* 视频模式 */
root_dev: .word 0 /* 根设备号 */
boot_flag: .word 0xAA55 /* 启动标志,经典魔数 */
这是内核的"传统身份证",包含了启动所需的基本信息:
sentinel: 0xff, 0xff
:哨兵值,用来检测损坏的引导加载器,就像"防伪标记"setup_sects - 1
:设置代码的扇区数,告诉引导器"我的设置代码有多长"boot_flag: 0xAA55
:启动标志,这是经典的"魔数",就像"合法启动的印章"
1.4 内核入口点定义
# offset 512, entry point - 偏移512字节处,内核的真正入口点
.globl _start
_start:
.byte 0xeb # short (2-byte) jump - 2字节短跳转
.byte start_of_setup-1f # 跳转到真正的设置代码
1:
# Part 2 of the header, from the old setup.S
.ascii "HdrS" # header signature - 头部签名
.word 0x020f # header version number - 头部版本号
这是内核的"真正入口":
_start
:这是引导加载器跳转到的第一个地址,就像"大门"- 短跳转指令:
0xeb
是一个2字节的短跳转,跳到真正的设置代码 "HdrS"
:头部签名,告诉引导器"这确实是Linux内核"- 版本号:确保兼容性,就像"协议版本号"
1.5 启动参数配置区域
type_of_loader: .byte 0 # 引导器类型
loadflags: .byte LOADED_HIGH # 加载标志,表示"请把我加载到高地址"
setup_move_size: .word 0x8000 # 如果设置代码不在0x90000,需要移动的大小
code32_start: .long 0x100000 # 32位代码起始地址,默认1MB处
ramdisk_image: .long 0 # 内存盘地址,就像"临时仓库的位置"
ramdisk_size: .long 0 # 内存盘大小,就像"临时仓库的容量"
cmd_line_ptr: .long 0 # 命令行参数指针,就像"启动时的特殊指令"
这部分是"启动配置参数",就像搬家时的详细说明,告诉引导器如何正确设置内核的运行环境。
第二阶段:引导加载器跳转到内核
2.1 引导加载器的跳转操作
# 引导加载器(如GRUB)执行类似操作:
jmp $KERNEL_CS, $_start # 跳转到header.S的_start标签
2.2 _start 标签的执行
_start:
# 必须用字节形式,避免汇编器生成3字节跳转
.byte 0xeb # 2字节短跳转指令
.byte start_of_setup-1f # 跳转偏移量,像"门口的指路牌"
1:
# 这里实际执行:jmp start_of_setup
第三阶段:start_of_setup 早期环境初始化
3.1 start_of_setup 开始执行
.section ".entrytext", "ax"
start_of_setup: # 内核真正开始执行的第一段代码
这是内核真正开始执行的第一段代码,就像"搬进新房子后的第一件事"。
3.2 段寄存器设置
# Force %es = %ds # 强制ES=DS,确保段寄存器一致
movw %ds, %ax # 让ES和DS指向同一个地方
movw %ax, %es # 就像"确保所有房间钥匙都能用"
cld # 清除方向标志,设置默认工作方向
段寄存器设置:
movw %ds, %ax; movw %ax, %es
:让ES和DS指向同一个地方,确保所有房间钥匙都能用cld
:清除方向标志,确保字符串操作是正向的,设置默认的工作方向
3.3 栈空间检查和设置
# 栈空间检查和设置,没有栈就无法调用函数
movw %ss, %dx # 检查栈段是否有效
cmpw %ax, %dx # %ds == %ss? 检查SS是否等于DS
movw %sp, %dx # 就像"检查临时工作台是否可用"
je 2f # -> assume %sp is reasonably set
# Invalid %ss, make up a new stack 栈无效时重新计算栈指针
movw $_end, %dx # 从代码结束处开始
testb $CAN_USE_HEAP, loadflags # 检查是否可以使用堆
jz 1f
movw heap_end_ptr, %dx # 如果可以使用堆,就用heap_end_ptr
1: addw $STACK_SIZE, %dx # 加上栈大小,给栈留出空间
jnc 2f
xorw %dx, %dx # Prevent wraparound 防止地址回绕
2: # Now %dx should point to the end of our stack space
andw $~3, %dx # dword align 4字节对齐
jnz 3f
movw $0xfffc, %dx # Make sure we're not zero 确保不为零
3: movw %ax, %ss # 设置栈段
movzwl %dx, %esp # 设置栈指针
sti # 开启中断
栈空间检查和设置: 这是最关键的部分,因为没有栈就无法调用函数:
- 检查栈段:检查SS是否等于DS,就像"检查临时工作台是否可用"
- 栈无效时的处理:如果栈段无效,就重新计算栈指针
- 栈对齐:确保栈是4字节对齐的,就像"工作台要摆放整齐"
- 设置栈:最后设置SS和ESP,开启中断
3.4 CS段规范化
# CS段规范化,让CS和DS保持一致
pushw %ds # 用远返回技巧规范化CS段
pushw $6f # 相当于跳转到DS:6f
lretw # 让CS和DS保持一致,统一门牌号格式
6:
这部分是"地址规范化":
- 由于历史原因,CS可能比DS高0x20,这里用一个巧妙的技巧
- 相当于跳转到DS:6f,让CS和DS保持一致
- 就像"把所有房间的门牌号统一格式"
3.5 签名验证
# Check signature at end of setup 检查设置代码末尾的签名
cmpl $0x5a5aaa55, setup_sig # 确保代码完整,检查包裹是否损坏
jne setup_bad # 签名错误就跳转到错误处理
签名检查:
- 检查设置代码末尾的签名,确保代码完整
- 就像"检查包裹是否完整,有没有损坏"
3.6 BSS段清零
# Zero the bss # BSS段清零,清理所有空房间
movw $__bss_start, %di # 设置目标地址为BSS段开始
movw $_end+3, %cx # 计算要清零的长度
xorl %eax, %eax # EAX清零,用作填充值
subw %di, %cx # 计算实际长度
shrw $2, %cx # 除以4,按双字处理
rep stosl # 重复存储,把BSS段全部清零
BSS段清零: 这就像"搬进新房子后,把所有空房间都打扫干净"。BSS段存放未初始化的全局变量,必须清零。
第四阶段:跳转到C语言代码
4.1 调用main函数
# Jump to C code (should not return) # 跳转到C代码,交给C语言继续
calll main # 调用main函数,进入C语言世界
4.2 start_of_setup 到 main.c 的完整调用路径
完整调用链:
引导加载器 → _start → start_of_setup → calll main → arch/x86/boot/main.c:main()
calll main 指令的执行过程:
- 压栈:将返回地址(下一条指令地址)压入栈
- 跳转:将EIP设置为main函数的地址
- 继续执行:从main函数第一条指令开始执行
4.3 main函数开始执行
// 文件:arch/x86/boot/main.c
void main(void)
{
// 复制启动参数
copy_boot_params();
// 初始化控制台
console_init();
// 检测内存
detect_memory();
// 设置键盘
keyboard_init();
// 检查CPU特性
validate_cpu();
// 设置视频模式
set_video();
// 进入保护模式
go_to_protected_mode(); // 这个函数不会返回
}
4.4 为什么能直接调用C函数?
在调用 main()
之前,start_of_setup已经完成了所有必要的准备工作:
- 栈空间就绪:有了栈,C函数才能正常调用
- 段寄存器统一:确保数据访问的一致性
- BSS段清零:C语言的全局未初始化变量存放在BSS段,必须清零
- 中断开启:允许系统响应硬件中断
第五阶段:错误处理路径
5.1 签名验证失败的处理
# Setup corrupt somehow... # 错误处理:如果签名检查失败
setup_bad:
movl $setup_corrupt, %eax # 显示错误信息
calll puts # "No setup signature found..."
# Fall through... # 继续执行die函数
.globl die
.type die, @function
die: # 死循环停机,发现问题就报错并停止
hlt # 停机指令
jmp die # 无限循环,不能继续危险操作
错误处理:
- 如果签名检查失败,就跳到
setup_bad
- 显示错误信息"No setup signature found..."
- 最后进入
die
函数:无限循环停机 - 就像"如果发现问题就报错并停止,不能继续危险的操作"
执行流程总览图
关键时间节点总结
- 头部解析阶段:引导加载器读取并验证内核头部
- 入口跳转阶段:从_start跳转到start_of_setup
- 环境初始化阶段:设置段寄存器、栈空间、清零BSS
- 验证检查阶段:确保代码完整性
- 语言切换阶段:从汇编代码跳转到C语言代码
- 后续初始化阶段:在main.c中进行硬件检测和模式切换
这个完整的执行顺序展示了header.S如何承担起Linux内核启动过程中的关键桥梁作用,确保系统能够从引导加载器平滑过渡到内核主体代码的执行。
重要技术细节
内存布局和调用栈
调用前的内存状态:
栈内存布局 (从高地址到低地址):
┌─────────────────┐ ← ESP指向这里
│ │ (栈顶,空闲空间)
│ 栈空间 │
└─────────────────┘ ← 栈底
代码内存布局:
┌─────────────────┐ 0x90000
│ 引导加载器数据 │
├─────────────────┤ 0x90200
│ header.S │ ← start_of_setup在这里
├─────────────────┤ 0x90xxx
│ main.c │ ← main函数在这里
└─────────────────┘
执行 calll main 时的栈变化:
调用前栈状态:
┌─────────────────┐ ← ESP
│ │
└─────────────────┘
执行 calll main 后:
┌─────────────────┐ ← ESP (新位置)
│ 返回地址 │ ← 指向calll main的下一条指令
├─────────────────┤
│ │
└─────────────────┘
多启动方式支持
UEFI启动支持:
#ifdef CONFIG_EFI_STUB
# "MZ", MS-DOS header # PE格式的"MZ"签名
.word IMAGE_DOS_SIGNATURE
.org 0x38
.long LINUX_PE_MAGIC
.long pe_header
#endif
这部分是为了让Linux内核能在UEFI系统上启动:
- PE格式的"MZ"签名,像护照上的国徽
- 告诉UEFI"详细信息在这里"
- 让Linux内核既能在传统BIOS上启动,也能在现代UEFI上启动
总结
header.S文件虽然代码量不大,但在Linux启动过程中发挥着至关重要的作用。它按照严格的执行顺序完成以下任务:
- 身份验证:通过多重签名证明内核的合法性
- 参数协商:在引导器和内核之间建立通信协议
- 环境准备:为后续C代码建立最基本的运行环境
- 平滑过渡:实现从汇编到C语言的无缝切换
这种设计让Linux内核能够在各种不同的硬件平台和启动环境中稳定运行,体现了Linux系统优秀的工程设计和架构思想。理解header.S的执行流程,有助于深入掌握操作系统的启动机制,为系统级编程和内核开发打下基础。