乐于分享
好东西不私藏

还不理解uboot启动流程?从源码_start汇编 开始一步步带你踏进 C 世界

还不理解uboot启动流程?从源码_start汇编 开始一步步带你踏进 C 世界

全景介绍

第一篇我们搭好了 QEMU ARM64 virt 平台 + U-Boot + GDB 调试环境,今天接着走:从芯片复位后第一条指令开始,用 GDB 跟着指令一步步走,一直走到 board_init_r,把藏在这里的 ARM64 启动知识点拆明白。

很多人看 U-Boot 启动代码,总觉得汇编阶段"太黑":异常等级怎么处理、PIE 重定位在做什么、栈到底在哪配的、BSS 段什么时候清的。今天直接用 GDB 踩一遍。

你会发现 U-Boot 汇编段其实特别规整——检测异常等级 → 配异常向量 → lowlevel 初始化 → 进 C 环境,步进清晰。

环境准备

继续用第一篇的环境:QEMU 虚拟 ARM64 virt 平台(qemu-system-aarch64 -machine virt -cpu cortex-a57),U-Boot v2024.07,GDB 远程调试端口 1234。

确保你能跑到 U-Boot 提示符,GDB 能连上,我们直接开始走代码。

起点:芯片复位,第一条指令从哪里执行

芯片松开复位那一刻,ARM64 硬件做了这几件事:PC = 0x0,停在 EL3,MMU 和 cache 全部禁用,所有寄存器清零。

但实际嵌入式产品,U-Boot 的复位入口不一定是 0x0:ROM code 从 SPI Flash/eMMC 读取第一段代码,SPL 把 U-Boot proper 拷贝到 DDR 高地址,跳转到 _start 开始执行。

U-Boot v2024.07 源码 arch/arm/cpu/armv8/start.S

1
2
3
4

.globl    _start_start:    b    reset

最简情况就是 b reset,直接跳到复位处理入口。

reset 入口:保存启动参数,先清理 PIE 重定位

save_boot_params 是一个弱函数,板级代码可以覆盖它。QEMU 把 U-Boot 当 Linux kernel 启动时,x0 传入的是设备树(FDT)在 DDR 中的地址。默认实现直接 b save_boot_params_ret 返回。之后是 PIE 重定位修正(如果启用了位置无关编译)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

#if CONFIG_POSITION_INDEPENDENT && !defined(CONFIG_SPL_BUILD)pie_fixup:    adr    x0, _start    ldr    x1, _TEXT_BASE    subs    x9, x0, x1    beq    pie_fixup_done    adrp    x2, __rel_dyn_start    add    x2, x2, #:lo12:__rel_dyn_start    adrp    x3, __rel_dyn_end    add    x3, x3, #:lo12:__rel_dyn_endpie_fix_loop:    ldp    x0, x1, [x2], #16    ldr    x4, [x2], #8    cmp    w1, #1027               /* R_AARCH64_RELATIVE? */    bne    pie_skip_reloc    add    x0, x0, x9    add    x4, x4, x9    str    x4, [x0]pie_skip_reloc:    cmp    x2, x3    b.lo    pie_fix_looppie_fixup_done:#endif

  • • adr x0, _start:取 _start 的实际运行地址
  • • ldr x1, _TEXT_BASE:取链接时指定的基地址(.quad CONFIG_TEXT_BASE
  • • 如果两者不等,遍历 .rela.dyn 段修正所有 R_AARCH64_RELATIVE 类型地址

QEMU 调试时 _start 的运行地址和链接地址一致(0x00000000),因为 CONFIG_SYS_TEXT_BASE=0x00000000,QEMU 将 U-Boot 直接加载到 Flash 首地址,所以 subs x9, x0, x1 结果为 0,直接跳过。之后 relocate_code 会把 U-Boot 从 0x00000000(Flash)拷贝到 relocaddr(DDR 高地址),搬迁后运行地址就变了。PIE 确保搬迁后代码仍正确运行。

检测异常等级,为当前 EL 设置异常向量表

PIE 修正完后,设置当前异常等级的异常向量表和寄存器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

    adr    x0, vectors    switch_el x1, 3f, 2f, 1f3:    set_vbar vbar_el3, x0    mrs    x0, scr_el3    orr    x0, x0, #0xf    msr    scr_el3, x0    msr    cptr_el3, xzr    b    0f2:    mrs    x1, hcr_el2    tbnz    x1, #HCR_EL2_E2H_BIT, 1f    orr    x1, x1, #HCR_EL2_AMO_EL2    msr    hcr_el2, x1    set_vbar vbar_el2, x0    mov    x0, #0x33ff    msr    cptr_el2, x0    b    0f1:    set_vbar vbar_el1, x0    mov    x0, #3 << 20    msr    cpacr_el1, x00:    msr    daifclr, #0x4

关键结论:U-Boot 不会主动在 reset 段切换异常等级。它调用 switch_el 宏检测 CurrentEL 寄存器,根据当前 EL 做相应配置:EL3 设 SCR_EL3/CPTR_EL3,EL2 设 HCR_EL2/CPTR_EL2,EL1 设 CPACR_EL1。

switch_el 宏的定义arch/arm/include/asm/macro.h):

异常等级快速回顾

ARMv8-A 体系结构定义了四个异常等级:

等级
CurrentEL 值
权限
典型使用者
EL3
0xc
最高
安全监控器 / ATF
EL2
0x8
次高
虚拟机监视器 / 部分平台 U-Boot
EL1
0x4
内核级
Linux 内核 / 少数平台 U-Boot(需 ATF 显式切 EL1)
EL0
0x0
最低
用户态应用程序

QEMU virt 调试环境:取决于是否有 ATF。无 ATF 时 U-Boot 从 EL3 开始(currentel = 0xc);有 ATF 时通常被切到 EL2currentel = 0x8)。

异常向量表的结构

adr x0, vectors 指向异常向量表,定义在 arch/arm/cpu/armv8/exceptions.S 中。.align 11 = 2048 字节对齐(ARM64 架构强制),每个 对齐 7(128 字节)是一个向量表项。U-Boot 默认实现全部调用 do_bad_* 函数——遇到异常直接报错。

c_runtime_cpu_setup 重定位向量表

向量表设置分两步:汇编阶段 start.S 设置当前 EL 的 VBAR;relocation 后调用 c_runtime_cpu_setup 重新设置 VBAR——因为 vectors 也搬到新地址了。

1
2
3
4
5
6
7
8
9
10
11

ENTRY(c_runtime_cpu_setup)    adr    x0, vectors    switch_el x1, 3f, 2f, 1f3:    msr    vbar_el3, x0    b    0f2:    msr    vbar_el2, x0    b    0f1:    msr    vbar_el1, x00:    retENDPROC(c_runtime_cpu_setup)

lowlevel_init:核心外设初始化

设置完异常向量表后,执行 bl lowlevel_init:主 CPU 初始化 GIC(通用中断控制器),多核系统中从 CPU 通过 eret 实现 EL 切换。

进 _main:配置栈,进 C 初始化

1
2
3
4

master_cpu:    msr    SPSel, #1    bl    _main

_main 定义在 arch/arm/lib/crt0.S,是整个启动链路中最关键的转折点:

1
2
3
4
5
6
7
8
9
10
11

ENTRY(_main)    ldr    r0, =(SYS_INIT_SP_ADDR)    bic    r0, r0, #7    mov    sp, r0    bl    board_init_f_alloc_reserve    mov    sp, r0    mov    r9, r0    bl    board_init_f_init_reserve    mov    r0, #0    bl    board_init_f

SYS_INIT_SP_ADDR 指向一段可用的 SRAM 或 DDR 临时栈空间。AArch64 栈须 16 字节对齐。

board_init_f → relocate_code → CLEAR_BSS → board_init_r

_main 完整流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

_main  ├─ 配置栈(SYS_INIT_SP_ADDR → sp)  ├─ board_init_f_alloc_reserve → 分配 GD 空间  ├─ board_init_f_init_reserve → 初始化 GD  ├─ board_init_f(0) → 第一阶段 C 初始化  │    └─ 初始化串口、定时器、检测 DDR 大小  │    └─ 决定重定位目标地址 gd->relocaddr  │    └─ 设置 gd->start_addr_sp、gd->new_gd  ├─ relocate_code(gd->relocaddr) → 拷贝 U-Boot 自身到新地址  ├─ relocate_vectors → 重定位异常向量表  ├─ c_runtime_cpu_setup → 重新设置 VBAR 指向新地址  ├─ CLEAR_BSS → 清空 BSS 段  └─ board_init_r(gd, gd->relocaddr) → 完整 U-Boot 初始化       └─ main_loop → 进入命令行交互

relocate_code:完整的重定位过程

board_init_f 检测 DDR 大小后计算出 gd->relocaddr(DDR 顶部附近),relocate_code 把整个镜像从 0x00000000(Flash)拷贝到 relocaddr(DDR 高地址):

1
2
3

relocate_code(relocaddr):  memcpy(relocaddr, _start, _end - _start)

启用 PIE 模式下不需要符号修正——PIE 可执行文件本来就可浮动。

CLEAR_BSS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

.macro CLEAR_BSS    ldr    r0, =__bss_start#ifdef CONFIG_USE_ARCH_MEMSET    ldr    r3, =__bss_end    mov    r1, #0x00000000    subs    r2, r3, r0    bl    memset#else    ldr    r1, =__bss_end    mov    r2, #0x00000000clbss_l:cmp    r0, r1    strlo    r2, [r0]    addlo    r0, r0, #4    blo    clbss_l#endif.endm

逐 8 字节(ARM64)或 4 字节(ARM32)清零。xzr 是 ARM64 的零寄存器。

重定位与符号修正:两阶段机制

U-Boot 的地址修正是两阶段的,很多人把它们搞混了:

第一阶段:pie_fixup(reset 段,搬迁前)

只在启用 PIE 时执行。此时 U-Boot 还在 Flash(0x00000000),pie_fixup 做的事:

  1. 1. adr x0, _start 取运行地址,ldr x1, _TEXT_BASE 取链接地址
  2. 2. subs x9, x0, x1 算出偏移量
  3. 3. 遍历 .rela.dyn 段,把所有 R_AARCH64_RELATIVE 类型的地址加上偏移量

这一步修正的是编译时生成的绝对地址引用(比如函数指针、全局变量地址)。修正后,代码中的绝对地址都变成了运行时正确的值。

QEMU 调试时链接地址=运行地址(都是 0x0),所以偏移量为 0,这段直接跳过。

第二阶段:relocate_code(_main 中,board_init_f 之后)

不管有没有 PIE 都会执行(只要需要搬迁)。做的事:

  1. 4. memcpy(relocaddr, _start, image_size) — 把整个 U-Boot 拷贝到 DDR 高地址
  2. 5. 如果没启用 PIE:修正绝对符号引用(rel_dyn_start 到 rel_dyn_end 的重定位项)
  3. 6. 如果已启用 PIE:第一阶段已经修正过了,这里只拷贝不修正
场景
第一阶段 pie_fixup
第二阶段 relocate_code
PIE 开,搬迁前链接=运行
跳过(偏移=0)
只拷贝
PIE 开,搬迁前链接≠运行
修正 .rela.dyn
只拷贝
PIE 关
不执行
拷贝 + 修正绝对符号

ARM64 启动全流程一览

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

复位(_start)  │  ├─ b save_boot_params  │  ├─ pie_fixup (if PIE)  │    └─ 修正 .rela.dyn 中 R_AARCH64_RELATIVE 类型地址  │  ├─ switch_el → 设置 VBAR  │    ├─ EL3: SCR_EL3 + CPTR_EL3  │    ├─ EL2: HCR_EL2 + CPTR_EL2  │    └─ EL1: CPACR_EL1  │  ├─ bl lowlevel_init  │    └─ (多核) 从 CPU 切到 EL2/EL1  │  ├─ master_cpu: msr SPSel,#1 / bl _main  │    │  │    └─ _main (crt0.S):  │         ├─ sp = SYS_INIT_SP_ADDR  │         ├─ board_init_f_alloc_reserve  │         ├─ board_init_f(0)  │         ├─ relocate_code  │         ├─ relocate_vectors  │         ├─ c_runtime_cpu_setup  │         ├─ CLEAR_BSS  │         └─ board_init_r() → main_loop

GDB 实战调试

QEMU 启动 U-Boot 后,在另一个终端连 GDB:

1
2
3
4
5
6
7

# 终端1:QEMU(已启动,等待 GDB 连接)qemu-system-aarch64 -machine virt -cpu cortex-a57 \  -bios u-boot.bin -nographic -S -s# 终端2:GDBaarch64-linux-gnu-gdb u-boot

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

# 连接 QEMU GDB server(gdb) target remote :1234# 在复位入口设硬件断点(地址 0x0 可能是 ROM/Flash,必须用硬件断点)(gdb) hbreak *0x00000000(gdb) c# 停在 _start,看当前指令(gdb) x/i $pc=> 0x0: b  0x60 <reset># 看异常等级:读 CPSR 的 M[3:0] 位(bits[4:2] 就是 EL)(gdb) print/x $cpsr$1 = 0x3c5          # 低 5 位 = 0b11101 → EL3h# EL 编码:0b10001=EL1h, 0b10010=EL2h, 0b11011=EL3h# 单步走 10 条指令,看 PIE 修正是否执行(gdb) stepi 10(gdb) info registers x0x0             0x00000000# 在 _main 设断点,一路跑到 C 入口(gdb) hbreak _main(gdb) c# 看栈指针是否配好(gdb) info registers spsp             0x4008f000

为什么用 hbreak 不用 b?b(软件断点)靠往内存写 trap 指令实现,地址 0x0 可能是只读的 ROM/Flash,写不进去。hbreak 用 CPU 硬件断点寄存器,不改内存,任何地址都能停。

总结

从芯片复位到进入完整的 C 环境,U-Boot 启动流程就是按顺序做好这几件事:

  1. 7. _start → reset:入口无条件跳转
  2. 8. PIE 修正(启用时):修正 .rela.dyn 中链接地址与运行地址的偏移
  3. 9. 检测异常等级、配异常向量表:不做主动 EL 切换,根据当前 EL 配 VBAR 和控制寄存器
  4. 10. lowlevel 初始化:GIC、多核从 CPU 的 EL 切换
  5. 11. 进 _main:栈配置、GD 分配、board_init_f 第一阶段 C 初始化
  6. 12. 重定位:拷贝到 DDR 高地址
  7. 13. BSS 清空VBAR 重配置
  8. 14. board_init_r 完整 U-Boot 环境
  9. 15. main_loop 命令行

用 GDB 过一遍,你会发现 U-Boot 的汇编启动代码组织得特别清晰——每一步对应一个明确的工程需求,没有多余的动作。