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段没清零。你连函数调用都做不了。
入口点代码的核心任务
入口点代码要做的事情,其实就几件:
- 设置异常向量表(如果还没设置)
- 切换到合适的处理器模式
- 初始化堆栈指针(每种模式都要设)
- 清零BSS段
- 拷贝数据段(如果需要)
- 跳转到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第一阶段初始化流程
这张图把第一阶段的核心流程串起来了。从CPU复位开始,到最终跳进C代码,每一步都不能省。我见过有人为了省事,把BSS清零跳过了。结果全局变量初始值全是乱的,程序行为完全不可预测。嗯,这种坑踩过一次就记住了。
特别提醒:堆栈指针的设置顺序很重要。先设SVC模式的堆栈,因为初始化代码本身就在SVC模式下运行。如果先切到IRQ模式设堆栈,万一此时来了中断,SVC模式的堆栈还没设,中断处理函数一调用就崩。我曾经在调试一个中断响应异常的问题时,发现就是堆栈设置顺序反了导致的。
好了,第一阶段的核心内容就这些。说白了就是三件事:放对向量表、写好汇编入口、配好堆栈和关键寄存器。这三件事做好了,C代码才能安心跑起来。