第 12 讲:从上电复位到内核启动
启动最令人困惑的地方是:加载程序需要已经在运行的程序,但机器刚上电时还没有任何软件环境。解决方法不是“一步启动完成”,而是把一小段必然可执行的代码固化在预定位置,再一级级加载能力更强的程序。
启动链的一张文字图
上电/复位
↓ CPU 从架构规定的复位向量取指
固件(MIPS 上的 ROM/U-Boot stage 1,x86 上的 BIOS/UEFI)
↓ 建立最小硬件环境,找可启动介质
Bootloader 后续阶段(U-Boot stage 2、GRUB 等)
↓ 识别内核映像,装入 RAM,准备参数
架构相关内核入口(head.S 等)
↓ 建栈、页表、异常向量,解压/移动内核
架构无关初始化(Linux start_kernel)
↓ 初始化内存、调度、中断、驱动、VFS
init/用户空间
每一级只做当时条件允许的事,然后把“可用环境”扩大一点。课件将其称为逐步释放系统灵活性。
Bootloader 的两个任务
Bootloader 一词包含:
- Booter:把 CPU、时钟、内存控制器、串口等从复位态带到可用态;
- Loader:找到 OS 内核映像,装入内存,把控制权跳转给内核入口。
它与硬件强相关:即使 CPU 相同,Flash、DRAM 控制器和板级连线不同,也需要移植。U-Boot 通常拆成两阶段:
- stage 1 体积小、多用汇编,先建立栈和基本 DRAM 环境;
- stage 2 已能运行 C 代码,初始化更多设备,读取环境变量,选内核和启动参数。
MIPS:为什么上电时要用 kseg1
32 位 MIPS 将 4 GiB 虚拟地址空间分区。课件关心的是:
kuseg是用户地址,需要 MMU/TLB 转换;kseg0是内核直接映射且可缓存的区域;kseg1也直接映射低端物理地址,但不经 Cache;- 其他内核区域需要 TLB 支持。
刚复位时 TLB 和 Cache 尚未配置,因此不能把启动绑定在它们上。MIPS 复位向量落在与 ROM/Flash 对应的 kseg1 地址,CPU 可以立即稳定取指。
MIPS + U-Boot 的接力
- 从 Flash 复位入口执行少量汇编;
- 初始化 CPU 相关状态、时钟和 DRAM;
- 建立栈,进入
board_init_f等 C 代码; - 初始化 timer、环境变量、串口等;
- 规划 RAM,搬迁 U-Boot 自身,建立堆栈;
- 把 Linux 内核复制到 RAM,跳到内核入口。
Linux 的架构相关入口 head.S 先完成早期映射、堆栈与异常环境,然后进入 start_kernel。后者才是大量架构无关子系统初始化的起点。
x86 BIOS/MBR 传统链路
1. 复位向量与 BIOS
x86 复位后从架构规定的固件入口取指;课件以 0xFFFF0 为例。BIOS 随后:
- 执行 POST 硬件自检;
- 初始化基本设备;
- 根据启动顺序选硬盘、U 盘或网络;
- 读取启动介质的首扇区。
BIOS 背负实模式、16 位代码等历史兼容包袱,这正是 UEFI 试图用模块化、C 风格接口和更强可扩展性替代它的原因。
2. MBR 为什么只有 512 B
传统 BIOS 从磁盘首扇区读取 MBR,常见布局是:
| 区域 | 大小 | 用途 |
|---|---|---|
| 引导代码及数据 | 446 B | 很小的第一阶段加载器 |
| 分区表 DPT | 64 B | 4 个 16 B 分区项 |
| 结束标志 | 2 B | 引导扇区签名 |
空间这么小,根本放不下一个懂文件系统、懂网络、有菜单的完整加载器。因此 GRUB 也要分阶段:MBR 中的 stage 1 只负责找到更强的后续阶段,后者再解析配置和文件系统。
MBR 同时限制了四个主分区。“活动分区”和扩展分区都是这条历史链上的兼容机制,不应当成所有现代 UEFI/GPT 系统都必然遵循的步骤。
3. GRUB 与内核映像
GRUB 后续阶段能够:
- 提供交互菜单和命令行;
- 识别文件系统,按路径找内核;
- 选择多个 OS 或多个内核版本;
- 准备内核命令行、初始 RAM 文件系统等参数;
- 把内核映像放到约定内存位置并跳转。
内核映像不是可直接随意跳入的普通用户程序。其引导头、早期设置代码和压缩内核有规定的内存布局与协议。
Linux 接管后的顺序
start_kernel 附近的主线可以理解为:
- 先让 CPU 和内核自身的基本数据结构稳定;
- 在完整中断子系统准备好前保持中断关闭;
- 建立内存管理、调度、定时器和中断框架;
- 初始化设备与文件系统;
- 建立第一批内核线程,最终启动用户空间
init。
顺序不能乱:驱动申请中断前,中断框架要存在;挂载根文件系统前,存储设备与 VFS 要可用。启动不是一张运行清单,而是一张依赖图的拓扑排序。
遇到启动题怎么判断
先问当前阶段已经有什么能力:
- 还没有 RAM:代码不能假设有正常栈和堆;
- 还没有页表/TLB:只能使用架构保证的直接地址;
- 还没有文件系统:只能按固定块位置或借助更早固件服务读取;
- 还没有驱动:不能假设通用 I/O 接口已存在。
然后再问它想把哪个下一级加载到哪里、如何验证、最后跳到哪个入口。这比死背 BIOS 的第几步更不容易混。