title: 【从零写OS内核】335 ARM 架构启动:从上电到 Kernel
category: os
tag: [arm, boot, embedded, u-boot]
source: https://github.com/golang12306/os-kernel-from-scratch
【从零写OS内核】335 ARM 架构启动:从上电到 Kernel
1. ARM 启动序列全景图
嵌入式设备的启动是一个层层接力赛。理解 ARM 架构启动,先从宏观上把握整个流程:
┌────────────────────────────────────────────────────────────┐
│ ARM 启动序列 │
│ │
│ POR (Power-On Reset) │
│ │ │
│ ▼ │
│ ┌─────────┐ ── ROM (iROM) ── 第一阶段 Bootloader │
│ │ BL1 │ (固化代码,厂商写死) │
│ └────┬────┘ │
│ │ copy to SRAM │
│ ▼ │
│ ┌─────────┐ ── SRAM ── 第二阶段 Bootloader │
│ │ BL2 │ (可修改,通常是 U-Boot SPL) │
│ └────┬────┘ │
│ │ jump to DRAM │
│ ▼ │
│ ┌─────────┐ ── DRAM ── U-Boot (完整的 U-Boot) │
│ │ U-Boot │ 加载 Kernel + DTB │
│ └────┬────┘ │
│ │ bootm / booti │
│ ▼ │
│ ┌─────────┐ ── DRAM ── Linux Kernel │
│ │ Kernel │ 解压自展,进入 ARM Linux │
│ └────┬────┘ │
│ │ start_kernel() │
│ ▼ │
│ ┌─────────┐ ── DRAM ── Init (PID 1) │
│ │ Init │ 用户态第一个进程 │
│ └─────────┘ │
└────────────────────────────────────────────────────────────┘2. 三星 Exynos 4412 启动流程详解
以三星 Exynos 4412(常见于 ARM 开发板)为例子,这是 ARM SoC 启动的典型代表。
2.1 BL1 — 固化在 iROM
CPU 上电复位向量指向 iROM(芯片内部只读存储):
# Exynos 4412 iROM 布局(简化)
iROM Base: 0x0000_0000
Vector Table:
b reset_handler @ 0x0000_0000 Reset
b undefined_handler @ 0x0000_0004 Undefined
b svc_handler @ 0x0000_0008 Supervisor
b prefetch_handler @ 0x0000_000C Prefetch Abort
b data_handler @ 0x0000_0010 Data Abort
b . @ 0x0000_0014 (Reserved)
b irq_handler @ 0x0000_0018 IRQ
b fiq_handler @ 0x0000_001C FIQ
reset_handler:
# 读取 OM 引脚判断启动设备
ldr r0, =0x1002_0000 @ OM (Operating Mode) reg
ldr r1, [r0]
and r1, r1, #0x7 @ mask boot source
cmp r1, #0x0 @ NAND
cmp r1, #0x1 @ SD卡
cmp r1, #0x2 @ eMMC
# ...2.2 BL2 — U-Boot SPL
SPL(Secondary Program Loader)是一个精简版 U-Boot,负责:
- 初始化 DRAM 控制器
- 从外部存储加载完整 U-Boot 到 DRAM
- 跳转执行
# SPL 代码关键片段(arch/arm/cpu/armv7/exynos/board.c)
void spl_board_init(void)
{
// 1. GPIO 初始化
exynos_pinmux_config(PIN_MMC, 0);
// 2. 时钟初始化
system_clock_init();
// 3. DDR3 初始化(关键!)
dmc_init(DDR3);
// 4. 从 MMC 加载完整 U-Boot
movi_read_blk(MOVI_BLKSIZE, MOVI_BLKPOS);
// 5. 跳转
jump_to_copy();
}2.3 时钟初始化对照表
┌────────────────────────────────────────────────────────┐
│ Exynos 4412 ARMCLK 时钟树 │
├────────────────────────────────────────────────────────┤
│ │
│ APLL (1.5GHz) ──┬── ARMCLK ─── CPU Core │
│ │ │
│ └── HCLK ──┬── AXIbus │
│ │ │
│ MPLL (800MHz) ─── KCLK ─────┴── PCLK ── 外设 │
│ │
│ 启动初期:APLL未配置,CPU跑在 24MHz XTAL 输入时钟 │
│ SPL 阶段:配置 APLL → 1.5GHz → ARMCLK │
└────────────────────────────────────────────────────────┘3. ARM 异常向量表
ARM 有 7 种异常模式,每种对应一个入口:
# ARM 异常向量表(必须在 0x0000_0000 或 0xFFFF_0000)
.section .vectors
.globl _start
_start:
b reset @ Supervisor Mode (复位)
ldr pc, _undefined @ Undefined Mode
ldr pc, _svc @ Supervisor Mode (SVC)
ldr pc, _pabort @ Prefetch Abort
ldr pc, _dabort @ Data Abort
nop @ Reserved
ldr pc, _irq @ IRQ
ldr pc, _fiq @ FIQ
_undefined: .word undefined_handler
_svc: .word svc_handler
_pabort: .word pabort_handler
_dabort: .word dabort_handler
_irq: .word irq_handler
_fiq: .word fiq_handler
reset:
# 设置栈指针,切换到 SVC 模式
mrs r0, cpsr
bic r0, r0, #0x1F @ Clear mode bits
orr r0, r0, #0x13 @ Set SVC mode
msr cpsr_c, r0 @ Write CPSR
ldr sp, =0x4000_0000 @ 设置栈顶(DDR3起始)
bl main @ 跳转到 C 代码异常模式与寄存器
┌─────────────────────────────────────────────────────┐
│ ARM 处理器模式与寄存器分组 │
├─────────────────────────────────────────────────────┤
│ │
│ User/System: r0-r12, sp, lr, pc, cpsr │
│ ─────────────────────────────────────────────────── │
│ FIQ: r0-r12, sp_fiq, lr_fiq, pc, cpsr │
│ (r8_fiq ~ r12_fiq 独立寄存器) │
│ IRQ: r0-r12, sp_irq, lr_irq, pc, cpsr │
│ Supervisor: r0-r12, sp_svc, lr_svc, pc, cpsr │
│ Abort: r0-r12, sp_abt, lr_abt, pc, cpsr │
│ Undefined: r0-r12, sp_und, lr_und, pc, cpsr │
│ │
│ SPSR (Saved PSR):每种异常模式有自己的 SPSR │
│ ─────────────────────────────────────────────────── │
│ 例:IRQ 发生时硬件自动: │
│ LR_irq = PC + 4 (或者 PC+8,取决于 ARM 版本) │
│ SPSR_irq = CPSR │
│ CPSR[4:0] = 0x12 ( IRQ 模式) │
│ CPSR[7] = 1 (关闭 IRQ) │
└─────────────────────────────────────────────────────┘4. U-Boot 启动命令链
U-Boot 启动后交互流程:
U-Boot 2017.05 (Jan 15 2026)
CPU: Exynos4412 @ 1.5GHz
Board: SMDK4412
DRAM: 1024 MiB
MMC: EXYNOS DWMMC: 0
# U-Boot 启动 Linux 内核的标准命令链
# 方式一:bootz(推荐 ARM64)
bootz 0x4000_7000 - 0x4000_1000
# ^kernel addr ^initrd addr (无则填 -)
# ^dtb address
# 方式二:bootm(ARM32)
bootm 0x4000_7000 0x4200_0000 0x4000_1000
# kernel initrd dtb
# 方式三:自动脚本(boot.scr)
load mmc 0:1 0x4000_1000 boot.scr
source 0x4000_10004.1 bootz 源码流程
┌──────────────────────────────────────────────────────┐
│ bootz 命令执行流程 │
├──────────────────────────────────────────────────────┤
│ │
│ do_bootz() │
│ │ │
│ ├── do_bootm_states(BOOTM_STATE_START ...) │
│ │ │ │
│ │ ├── locate_kernel() ← 找到 kernel │
│ │ └── bootm_start() ← 初始化启动信息 │
│ │ │
│ ├── bootm_load_os() ← 解压 kernel │
│ │ │ (若压缩则 gunzip) │
│ │ │ │
│ ├── bootm_find_dtb() ← 找到 DTB │
│ │ │ │
│ ├── bootm_find_initrd() ← 找到 initrd │
│ │ │ │
│ ├── bootm_run_os() ← 真正跳转执行 │
│ │ │ → aarch64_boot_kernel() │
│ │ │ → kernel_entry(0, mach_id, &info) │
│ │ │ │
│ └── do_bootm_states(BOOTM_STATE_FOLS ...) │
│ │
└──────────────────────────────────────────────────────┘5. ARM64 启动协议(bootz/booti)
Linux ARM64 使用统一启动协议:
┌─────────────────────────────────────────────────────────────┐
│ ARM64 Linux 内核启动要求(boot protocol) │
├─────────────────────────────────────────────────────────────┤
│ │
│ 寄存器约定(ARM64Calling Convention): │
│ ───────────────────────────────────────────────────────── │
│ x0 = 操作系统参数(DTB物理地址 or 0) │
│ x1 = 0 (保留) │
│ x2 = 0 (保留) │
│ x3 = 0 (保留) │
│ x4..x7 = 0 (保留,未来可能传 initrd 地址) │
│ │
│ 镜像必须加载到: │
│ 文本起始 >= 0x80000 (512KB 偏移,避免错误映射区域) │
│ │
│ DTB 要求: │
│ 必须位于 512MB 范围内(内核通过简单映射访问) │
│ 必须是 Linux 要求的正确版本 (e.g. FDT) │
│ │
│ ARM64 kernel image layout: │
│ ┌────────────────────┐ │
│ │ kernel magic │ "OSK" (0x644d5241) │
│ ├────────────────────┤ │
│ │ text + rodata │ 内核代码段 │
│ ├────────────────────┤ │
│ │ rodata │ 只读数据 │
│ ├────────────────────┤ │
│ │ rwdata │ 可写数据 │
│ ├────────────────────┤ │
│ │ bss │ 未初始化全局变量 │
│ └────────────────────┘ │
│ 内核解压后自己会重新安置到正确的虚拟地址 │
└─────────────────────────────────────────────────────────────┘6. 寄存器abank 对比:ARM vs x86 Boot
| 阶段 | ARM | x86 |
|---|---|---|
| 复位向量 | 0x0000_0000 | 0xFFFF_FFF0 (BIOS ROM) |
| 初始栈 | 手动设置(无默认) | BIOS 自动设置 |
| 内存探测 | U-Boot/DDR driver | BIOS POST |
| 驱动模型 | 编译时固化 | PCI/ACPI 自动发现 |
| Bootloader | U-Boot/SPL | GRUB/Coreboot |
| 内核协议 | bootz/booti | bzImage |
7. Linux Kernel ARM 启动入口
// arch/arm64/kernel/head.S 关键伪代码
/*
* stext 是 ARM64 内核真正入口
* 入口时 MMU 已关闭(内存直接映射)
* 页表已建立好
*/
ENTRY(stext)
bl preserve_boot_args @ 保存启动参数
adrp x23, early_pgtable @ 页表基地址
bl el2_setup @ 如果是 EL2 跳 EL1
bl __cpu_setup @ 初始化 CPU 寄存器
b start_kernel @ 跳转到 C 代码
END(stext)8. 调试实践
# 查看 ARM 开发板启动输出(U-Boot 串口)
# 波特率 115200 8N1
screen /dev/ttyUSB0 115200
# 在 U-Boot 中手动加载内核和 DTB
load mmc 0:1 0x4000_7000 uImage
load mmc 0:1 0x4000_1000 exynos4412-scp.dtb
setenv bootargs "console=ttySAC0,115200 root=/dev/mmcblk0p2"
bootm 0x4000_7000 - 0x4000_1000
# 查看 ARM CPU 信息
adb shell cat /proc/cpuinfo # Android
# 或者
cat /proc/cpuinfo # Linux PC (如果 QEMU ARM)
# 使用 QEMU 模拟 ARM vexpress-a9 启动 Linux
qemu-system-arm
-M vexpress-a9
-kernel zImage
-dtb vexpress-v2p-ca9.dtb
-append "console=ttyAMA0"
-nographic9. 小结
ARM 启动完整链路:
iROM (BL1, 固化) → SRAM (BL2, SPL) → DRAM (U-Boot)
│
bootm/bootz
│
┌─────┴─────┐
│ │
Kernel DTB
│
start_kernel()
│
Init (PID 1)
关键里程碑:
- iROM: 固化代码,只读,芯片厂商提供
- SPL: 初始化 DDR,搬运 U-Boot
- U-Boot: 完整 bootloader,加载 kernel + dtb
- Kernel: 解压自展,建立页表,start_kernel()
- Init: 第一个用户进程理解 ARM 启动,关键是理解分段接力:每段 bootloader 只做一件事(初始化 DRAM、加载下一段),然后跳转。Windows 的 BIOS + MBR + Bootmgr 也是类似的分级思想,只是没有嵌入式那么多层。
微信公众号
· 深入浅出技术解读 ☕️
· 欢迎搜索关注「码农的螺丝刀」
评论