全景介绍
第一篇我们搭好了 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 体系结构定义了四个异常等级:
QEMU virt 调试环境:取决于是否有 ATF。无 ATF 时 U-Boot 从 EL3 开始(currentel = 0xc);有 ATF 时通常被切到 EL2(currentel = 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. adr x0, _start取运行地址,ldr x1, _TEXT_BASE取链接地址2. subs x9, x0, x1算出偏移量3. 遍历 .rela.dyn段,把所有R_AARCH64_RELATIVE类型的地址加上偏移量
这一步修正的是编译时生成的绝对地址引用(比如函数指针、全局变量地址)。修正后,代码中的绝对地址都变成了运行时正确的值。
QEMU 调试时链接地址=运行地址(都是 0x0),所以偏移量为 0,这段直接跳过。
第二阶段:relocate_code(_main 中,board_init_f 之后)
不管有没有 PIE 都会执行(只要需要搬迁)。做的事:
4. memcpy(relocaddr, _start, image_size)— 把整个 U-Boot 拷贝到 DDR 高地址5. 如果没启用 PIE:修正绝对符号引用( rel_dyn_start到rel_dyn_end的重定位项)6. 如果已启用 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 启动流程就是按顺序做好这几件事:
7. _start → reset:入口无条件跳转 8. PIE 修正(启用时):修正 .rela.dyn中链接地址与运行地址的偏移9. 检测异常等级、配异常向量表:不做主动 EL 切换,根据当前 EL 配 VBAR 和控制寄存器 10. lowlevel 初始化:GIC、多核从 CPU 的 EL 切换 11. 进 _main:栈配置、GD 分配、board_init_f 第一阶段 C 初始化 12. 重定位:拷贝到 DDR 高地址 13. BSS 清空、VBAR 重配置 14. board_init_r 完整 U-Boot 环境 15. main_loop 命令行
用 GDB 过一遍,你会发现 U-Boot 的汇编启动代码组织得特别清晰——每一步对应一个明确的工程需求,没有多余的动作。
夜雨聆风