一颗 RISC-V 芯片从上电(Power-On Reset)到执行 main() 的完整过程。
这是芯片底层软件工程师的必备知识,也是 RISC-V 芯片公司(如时擎)面试高频考点。
参考《嵌入式自我修养》的"从电源到 main"框架,结合 RISC-V 特权架构与真实启动代码。


一、整体流程图

上电 / 复位
Power-On Reset

硬件复位
PC = 复位向量地址

进入 Machine Mode
最高特权级

执行启动汇编 _start
(crt0.S)

关中断 + 关看门狗

初始化栈指针 sp
(每个 hart)

初始化全局指针 gp
(RISC-V 特有)

重定位 .data 段
(Flash → RAM)

清零 .bss 段

初始化时钟 / PLL
(可提前到 C)

设置异常向量 mtvec

切换特权级(可选)
M → S/U

跳转 main()

C 运行环境建立
main 之后


二、第一阶段:硬件复位(上电 → 第一条指令)

2.1 上电发生了什么

步骤内容
1电源稳定,复位信号(reset)拉低再拉高
2CPU 内核被复位,所有寄存器清零
3PC(程序计数器)被加载为复位向量地址
4进入 Machine Mode(RISC-V 最高特权级)

2.2 RISC-V 复位向量在哪

RISC-V 规范没有规定固定的复位地址,由具体实现决定。常见情况:

芯片类型复位向量地址说明
裸机 MCU0x0000_0000 或 Flash 基地址上电直接跑 Flash/ROM 代码
Linux 级 SoC0x8000_0000 或 0x0001_0000SiFive HiFive 等
多核启动各 hart 从同一复位向量启动,用 mhartid 区分判断主核/从核

关键点:复位后第一条指令从复位向量开始取指,这是硬件决定的,不是软件配置的。

2.3 复位后的寄存器状态

寄存器复位后状态软件要做什么
pc= 复位向量无
通用寄存器 x0-x31未定义(x0 恒为 0)软件初始化
sp(x2)未定义必须软件设置栈
gp(x3)未定义必须软件设置(RISC-V 特有)
mstatus大部分清零,MIE=0(关中断)按需配置
mtvec实现定义软件设置异常向量
mhartid= 当前 hart ID多核判断用

RISC-V 与 ARM 的关键区别:ARM 上电后有 CPSR/SPSR 寄存器定义模式;RISC-V 没有 CPSR,用 mstatus CSR 表达状态。ARM 有固定复位向量表(0x0000 或 0xFFFF0000),RISC-V 复位地址由实现定义。


三、第二阶段:启动汇编 _start(crt0.S)

_start 是链接脚本指定的入口,是上电后执行的第一段软件。它做的事是"把硬件环境准备好,交给 C 语言"。

3.1 关中断 + 关看门狗

# 复位后 mstatus.MIE 通常已是 0,但显式确保
csrc mstatus, MIE        # 清中断使能位

# 关看门狗(防止启动过程中被复位)
# 具体操作依赖 SoC 的看门狗外设寄存器

为什么先关看门狗:启动过程可能耗时(拷贝 .data、清零 .bss),若看门狗超时会导致反复复位。这也是《嵌入式自我修养》强调的经典第一课。

3.2 初始化栈指针 sp(每个 hart 都要)

# 为当前 hart 设置栈
la sp, _stack_top        # 加载栈顶地址

多核场景下,每个 hart 需要独立的栈,通常用 mhartid 区分:

csrr t0, mhartid         # 读 hart ID
slli t0, t0, STACK_SHIFT # 乘以栈大小
la sp, _stack_top
sub sp, sp, t0           # 每个 hart 分配独立栈空间

关键点:没有栈,C 函数就无法调用(call 指令要把返回地址压栈)。所以设置 sp 是进入 C 之前的第一要务。

3.3 初始化全局指针 gp(RISC-V 特有,最易被问)

这是 RISC-V 区别于 ARM/x86 的独特环节:

.option push
.option norelax
la gp, __global_pointer$  # 链接脚本定义的符号
.option pop

为什么需要 gp:

  • RISC-V 的 lw/sw 指令用 gp 相对寻址访问全局变量(通过 linker relaxation 优化)
  • gp 约定指向 .sdata 段 + 0x800(小数据段中间偏移,±2KB 范围)
  • 若 gp 未初始化,所有通过 gp 相对寻址的全局变量访问都会跳错地址
概念说明
.sdata / .sbss小数据段(small data),放入 gp 可达 ±2KB 的小变量
__global_pointer$链接脚本定义的符号,= .sdata + 0x800
linker relaxation编译期把绝对寻址优化为 gp 相对寻址,省一条指令

面试答法:RISC-V 启动里必须初始化 gp 寄存器,它指向 .sdata+0x800,用于全局变量的小数据段相对寻址;不初始化会导致全局变量访问错误。这是 RISC-V 特有的,ARM 没有。

3.4 重定位 .data 段(Flash → RAM)

为什么需要重定位:.data(已初始化全局变量)的加载地址 LMA 在 Flash(断电不丢),但运行地址 VMA 在 RAM(可写)。上电后必须把初值从 Flash 拷贝到 RAM。

# 拷贝 .data 段
la t0, _data_lma        # Flash 中的源地址(加载地址)
la t1, _data_vma        # RAM 中的目标地址(运行地址)
la t2, _data_end
copy_loop:
    lw t3, 0(t0)
    sw t3, 0(t1)
    addi t0, t0, 4
    addi t1, t1, 4
    bltu t1, t2, copy_loop

这些符号(_data_lma/_data_vma/_data_end)由链接脚本定义。

3.5 清零 .bss 段

.bss(未初始化全局变量)在运行时必须是 0,但 Flash 里不存它的内容,所以要在 RAM 里清零:

# 清零 .bss 段
la t0, _bss_start
la t1, _bss_end
zero_loop:
    sw zero, 0(t0)       # zero 是 x0,恒为 0
    addi t0, t0, 4
    bltu t0, t1, zero_loop

面试要点:.data 要"拷贝"(因为有初值),.bss 要"清零"(因为没有初值,运行前必须是 0)。两者都发生在进入 main 之前,否则全局变量的初始值不对。

3.6 设置异常向量 mtvec

la t0, trap_entry        # 异常处理入口
csrw mtvec, t0           # 写入 mtvec CSR

mtvec 的低 2 位定义模式:

  • 00:直接模式,所有异常跳转到同一地址
  • 01:向量模式,按异常类型跳转到不同偏移

四、第三阶段:跳转 main 与特权级切换

4.1 直接跳转(裸机 M 模式)

最简单的裸机场景,全程在 Machine Mode,直接调用:

call main                 # 或 jal ra, main

4.2 特权级切换(需要 U/S 模式时)

若需要从 M 模式切换到 U/S 模式(如跑 OS),用 mret:

# 设置返回后要进入的特权级
csrw mepc, main          # 返回地址 = main
# 设置 mstatus.MPP = 00 (U) 或 01 (S)
# 清除 mstatus.MPIE
mret                      # 触发特权级切换 + 跳转 mepc

要点:mret 会根据 mstatus.MPP 切换到目标特权级,并跳转到 mepc。这是从固件进入 OS 内核的标准方式。


五、第四阶段:main 之后的 C 运行环境

进入 main() 不代表万事大吉,C 运行环境还要靠一些"隐式"机制:

5.1 main 之前的隐藏工作

很多工具链(如 GCC)会插入 __libc_init_array 之类的调用,在 main 之前执行:

机制作用
构造函数(constructor)__attribute__((constructor)) 函数,在 main 前执行
C++ 静态对象构造全局对象的构造函数
.init_array 段构造函数数组,逐个调用

5.2 main 的返回值与退出

  • 裸机:main 通常 while(1) 死循环
  • 有 OS:main 返回后调用 exit(),回到启动代码

六、链接脚本(Linker Script)——启动流程的"地图"

启动代码里所有符号(_stack_top、__global_pointer$、_data_lma 等)都来自链接脚本。它是理解启动流程的关键:

SECTIONS {
    . = 0x00000000;           /* 复位向量 / Flash 基地址 */

    .text : {                  /* 代码段 */
        *(.vectors)            /* 向量表放最前面 */
        *(.text)
    }

    .rodata : { *(.rodata) }   /* 只读数据 */

    .data : AT(LOADADDR(.data)) {  /* .data 加载在 Flash,运行在 RAM */
        __data_vma = .;
        *(.data)
        __data_end = .;
    } > RAM

    .sdata : {                 /* 小数据段,gp 寻址 */
        __global_pointer$ = . + 0x800;
        *(.sdata)
    } > RAM

    .bss : {
        __bss_start = .;
        *(.bss)
        __bss_end = .;
    } > RAM
}

段与 LMA/VMA 对照表

段内容LMA(加载)VMA(运行)启动处理
.text代码FlashFlash(XIP)或 RAM无需处理(或重定位)
.rodata只读数据FlashFlash无需处理
.data已初始化全局变量FlashRAM拷贝
.sdata小数据段FlashRAM拷贝 + gp 指向
.bss未初始化全局变量无RAM清零
栈局部变量无RAM设置 sp

七、面试速记(时擎必考)

7.1 一句话回答

“RISC-V 上电后,PC 跳到复位向量,进入 Machine Mode,执行启动汇编 _start:先关中断和看门狗,然后设置栈指针 sp 和全局指针 gp(RISC-V 特有),接着把 .data 段从 Flash 拷贝到 RAM、清零 .bss,设置异常向量 mtvec,最后跳转到 main。如果要从 M 模式切到 S/U 模式,用 mret 完成切换。”

7.2 高频追问与答案

追问答案
为什么 .data 要拷贝?.data 是已初始化全局变量,初值存在 Flash(LMA),运行时要写到 RAM(VMA),所以启动时拷贝
为什么 .bss 要清零?.bss 是未初始化全局变量,Flash 不存内容,但运行前必须是 0,所以启动时清零
gp 寄存器干什么的?RISC-V 特有,指向 .sdata+0x800,用于全局变量 gp 相对寻址(linker relaxation),不初始化会导致全局变量访问错误
sp 为什么必须软件设置?复位后 sp 未定义,没有栈就无法调用 C 函数(call 要压返回地址)
mtvec 是什么?异常向量基址 CSR,低 2 位定义直接/向量模式
怎么从 M 切到 U/S?设 mepc=目标地址、mstatus.MPP=目标特权级,执行 mret
多核怎么区分主从?读 mhartid CSR,主核(通常 0)做全局初始化,从核等信号
RISC-V 复位地址是多少?规范不固定,实现定义(MCU 常 0x0,Linux SoC 常 0x8000_0000)

7.3 与真实启动代码的印证

真实芯片启动代码(如 Telink 的 cstartup_flash.S)结构一致:

  1. .section .vectors 向量表,.org 0x0 放第一条指令 tj __reset
  2. __start 是链接脚本指定入口
  3. 按 __LOAD_RAM/__LOAD_FLASH 宏决定是否重定位到 RAM
  4. 设置栈、关看门狗、初始化时钟,最后跳 C 代码

八、ARM 启动流程对比(Cortex-M 与 Cortex-A)

把 ARM 的启动流程对照 RISC-V 梳理,便于面试时"对比着讲"。
分两条线:Cortex-M(嵌入式 MCU,对应 Sharpa/时擎的 RTOS 场景)与 Cortex-A(应用处理器,对应后摩/知存的 Linux SoC 场景)。

8.1 Cortex-M 启动流程(嵌入式 MCU)

Cortex-M 上电流程:

步骤内容
1复位后,硬件从向量表地址(通常 0x0000_0000,可由 VTOR 重映射)取前两项
2第 0 项(0x0)= 主栈指针 MSP 初始值,硬件自动加载到 SP
3第 1 项(0x4)= Reset_Handler 复位向量,硬件自动加载到 PC
4进入 Reset_Handler(Thread 模式 + 特权级)运行
5Reset_Handler:关看门狗 → 拷贝 .data → 清零 .bss → SystemInit(时钟)→ __main → main

核心特点(与 RISC-V 最大的区别):

特点说明
SP 硬件自动加载向量表首项是 MSP 初值,硬件自动设 SP,无需汇编设栈(RISC-V 必须软件设 sp)
无 gp 寄存器全局变量用绝对地址或 PC 相对寻址(位置无关),无 RISC-V 的 gp 概念
NVIC 硬件向量表中断由 NVIC 硬件管理,向量表基址 VTOR 可重定位
模式简单Thread/Handler 模式 + 特权/非特权(CONTROL 寄存器控制)
异常返回即切换用 BX LR(特殊 EXC_RETURN 值)触发异常返回与模式切换

8.2 Cortex-A 启动流程(应用处理器 / Linux SoC)

Cortex-A 上电流程:

步骤内容
1复位向量 0x00000000 或 0xFFFF0000(由 SCTLR.V 或实现决定)
2进入最高异常等级 EL3(或 EL1),CPSR 决定复位后的初始模式(SVC)
3用 MPIDR 区分多核(主核做全局初始化,从核等信号)
4启动汇编:关中断/关 MMU/关 Cache → 设置各模式栈指针(SVC/IRQ/FIQ/ABT/UND)→ 设异常向量表 VBAR → 初始化 MMU/页表 → 跳 C
5特权级逐级下降:EL3(固件)→ EL2(hypervisor)→ EL1(OS)→ EL0(应用),用 eret 切换

核心特点(与 RISC-V 的对应关系):

Cortex-A 概念RISC-V 对应说明
CPSR/SPSRmstatus状态寄存器(模式位、中断使能)
VBARmtvec异常向量基址寄存器
MPIDRmhartid多核 ID 区分
eretmret特权级切换 + 返回指令
EL0-EL3M/S/U特权级模型
每模式一个栈每 hart 一个栈栈组织差异

8.3 逐项对比总表

维度ARM Cortex-MARM Cortex-ARISC-V
复位向量向量表:首项 SP + 次项 PC0x0 或 0xFFFF0000实现定义(MCU 常 0x0,SoC 常 0x8000_0000)
SP 初始化硬件自动加载 MSP软件设置各模式栈软件设置 sp
全局指针无无gp(RISC-V 特有)
状态寄存器xPSR(CONTROL/PRIMASK)CPSR/SPSRmstatus CSR
异常向量NVIC 向量表(VTOR)VBARmtvec CSR
特权级Thread/Handler + 特权/非特权EL0-EL3M/S/U
特权切换异常返回(EXC_RETURN)eretmret
多核区分—(单核为主)MPIDRmhartid
中断控制器NVICGICPLIC / CLINT
.data/.bss 处理拷贝/清零(同)拷贝/清零(同)拷贝/清零(同)

共性:三种架构都要"拷贝 .data、清零 .bss、初始化栈",这是跨架构通用的启动步骤。
差异核心:Cortex-M 硬件代劳最多(自动设 SP),Cortex-A 模式栈最复杂(多模式多栈),RISC-V 最精简(软件设 sp + 特有的 gp)。

8.4 一句话对比

“ARM Cortex-M 上电后硬件自动加载 SP 和 PC(向量表前两项),RISC-V 要软件设 sp;ARM 没有 gp 寄存器,RISC-V 有 gp(指向 .sdata+0x800 做全局变量寻址);ARM 用 CPSR/VBAR/eret,RISC-V 用 mstatus/mtvec/mret;Cortex-A 多核用 MPIDR 区分,RISC-V 用 mhartid。”


Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐