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 架构启动,先从宏观上把握整个流程:

Bash
┌────────────────────────────────────────────────────────────┐
│                     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(芯片内部只读存储):

Bash
# 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,负责:

  1. 初始化 DRAM 控制器
  2. 从外部存储加载完整 U-Boot 到 DRAM
  3. 跳转执行

Bash
# 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 时钟初始化对照表

Bash
┌────────────────────────────────────────────────────────┐
│          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 种异常模式,每种对应一个入口:

Asm
# 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 代码

异常模式与寄存器

Bash
┌─────────────────────────────────────────────────────┐
│  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 启动后交互流程:

Bash
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_1000

4.1 bootz 源码流程

Bash
┌──────────────────────────────────────────────────────┐
│              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 使用统一启动协议:

Bash
┌─────────────────────────────────────────────────────────────┐
│         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 启动入口

C
// 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. 调试实践

Bash
# 查看 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" 
    -nographic

9. 小结

Bash
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 也是类似的分级思想,只是没有嵌入式那么多层。


微信公众号
· 深入浅出技术解读 ☕️
· 欢迎搜索关注「码农的螺丝刀」

最后修改: 2024年7月16日

作者

评论

发表评论

您的邮箱地址不会被公开。