29、不同架构的启动对比:ARM vs RISC-V vs X86
做嵌入式这些年,我接触过不少架构。ARM、RISC-V、X86,这三兄弟的启动流程,说白了就是「从复位到main函数」这条路怎么走。但每条路的走法,差别还挺大。
我记得刚入行那会儿,被一个X86的BIOS问题折腾了两天。后来切到ARM项目,发现人家压根没有BIOS这回事。嗯,这就是架构差异带来的「惊喜」。
启动流程的宏观对比
先看一张总览图,把三个架构的启动路径画出来。你对比着看,心里就有谱了。
从图上你能看出来,ARM和RISC-V的启动路径比较「瘦」,X86则多了一层BIOS/UEFI的「脂肪」。这跟它们的历史包袱有关。
ARM 启动:向量表驱动
ARM Cortex-M系列的启动,我个人觉得是最优雅的。它靠一张向量表搞定一切。
芯片上电后,硬件自动做两件事:
- 从地址 0x00000000 读取栈指针(SP)初始值
- 从地址 0x00000004 读取复位向量(Reset_Handler 地址)
然后直接跳过去执行。就这么简单。
关键点:ARM Cortex-M 的向量表前两个字必须是 SP 和 PC。这是硬件写死的,改不了。
我在项目中遇到过一个问题:有人把向量表放到了 0x08000000(Flash 起始地址),但芯片复位后默认从 0x00000000 取向量。结果怎么都跑不起来。后来发现是忘了配置 VTOR 寄存器。嗯,这个坑我踩过。
典型的启动代码长这样:
__Vectors DCD __initial_sp ; 栈顶地址
DCD Reset_Handler ; 复位向量
DCD NMI_Handler ; NMI
DCD HardFault_Handler ; 硬错误
; ... 其他中断向量
Reset_Handler PROC
EXPORT Reset_Handler
; 初始化 .bss 段
LDR r0, =__bss_start
LDR r1, =__bss_end
MOV r2, #0
bss_loop CMP r0, r1
ITT LT
STR r2, [r0], #4
BLT bss_loop
; 跳转 main
BL main
ENDP
RISC-V 启动:灵活但费心
RISC-V 的启动,说白了就是「给你自由,但你要自己负责」。它没有 ARM 那种硬件自动加载向量表的机制。
复位后,PC 被设置成一个固定地址(通常是 0x80000000 或 0x00000000,取决于具体实现)。然后从那里开始执行第一条指令。
这里有个细节:RISC-V 的异常向量表(trap vector)需要软件自己设置。通过 csrw mtvec, handler_addr 指令写入 mtvec 寄存器。
我的建议:写 RISC-V 启动代码时,第一件事就是设置栈指针。因为很多 RISC-V 核复位后 sp 是未定义的。我曾经见过有人没设栈就调用函数,结果压栈压到了非法地址,直接跑飞。
一个典型的 RISC-V 启动代码片段:
.section .text.init
.globl _start
_start:
// 设置栈指针
la sp, _stack_top
// 设置 trap 向量
la t0, trap_handler
csrw mtvec, t0
// 清零 bss
la t0, __bss_start
la t1, __bss_end
bss_clear:
sw zero, 0(t0)
addi t0, t0, 4
blt t0, t1, bss_clear
// 跳转 main
jal main
你想想看,RISC-V 的启动代码其实比 ARM 多了一步:手动设置异常向量。ARM 的向量表是硬件自动识别的,RISC-V 得靠软件写寄存器。这就是「灵活」的代价。
X86 启动:历史包袱最重
X86 的启动流程,嗯,怎么说呢,它背负了太多历史兼容性。从 8086 到现在的酷睿,启动方式一直在变,但核心逻辑没变。
复位后,CPU 处于实模式。CS=0xF000, IP=0xFFF0,所以第一条指令地址是 0xFFFF0。这个地址映射到 BIOS ROM 的最后 16 字节。
BIOS 会做以下几件事:
- POST(上电自检):检查内存、CPU、外设
- 初始化中断向量表(IVT)
- 扫描引导设备,加载 MBR 到 0x7C00
- 跳转到 0x7C00 执行引导代码
注意:X86 的启动代码必须处理实模式到保护模式的切换。这涉及到 GDT、LDT、页表等一系列复杂操作。我当年第一次写保护模式切换代码时,被段描述符的权限位折腾得够呛。
一个简化的 X86 启动流程(从 BIOS 到 main):
; 引导扇区代码 (加载到 0x7C00)
[org 0x7C00]
start:
; 设置段寄存器
xor ax, ax
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0x7C00
; 加载内核到内存
mov ah, 0x02 ; 读扇区
mov al, 0x10 ; 16个扇区
mov ch, 0 ; 柱面0
mov cl, 2 ; 起始扇区2
mov dh, 0 ; 磁头0
mov bx, 0x1000 ; 加载地址
int 0x13
; 跳转到保护模式入口
jmp 0x1000
你看,X86 的启动代码里还有 int 0x13 这种 BIOS 中断调用。这在 ARM 和 RISC-V 的世界里根本不存在。说白了,X86 的启动依赖 BIOS 这个「中间人」,而 ARM 和 RISC-V 直接跟硬件打交道。
三个架构的核心差异
我把关键差异整理成了一张表,方便你对比:
| 对比项 | ARM (Cortex-M) | RISC-V (RV32) | X86 (IA-32) |
|---|---|---|---|
| 复位后 PC 来源 | 向量表偏移 0x04 | 硬件固定地址 | CS:IP = 0xF000:FFF0 |
| 栈指针初始化 | 硬件自动加载 | 软件手动设置 | 软件手动设置 |
| 异常向量设置 | 硬件自动识别 | csrw mtvec | IDT 设置 |
| 是否需要 BIOS | 否 | 否 | 是 |
| 启动模式 | 直接运行 | 直接运行 | 实模式 → 保护模式 |
| 启动代码复杂度 | 低 | 中 | 高 |
避坑指南
做启动代码开发,有些坑是共通的。我根据自己的经验,给你列几条:
- 栈指针必须最先设置:不管什么架构,复位后第一件事就是确认栈指针有效。我曾经在 RISC-V 上吃过亏,没设栈就调了函数,结果压栈到 0 地址,直接 HardFault。
- BSS 清零不能忘:全局变量默认值必须是 0,这是 C 标准要求的。如果启动代码忘了清 BSS,未初始化的全局变量就是随机值。这个 bug 极难排查。
- 向量表对齐:ARM 要求向量表 512 字节对齐,RISC-V 要求 mtvec 按 4 字节对齐。不对齐的话,硬件可能取错向量地址。
- X86 的 A20 地址线:如果你在 X86 上做裸机开发,别忘了开启 A20 地址线。否则访问 1MB 以上的内存会回绕。这个坑我当年调了整整一天。
总结一下:ARM 的启动最省心,硬件帮你做了大部分工作。RISC-V 给你自由,但你要自己搭框架。X86 历史包袱最重,启动流程最复杂。选哪个架构,取决于你的项目需求和个人偏好。我个人做小项目喜欢用 ARM,省事;做研究或定制化项目,RISC-V 更灵活。
公众号:蓝海资料掘金营,微信deep3321