BootROM启动初始化与镜像加载调试 - 第3节

BootROM第一阶段:CPU复位与异常向量表

芯片上电那一刻,CPU到底在干什么?

很多人以为复位后CPU直接跳进main函数。其实不是。CPU复位后第一件事,是去一个固定地址取第一条指令。这个地址,就是复位向量。我刚开始做嵌入式时,以为只要把代码烧进去就行,结果板子死活跑不起来。后来才发现,是复位向量表没放对位置。

复位后的CPU状态

CPU复位后,所有寄存器都回到默认值。具体来说:

  • 程序计数器(PC)被强制设为复位向量地址
  • 大部分控制寄存器清零
  • Cache和MMU默认关闭
  • 中断被全局屏蔽

说白了,CPU此时处于最原始的状态。你想想看,这时候连堆栈都没有,C代码根本跑不了。所以必须用汇编来初始化。

关键点:复位后CPU处于实模式或等效的平坦地址模式。地址空间是直接映射的,没有虚拟地址转换。这意味着你写的地址就是物理地址。

异常向量表的结构

异常向量表,就是一张地址表。每个表项对应一种异常类型。CPU发生异常时,硬件自动跳转到对应的表项地址执行。

向量偏移 异常类型 优先级 触发条件
0x00 复位 最高 上电或硬件复位
0x04 未定义指令 低 CPU遇到无法识别的指令码
0x08 软件中断 中 执行SWI指令
0x0C 预取中止 中 取指令时发生异常
0x10 数据中止 中 数据访问时发生异常
0x14 保留 - 未使用
0x18 IRQ 低 普通中断请求
0x1C FIQ 高 快速中断请求

每个表项通常放一条跳转指令。比如:

; 异常向量表示例(ARM架构)
.section .vectors, "ax"
.globl _start

_start:
    b   reset_handler      ; 0x00: 复位
    b   undef_handler      ; 0x04: 未定义指令
    b   swi_handler        ; 0x08: 软件中断
    b   prefetch_handler   ; 0x0C: 预取中止
    b   data_handler       ; 0x10: 数据中止
    b   .                  ; 0x14: 保留(死循环)
    b   irq_handler        ; 0x18: IRQ
    b   fiq_handler        ; 0x1C: FIQ

个人经验:我习惯在保留向量处放一个死循环或跳转到错误处理函数。这样万一发生意外跳转,能快速定位问题。曾经有个项目,因为中断向量表没对齐,导致IRQ跳到了保留向量,板子直接卡死。查了两天才找到原因。

BootROM入口点与初始化汇编代码

复位向量指向的代码,就是BootROM的入口点。这段代码必须用汇编写。为什么?因为C运行环境还没建立。堆栈没有,全局变量没有,BSS段没清零。你连函数调用都做不了。

入口点代码的核心任务

入口点代码要做的事情,其实就几件:

  1. 设置异常向量表(如果还没设置)
  2. 切换到合适的处理器模式
  3. 初始化堆栈指针(每种模式都要设)
  4. 清零BSS段
  5. 拷贝数据段(如果需要)
  6. 跳转到C入口函数

我见过很多新手写的启动代码,只设了一个堆栈指针。结果中断一来,直接崩。因为IRQ模式和FIQ模式有自己的堆栈,你不设,它们就用默认值,大概率是0。往地址0写数据,后果你懂的。

; BootROM入口点示例(简化版)
.section .text.startup
.globl _start

_start:
    ; 1. 设置异常向量表基址
    ldr r0, =_vectors
    mcr p15, 0, r0, c12, c0, 0   ; 写入VBAR寄存器

    ; 2. 切换到各个模式并设置堆栈
    ; 先设SVC模式堆栈
    mrs r0, cpsr
    bic r0, r0, #0x1F
    orr r0, r0, #0x13            ; SVC模式
    msr cpsr, r0
    ldr sp, =__svc_stack_top

    ; 再设IRQ模式堆栈
    mrs r0, cpsr
    bic r0, r0, #0x1F
    orr r0, r0, #0x12            ; IRQ模式
    msr cpsr, r0
    ldr sp, =__irq_stack_top

    ; 再设FIQ模式堆栈
    mrs r0, cpsr
    bic r0, r0, #0x1F
    orr r0, r0, #0x11            ; FIQ模式
    msr cpsr, r0
    ldr sp, =__fiq_stack_top

    ; 3. 切回SVC模式
    mrs r0, cpsr
    bic r0, r0, #0x1F
    orr r0, r0, #0x13
    msr cpsr, r0

    ; 4. 清零BSS段
    ldr r0, =__bss_start
    ldr r1, =__bss_end
    mov r2, #0
bss_loop:
    cmp r0, r1
    strlo r2, [r0], #4
    blo bss_loop

    ; 5. 跳转到C入口
    bl boot_main

注意:不同架构的异常向量表基址设置方式不同。ARM通过CP15协处理器设置VBAR,Cortex-M系列则固定放在0x00000000或0x00000004。RISC-V的mtvec寄存器又是另一套。写代码前一定要查架构手册。

堆栈指针与关键寄存器配置

堆栈指针,说白了就是CPU用来记录栈顶位置的寄存器。SP设错了,函数调用、局部变量、中断保存现场全都会出问题。我曾经在一个项目里,因为堆栈大小算错了,导致递归调用时栈溢出,数据被悄悄覆盖。查了三天,最后用JTAG单步跟踪才发现。

堆栈指针的设置原则

  • 每种特权模式都要设独立的堆栈:SVC、IRQ、FIQ、ABT、UND、SYS,一个都不能少
  • 堆栈大小要留余量:至少比预估大30%。中断嵌套、异常处理都会额外消耗栈空间
  • 堆栈地址要对齐:ARM要求8字节对齐,x86要求4字节对齐。不对齐会导致性能下降甚至异常
  • 堆栈方向要明确:ARM默认是满递减栈(SP指向最后一个压入的数据),x86也是满递减。但有些架构是空递增,别搞混

关键寄存器的配置清单

除了堆栈指针,还有几个寄存器必须在C代码运行前配好:

寄存器 作用 配置时机 常见错误
VBAR/MTVEC 异常向量表基址 复位后立即配置 忘记设置,中断无法响应
SCTLR 系统控制寄存器 初始化早期 Cache/MMU开关状态不对
CPSR/SPSR 程序状态寄存器 模式切换时 中断使能位设置错误
CPACR 协处理器访问控制 使用FPU/NEON前 未使能导致浮点指令异常
GIC配置 中断控制器 中断使能前 中断优先级分组未初始化

我的习惯:在汇编初始化代码里,我会加一段校验逻辑。比如配完堆栈后,往栈顶写一个魔数,然后在C入口函数里检查这个魔数是否被覆盖。如果被覆盖,说明堆栈设置有问题或者栈太小。这个方法帮我抓过好几个栈溢出bug。

SVG:BootROM第一阶段初始化流程

BootROM第一阶段初始化流程 BootROM第一阶段初始化流程 CPU复位 取复位向量 设置异常向量表基址 配置各模式堆栈指针 清零BSS段 跳转C入口函数 关键说明 • 所有步骤在汇编中完成 • 堆栈需为每种模式独立设置 • BSS清零后才能访问全局变量 常见陷阱 • 忘记设IRQ/FIQ堆栈 • 向量表未对齐到边界 • 堆栈大小估算不足

这张图把第一阶段的核心流程串起来了。从CPU复位开始,到最终跳进C代码,每一步都不能省。我见过有人为了省事,把BSS清零跳过了。结果全局变量初始值全是乱的,程序行为完全不可预测。嗯,这种坑踩过一次就记住了。

特别提醒:堆栈指针的设置顺序很重要。先设SVC模式的堆栈,因为初始化代码本身就在SVC模式下运行。如果先切到IRQ模式设堆栈,万一此时来了中断,SVC模式的堆栈还没设,中断处理函数一调用就崩。我曾经在调试一个中断响应异常的问题时,发现就是堆栈设置顺序反了导致的。

好了,第一阶段的核心内容就这些。说白了就是三件事:放对向量表、写好汇编入口、配好堆栈和关键寄存器。这三件事做好了,C代码才能安心跑起来。