这是“ARM64 Linux 系统启动”专题合集总结上篇。本文沿控制权、硬件描述与地址空间三条主线,串起 BootROM、固件、Bootloader、内核早期启动、SMP、根文件系统与 PID 1。
基线:Linux 6.6.145。
这份总结的目标是把启动理解成一条持续交接控制权、内存描述和硬件信息的链路,而不是若干互不相关的组件:
芯片复位 → 固件建立可信执行环境 → Bootloader 装载并传参 → Linux 建立自身地址空间与内核子系统 → 挂载根文件系统 → init 成为 PID 1 → 用户服务运行。
其中,前几个阶段主要解决“能不能安全地执行内核”;内核阶段解决“能不能管理机器资源”;用户空间阶段解决“能不能作为一个可用系统提供服务”。
1. 全景:控制权与关键信息如何一路传递
| | | |
|---|
| | | |
| | | |
| | 初始化安全世界、DDR、PSCI 电源管理接口;通常准备从高异常级降到非安全世界 | |
| | 装载 Linux Image、DTB、可选 initramfs;组织内核命令行 | x0 = DTB 物理地址 |
| | | 可执行的内核虚拟地址空间,进入 start_kernel() |
| | | |
| | 解包 initramfs 或挂载真正 rootfs | |
| | execve | |
这里有三条必须始终同步观察的信息流:
- 控制流
- 描述流:DTB、内核命令行、内存保留区、CPU 拓扑、设备地址等信息怎样交接。
- 地址空间流:从物理地址/无 MMU,到内核页表与虚拟地址,再到用户进程独立页表。
2. 关键概念与边界
2.1 异常级别:ARM64 的“权限楼层”
ARMv8-A 的异常级别(Exception Level,EL)可以先直观理解为硬件规定的权限楼层:
- EL3:最高特权层,通常由安全固件使用,负责安全世界与非安全世界的边界。
- EL2:虚拟化层;没有虚拟化需求时,内核仍可能由 Bootloader 从 EL2 进入,再自行降到 EL1。
- EL1:Linux 内核运行处,能配置页表、中断、异常向量和设备驱动。
- EL0:普通应用程序运行处,不能直接操作硬件控制寄存器。
Linux 的稳定目标是:
用户程序:EL0Linux 内核:EL1可选虚拟化管理:EL2安全固件:EL3
不要误解: “Linux 从 EL2 启动”不等于“Linux 最终运行在 EL2”。普通非虚拟化 ARM64 Linux 通常会确保其内核执行环境是 EL1。
本地源码的 arch/arm64/kernel/head.S 中,init_kernel_el() 会读取 CurrentEL,若当前处于 EL2,则先初始化 EL2 状态和 EL1 状态,再通过 eret 进入 EL1;若已经在 EL1,则直接建立规范 EL1 状态。见 arch/arm64/kernel/head.S:533-610。
2.2 Device Tree:硬件清单,不是驱动
设备树(Device Tree,DT) 是一棵数据树,描述“板子上有什么硬件、它在哪里、如何连接”。
其二进制形式为 DTB。Bootloader 将 DTB 放进内存,并在 ARM64 Linux 启动入口通过寄存器 x0 把 DTB 的物理地址传入内核。
本地 head.S 明确声明 ARM64 Linux Image 入口要求:
见 arch/arm64/kernel/head.S:44-54。
DTB 至少会影响:
- 保留内存:固件、DMA 缓冲区、initrd 等哪些范围不能误分配;
/chosen 节点中的 bootargs、initrd 起止地址等。
2.3 initramfs、根文件系统、init 与 systemd
这四个概念最容易混淆:
- initramfs:内核启动早期解包到内存中的临时根文件系统。常用于加载存储驱动、解密磁盘、组 RAID、找到真正 rootfs。
- 根文件系统(rootfs) :最终以
/ 挂载的文件系统,例如 ext4、NFS、UBIFS、squashfs。 init:内核启动的第一个用户态进程,成功后其 PID 是 1。- systemd:一种常见的
init 实现;它不是内核,也不是必须组件。BusyBox init、SysV init 都可充当 PID 1。
Linux 不会“启动 systemd”这一固定对象;它只会尝试执行指定或约定路径下的第一个用户态程序。
在 Linux 6.6.145 中,内核按以下顺序尝试:
rdinit=init=- 编译配置的
CONFIG_DEFAULT_INIT; /sbin/init/etc/init/bin/init/bin/sh
若都失败,则 panic:No working init found。见 init/main.c:1493-1517。
3. 启动对象关系图
下面的图回答一个问题:每层拥有何种职责,以及启动中传递的主要对象分别服务于谁。
图1:启动对象关系图解读:
- BootROM、TF-A、U-Boot 都不是 Linux 内核的一部分,它们负责让内核获得一个可预测的执行环境。
- DTB 是 Bootloader 交给内核的“硬件合同”;内核据此初始化内存、CPU、GIC、串口和存储等资源。
- rootfs 的价值不是“有一块磁盘”,而是它必须最终提供一个可被内核执行的 PID 1。
4. 分阶段总结:输入、工作、输出与依赖
4.1 阶段一:上电、复位与 BootROM
输入
核心任务
BootROM 是芯片厂商写进 ROM 的不可修改代码。它通常只做最小且可信的事:
- 确定从 SPI Flash、eMMC、SD、USB、网络等哪个介质启动;
输出
一个早期固件入口被执行。
常见误区
- BootROM 通常无法打印 Linux 风格的串口日志。 如果连 U-Boot banner 都没有,故障可能早于 U-Boot,必须查电源、时钟、启动模式、镜像布局和 BootROM 支持的镜像格式。
- BootROM 的加载格式、偏移、签名方式高度依赖 SoC,不能把某块开发板的流程直接套用到另一块板。
4.2 阶段二:安全固件——以 TF-A 为代表,但并非所有平台都有相同布局
在很多 ARM64 平台上,Trusted Firmware-A(TF-A) 负责 EL3 的安全固件工作。常见镜像分层是:
输入
核心任务
- 初始化 DRAM,使后续大镜像和 DTB 有可用内存;
PSCI 是什么,为什么重要
PSCI(Power State Coordination Interface) 可以先理解为“Linux 请求固件帮忙管理 CPU 电源状态的标准电话协议”。
Linux 在 EL1,通常不能直接可靠地把另一个 CPU 从断电状态启动。它会请求固件:
CPU_ONCPU_OFFCPU_SUSPENDSYSTEM_OFF
PSCI 的价值在于,Linux 不必知道每个 SoC 的具体电源控制寄存器;平台差异被固件封装。
输出
4.3 阶段三:Bootloader——以内核可启动为目标组织所有输入
Bootloader 常见为 U-Boot,但不是唯一选择。UEFI、厂商 Loader 都可能承担同样职责。
输入
核心任务
- 装载 ARM64 Linux
Image 到适当物理内存; - 写入
/chosen/bootargs,或以平台规定方式提供命令行;
ARM64 Linux 的关键交接契约
Linux 6.6.145 的 head.S 将最关键的契约写在源码注释中:
- 内核入口时预期:MMU 关闭、D-cache 关闭;
x0
见 arch/arm64/kernel/head.S:44-54。
进入 primary_entry 后,内核立即保存启动参数。源码将 x0 存入 x21,并同时保留 x0..x3 的原始值,见 arch/arm64/kernel/head.S:161-178。
这说明一个重要的定位原则:
如果 Linux 尚未打印任何 earlycon 日志,不一定是“内核没启动”;也可能是 Bootloader 的跳转状态、Image 装载地址、DTB 地址或缓存/MMU 状态违反了 ARM64 boot protocol。
必经与可选内容
通常必经:
平台可选:
- FIT 镜像、U-Boot
bootm/booti;
4.4 阶段四:ARM64 内核早期启动
这是从“Bootloader 能跳进来”变成“Linux 能用自身虚拟地址和内核 C 代码运行”的阶段。
4.4.1 primary_entry:保存参数,准备页表和 CPU
本地 Linux 6.6.145 的主 CPU 早期路径位于:
arch/arm64/kernel/head.S
核心顺序为:
primary_entry → record_mmu_state → preserve_boot_args → create_idmap → init_kernel_el → __cpu_setup → __primary_switch → start_kernel
源码可见 primary_entry 在 arch/arm64/kernel/head.S:89-124:
record_mmu_state():记录 Bootloader 是否意外带着 MMU 启动;preserve_boot_args()create_idmap()init_kernel_el():将 CPU 整理到 Linux 所需 EL1 环境;__cpu_setup():准备 TCR、MAIR 等 MMU 相关控制寄存器;- 随后切换到内核页表并进入 C 函数
start_kernel()。
4.4.2 恒等映射为什么不可少
恒等映射(identity mapping,idmap) 指虚拟地址等于物理地址的临时映射。
开启 MMU 是一个“脚下地板会改变”的动作:
- 如果当前正在执行的下一条指令没有有效映射,CPU 会立即取指异常。
因此内核在一段特殊的 .idmap.text 代码中执行页表切换,使当前路径在 MMU 开启前后都能继续被正确访问。
4.4.3 MMU、TTBR 与页表的核心因果
MMU(Memory Management Unit,内存管理单元) 负责把 CPU 发出的虚拟地址翻译成物理地址,并按页表权限进行检查。
ARM64 中:
TTBR0_EL1:通常用于低地址范围的映射,早期启动中可暂用于 idmap;TTBR1_EL1TCR_EL1MAIR_EL1SCTLR_EL1.M
Linux 的 __enable_mmu 会写入 TTBR0_EL1 和 TTBR1_EL1,随后设置 SCTLR_EL1 以使 MMU 生效;见 arch/arm64/kernel/head.S:721-749。
4.4.4 异常向量
ARM64 的异常不是 x86 的 IDT。它通过 VBAR_EL1 指向异常向量表。
主核与次级 CPU 均需要在适当时机设置:
VBAR_EL1 → vectors
次级 CPU 的路径可在 arch/arm64/kernel/head.S:657-680 看到:
finalise_el2()- 进入
secondary_start_kernel()。
4.5 阶段五:start_kernel()——通用内核建立可运行环境
当 ARM64 汇编已经完成最小地址空间和 CPU 状态建设后,内核进入:
void __init __noreturn start_kernel(void)
位置:init/main.c:887。
前半段:不能依赖中断和完整调度
start_kernel() 一开始明确关闭本地中断:
local_irq_disable();early_boot_irqs_disabled = true;
见 init/main.c:899-905。
此时系统并不是“完全不能运行”,而是必须严格避免依赖还没初始化完成的中断、时钟、调度和驱动框架。
关键路径包括:
boot_cpu_init()setup_arch(&command_line)setup_command_line()setup_nr_cpu_ids()、setup_per_cpu_areas():建立 CPU 数量和 per-CPU 区域;smp_prepare_boot_cpu()parse_early_param()mm_core_init()sched_init()init_IRQ()、tick_init()、timekeeping_init()、time_init():中断、tick、时间体系;local_irq_enable()console_init()
对应源码区间可重点阅读:
init/main.c:887-1085
setup_arch():ARM64 与设备树真正接合的地方
ARM64 的 setup_arch() 位于:
arch/arm64/kernel/setup.c:297-393
核心动作:
setup_machine_fdt(__fdt_pointer);arm64_memblock_init();paging_init();unflatten_device_tree();psci_dt_init();init_bootcpu_ops();smp_init_cpus();smp_build_mpidr_hash();
它们的因果关系是:
setup_machine_fdt(__fdt_pointer)使用启动时保存的 DTB 指针验证和读取硬件信息。arm64_memblock_init()将 DTB 中的内存节点、保留内存等转换为早期内存管理器 memblock 的记录。memblock 可以理解为“伙伴系统尚未就绪前,内核使用的简化物理内存账本”。paging_init()建立更完整的内核页表与线性映射,为后续的常规内存管理做准备。unflatten_device_tree()将扁平二进制 DTB 转换为内核中的设备树节点结构,供 OF(Open Firmware)框架和驱动匹配使用。psci_dt_init()从 DT 的 /psci 节点解析 PSCI 调用方式,如 SMC 或 HVC。smp_init_cpus() 与 smp_build_mpidr_hash()根据 DT 描述的 CPU 节点建立 Linux 逻辑 CPU 编号与硬件 MPIDR 的映射。
注意:若系统采用 ACPI 而非 DT,路径会转向 ACPI 的表解析;本地源码中也明确根据 acpi_disabled 分支选择 psci_dt_init() 或 psci_acpi_init(),见 arch/arm64/kernel/setup.c:369-376。
4.6 阶段六:PSCI、SMP 与次级 CPU online
多核启动的核心问题是:
主核已经进入 Linux 后,如何让尚未运行的物理 CPU 获得正确入口、页表、栈和调度上下文,并被安全标记为 online?
4.6.1 逻辑 CPU 与物理 CPU
Linux 中的 CPU0、CPU1 是逻辑编号;硬件通过 MPIDR 标识 CPU 的层级拓扑身份。
DT 中 CPU 节点提供硬件标识,内核建立:
逻辑 CPU 编号 ↔ MPIDR 硬件标识 ↔ CPU 启动操作
本地 setup_arch() 在解析 CPU 后调用:
smp_init_cpus();smp_build_mpidr_hash();
见 arch/arm64/kernel/setup.c:374-376。
4.6.2 启动次级 CPU 的完整因果链
通用内核走到 kernel_init_freeable() 后,开始允许 SMP 完整上线:
smp_prepare_cpus(setup_max_cpus);smp_init();sched_init_smp();
见 init/main.c:1547-1559。
对单个次级 CPU,ARM64 关键路径为:
CPU online 请求 → __cpu_up() → secondary_data.task = idle → update_cpu_boot_status(CPU_MMU_OFF) → boot_secondary() → CPU 操作集 cpu_boot() → 平台 PSCI CPU_ON → 次级 CPU 从固件给定入口开始执行 → secondary_entry / secondary_holding_pen → __cpu_setup() → __enable_mmu() → __secondary_switched → secondary_start_kernel() → CPU 标记 online
__cpu_up() 的源码在 arch/arm64/kernel/smp.c:112-174:
- 先把次级 CPU 应使用的 idle 任务栈保存在
secondary_data.task; - 若 CPU 没有 online,则读取
secondary_data.status 和 __early_cpu_boot_status 判断卡在哪个早期状态。
这是很实用的故障定位设计:主 CPU 并非只能得到“次核没起来”,还能区分“MMU 未打开”“不支持页粒度”“不支持 52 位 VA”等早期失败原因。
4.6.3 次级 CPU 的两类入口
head.S 有两种次级 CPU 入口语义:
secondary_holding_pen用于某些平台让次核先在 holding pen 等待。它不断比对 secondary_holding_pen_release,未轮到自己就执行 wfe 低功耗等待。见 arch/arm64/kernel/head.S:612-628。secondary_entry用于 CPU 被内核动态带起的场景。它直接初始化 EL 状态,再进入共同的 secondary_startup。见 arch/arm64/kernel/head.S:630-655。
无论从哪条入口进入,最终都要完成:
- 调用
secondary_start_kernel()。
4.6.4 CPU hotplug 的本质
CPU hotplug 是运行中 CPU 的上线和下线机制,不只是“启动阶段多核初始化”。
- online:把一个 CPU 从离线状态带入 Linux 调度和中断体系;
- offline:迁移任务、中断和 per-CPU 工作,停止 CPU,并常通过 PSCI
CPU_OFF 让其进入低功耗状态。
启动阶段的 CPU bring-up 与运行时 hotplug 共享许多低层机制,例如 CPU operation、PSCI、页表与次级入口;但运行时 hotplug 还要面对任务迁移、CPUHP 状态机和资源回收问题。
5. 主路径流程图
这张图回答:从复位到 PID 1 的必经控制路径是什么,在哪些位置会发生关键状态切换。
图2:主路径流程图解读:
开启 MMU 是内核早期最危险的转折点:页表、执行地址、缓存与屏障必须一致。解析 DTB 早于大部分设备驱动初始化,因为没有硬件资源描述,驱动无法正确匹配和获得寄存器、中断、时钟等资源。执行 init 是内核到用户空间的边界;它不是一次普通 fork,而是内核第一次通过 kernel_execve() 启动用户态程序。
6. 时序图:一次成功启动中各层如何交接
图3:固件、Bootloader 与主 CPU 的启动交接
图4:主 CPU、次级 CPU 与用户空间的启动交接解读:
- 固件和内核之间的 PSCI 调用不是普通函数调用,而是跨异常级的服务请求,底层可能通过 SMC 或 HVC 指令完成。
- 次级 CPU 的“启动成功”不是固件返回成功就结束,而是必须经过内核早期汇编、MMU 启用、C 级初始化,并最终被 Linux 标记为 online。
- init 的成功表示内核已经完成“把机器带起来”的职责;之后系统策略,例如网络服务、挂载额外文件系统、图形界面,主要由用户空间负责。
7. 从 start_kernel() 到用户态 init 的关键阶段
7.1 rest_init():首次建立内核线程与 PID 1 的执行上下文
start_kernel() 完成核心初始化后调用 arch_call_rest_init();默认实现直接进入 rest_init(),见:
init/main.c:838-841init/main.c:1084-1085
rest_init() 位于 init/main.c:698 附近,其核心角色是:
- 创建
kernel_init,它将成为最终负责准备 rootfs 并执行 init 的内核线程;
7.2 kernel_init_freeable():让系统具备“运行完整初始化”的条件
在 init/main.c:1535-1590,关键顺序是:
smp_prepare_cpus(setup_max_cpus);workqueue_init();smp_init();sched_init_smp();do_basic_setup();wait_for_initramfs();console_on_rootfs();prepare_namespace();
其意义:
smp_prepare_cpus() / smp_init():准备和拉起次级 CPU;workqueue_init()do_basic_setup():初始化驱动核心,并按 initcall 等级调用内建驱动初始化函数;wait_for_initramfs()prepare_namespace():在没有早期 initramfs init 时,准备并挂载最终 rootfs;console_on_rootfs():打开 /dev/console 并将其复制为 init 的标准输入、输出、错误输出。
7.3 initcall:内建驱动为什么不是随意初始化
内核把大量初始化函数分成层级:
early → pure → core → postcore → arch → subsys → fs → device → late
Linux 6.6.145 中,do_initcalls() 顺序执行这些层级,见 init/main.c:1269-1331;do_basic_setup() 执行 driver_init() 和 do_initcalls(),见 init/main.c:1340-1347。
这不是装饰性的分类。它表达依赖关系,例如:
启动定位中可加:
initcall_debug
内核会输出每个 initcall 的调用与耗时,有助于定位“启动卡在某个内建驱动”的问题。
8. 启动状态图
这张图回答:系统何时真正从“不可用”跨越到“可调度、可挂载、可运行用户程序”。
图5:启动状态图解读:
- “内核解压成功”不代表 Linux 已可用;在 ARM64 raw
Image 流程中,重点是入口状态、页表切换和 start_kernel() 早期初始化。 - “看到了内核 banner”不代表 rootfs 成功;rootfs、init 和用户服务是之后的独立阶段。
- “CPU1 没上线”也不一定表示内核整体失败;它通常属于 SMP bring-up 或 PSCI/CPU DT 描述问题。
上篇小结
从芯片复位到 PID 1,启动的本质是逐层建立下一阶段所需的最小运行前提:固件提供可信执行环境,Bootloader 完成镜像与参数交接,内核建立地址空间并初始化资源,最终由 init 接管用户空间。下篇将转向实战:如何识别启动产物、依据最后一条日志划分故障边界,并用 QEMU 验证关键路径。
我是熊树先生,持续记录 Linux 内核、嵌入式系统与底层技术学习,分享内核源码分析、驱动开发和系统软件实践。
如果你觉得今天这篇有收获,欢迎点赞、在看、转发三连,我们下篇见。