RISC-V 64 Bringup 技术调研报告(v3.0):从加电到 init —— OpenSBI、U-Boot 与 Linux/Android/RTOS 启动链
| 字段 | 内容 |
|---|---|
| 文档类型 | 技术调研报告 |
| 版本 | v3.0 |
| 状态 | 公开资料整理;源码版本锁定到 commit;未做真板启动时间与稳定性实测 |
| 密级 | 公开资料 |
| 作者 | Qoder Research |
| 生成日期 | 2026-10-10 |
| 研究截点 | 2026-10-10 |
| 目标读者 | 芯片/固件工程师、OS 移植工程师、板卡 bringup 与验证团队、RISC-V 平台选型负责人 |
| 调研问题 | ① 从上电到 init 的每一级交接由哪些寄存器、结构体与文件承载,写错哪一位会在什么位置爆炸;② OpenSBI 冷/热启动路径与域、PMP 配置的源码级事实;③ U-Boot SPL 与 proper 两次接力的调用链与镜像装载判定;④ Linux 从 head.S 到 PID 1 的页表、SMP 与 init 候选链;⑤ Android 两阶段 init 与 RTOS 启动路径如何与同一内核入口分岔 |
| 覆盖范围 | RISC-V 64 标准启动链(BootROM/ZSBL → SPL → OpenSBI → U-Boot proper → 内核 → init);OpenSBI v1.9 开发线(commit 6ad246a1);U-Boot(版本与 commit 见来源台账);Linux v7.3.0-rc6(commit 3857c2fe);Android init 两阶段与 AVB;Zephyr/FreeRTOS/NuttX 启动路径;QEMU virt 与国内平台(VisionFive 2、Lichee Pi 4A、SpacemiT K1、Pioneer) |
| 排除范围 | 芯片内部 DDR 训练算法与未公开初始化细节;商业工具授权与报价;真板启动时间、功耗、稳定性实测;安全认证结论;采购建议 |
摘要
- RISC-V 64 启动链的每一级交接都由一组可以 grep 的寄存器与结构体约定承载:M→S 交接是
a0=hartid、a1=FDT、satp=0与mstatus.MPP=S;SPL→OpenSBI 是struct fw_dynamic_info的 magic/version/next_addr/next_mode/options/boot_hart 六字段;内核入口要求在Documentation/arch/riscv/boot.rst逐条写明。bringup 挂死绝大多数是某级字段写错,而不是「玄学」。 [S02][S06][S07] - OpenSBI master(v1.9 开发线)已把 PMP 配置重构为 hart protection 框架:冷启动在
sbi_init()末尾经sbi_hart_protection_configure()落盘,root 域默认为「固件区 M+FW 权限 + 全内存 SU RWX」;域 dump 形如Domain0 Region00 … M: (F,RWX) S/U: (RWX),S 态越界首访即 trap 回 M 态并可在mepc定位。 [S02][S03] - Linux v7.3.0-rc6 的
head.S中relocate_enable_mmu已回到 head.S 本体(非 mm/init.c),satp 经 trampoline 两次切换后才启用内核页表;a1=FDT 的现行要求不含旧版报告所称的「8 字节对齐」要求,真实硬约束是 a0=hartid、$satp=0、镜像置于 PMD 边界(rv64 为 2 MiB)。 [S06][S07] - SMP 从核的两条路径在
cpu_set_ops()分流:探测到SBI_EXT_HSM才用cpu_ops_sbi,否则回退 spin-table;后者要求所有 hart 各就各位自旋等__cpu_spinwait_stack_pointer,是 QEMU virt 与真板行为差异的常见来源。 [S06][S15] - 排查顺序应先建立「串口最后一句 → 下一级第一个越界访问」的映射,再分别核对 PMP 区间、页表项与 init 候选链;JTAG 读
pmpcfg/pmpaddr、satp、mepc/mcause能在不重启的情况下把故障钉死到单条指令。 [S02][S06][S23]
背景与现状
RISC-V 64 的启动链由四级交接串成:BootROM/ZSBL 复位后按介质装载 → SPL(片内 SRAM)→ OpenSBI(M 态)→ U-Boot proper(S 态)→ 内核 Image(S 态)→ 用户态 init。与 x86/ARM 的主要差别不在顺序,而在于每一级交接的载体都是可逐字核对的寄存器与内存结构:hartid/FDT 走 a0/a1,SPL 与 OpenSBI 之间以 struct fw_dynamic_info 传递入口与选项,内核入口条件在 boot.rst 中逐条写死。bringup 的工程本质因此是「把每一级交接的字段核对到字节」。本文版本锚点全部锁定到 commit:OpenSBI master 6ad246a1(v1.9 开发线)、Linux master 3857c2fe(v7.3.0-rc6)、U-Boot master ece349ad(2026.07 开发线),其余见参考来源台账。图 1 给出逐级交接的全景。
图 1:从 BootROM 到 PID 1 的逐级交接 —— 每一级标注了承载交接的寄存器、结构体与源码文件,右上两张便签是两组最常被写错的全局约定。来源:按 OpenSBI master、Linux master 与 U-Boot 文档(版本见台账)整理 [S02][S05][S06];数据口径:以 commit 锁定的源码为准,QEMU virt 上无真实 BootROM 阶段。
见图 1,读图先看两条竖向链条:左侧 7 个节点是完整的物理启动顺序,右侧两张便签(boot.rst 入口要求与 fw_dynamic_info 结构)是两条最常被写错的全局约定。任何一个节点向右「串线」的部分——如 SPL 直接喂给 OpenSBI 的 a2 块——出问题时串口通常会停在上一级的最后一行横幅。
标准与上游实现现状
在规范层,M 态与 S 态的职责边界已由 RISC-V SBI Specification v3.0 与各 Profile 固化:S 态内核永不自管时钟与 IPI,一切经 ecall 走 SBI 扩展(TIME/IPI/RFENCE/HSM/SRST/SSE 等);固件实现侧由 OpenSBI(master 线为 v1.9-dev)参考实现兜底,其 fw_dynamic_info 结构把「下一级入口地址/模式/FDT/选项/偏好 boot hart」序列化成可校验的交接块 [S01][S02]。平台层,Boot and Runtime Services(BRS)v1.0 于 2025-08-29 冻结,把「OS 期望固件提供什么」写成标准,配合 OS-A 平台规范与 RISC-V UEFI 集成指引,收敛出「BootROM → SPL/固件 → U-Boot/EDK2 → 内核」的主流形态 [S16]。
在实现层,上游三件套的行为可以逐文件核对:OpenSBI 的 firmware/fw_base.S 在 _start 后先处理 128 字节固件头(magic 0x4942534f)并选 boot hart,再由 lib/sbi/sbi_init.c 的 init_coldboot() 按固定顺序初始化十余个子系统,最后经 sbi_hsm_hart_start_finish() 调用 sbi_hart_switch_mode() 以 mret 交接;U-Boot 在 arch/riscv/cpu/start.S 里区分 SPL 与 proper 两段入口,proper 段经 relocate_code 自搬移后进入 board_init_r 启动 bootstd 扫描;Linux 内核入口在 arch/riscv/kernel/head.S,_start_kernel 清 BSS、setup_vm 建恒等映射后 relocate_enable_mmu 写 CSR_SATP(Sv39/Sv48/Sv57 由 satp_mode 探测),随后 tail start_kernel 进入 init/main.c [S02][S04][S06]。
在接口层,所有交接的要求都在上游文档中可查:内核入口要求 $a0=hartid、$a1=FDT 物理地址、$satp=0(MMU 关闭),镜像须置于 PMD 边界(rv64 为 2 MiB);/memory 节点提供 memblock 的全部原料;镜像头以 magic "RISCV" 与 image_size 自描述,供引导器装载前校验 [S06][S07][S08]。值得强调的是,现行 boot.rst 并未规定 a1 的 8 字节对齐——这一在旧版报告与二手文章间流传的说法没有一手依据,真正需要对齐的是内核对 DTB 的 fixmap 映射(FIX_FDT % (PMD_SIZE / PAGE_SIZE) 的编译期断言)。这类「流传甚广但无出处」的细节正是本文以 commit 级源码逐条校正的对象 [S06]。
启动软件的成熟度也可以从版本节奏读出:OpenSBI 从 v1.7(2025-06)的 SBI v3.0 合规、v1.8(2025-12)的动态堆与国内平台(SpacemiT K1、ESWIN EIC7700)接纳,到 v1.9 引入 Zkr 栈保护与 Smrnmi NMI;Linux 侧 RISC-V 已长期处于「入口稳定、平台支持持续合入」状态(本轮 master 甚至移除了长期缺乏维护的 XIP 分支);U-Boot 的 bootstd 框架把「找设备 → 找方法 → 引导」从板级代码收敛为通用框架 [S02][S06][S04]。三者的共同趋势是:板级私有差异被压缩到设备树与少数平台钩子(如 spl_board_init())中,其余全部上游公共路径可复用——这正是「按接口排查」能够跨平台成立的前提 [S04][S05][S16]。整体层级与失败面见图 1。
各能力维度的全球基线、成熟度与边界前提汇总如下(版本锚点见参考来源)。
| 能力维度 | 全球基线(commit/版本锁定) | 成熟度 | 关键实现 | 边界/前提 |
|---|---|---|---|---|
| 固件交接接口 | fw_dynamic_info 六字段 + magic/version 校验(OpenSBI v1.9-dev) | 规模化 | include/sbi/fw_dynamic.h、firmware/fw_dynamic.S [S02] | 直接加载 u-boot.bin 的板子不经此交接 |
| 冷启动初始化次序 | init_coldboot() 固定十余步序列,失败可定位 | 规模化 | lib/sbi/sbi_init.c [S02] | 平台钩子(platform_early/final)内部行为随 SoC 而变 |
| PMP/域隔离 | hart protection 框架(pmp rating 100 / epmp rating 200) | 规模化 | sbi_hart_pmp.c、sbi_hart_protection.c [S02][S03] | SMEPMP 扩展缺席时退化为经典 PMP |
| SPL/FIT 装载 | SPL + bootstd 上游通用框架 | 规模化 | U-Boot docs/spl、bootstd [S04][S05] | DDR 训练 blob 不可审计的平台例外 |
| 内核入口要求 | boot.rst 逐条 + 镜像头 magic | 规模化 | Documentation/arch/riscv/boot.rst [S06][S07] | 未来规范版本可能追加要求 |
| SMP 从核路径 | HSM 规范化 + spin-table 兼容 | 可用 | cpu_ops.c 分流 [S06] | 完整语义要求固件实现 HSM |
| init 分岔 | initramfs/PID1 链 + Android 两阶段 + RTOS 调度器 | 可用 | init/main.c、AOSP init、各 RTOS [S06][S09][S12] | Android 官方 RISC-V 支持仍缺位 |
国内平台与生态现状
国内平台近两年的 bringup 主线是「固件上游化 + 发行版就绪」。VisionFive 2(JH7110)与 HiFive Unmatched 是完整上游化的参照平台:SPL → OpenSBI → U-Boot proper → 内核四段链条与镜像分区均有公开文档可复现;SpacemiT K1 自 OpenSBI v1.8 起获得上游支持,社区同时维护 UEFI 替代方案;Pioneer(SG2042)官方安装镜像走 EDK2 UEFI + GRUB2 路径,把服务器类平台的启动固化到标准固件栈;Lichee Pi 4A(TH1520)沿厂商 DDR blob + fastboot 写入 u-boot-with-spl.bin 的路径,是「SPL 前阶段不可审计」的典型样本 [S17][S18][S19][S20]。
发行版侧的门槛已明显降低:openEuler 24.03 LTS 同时为 Pioneer 与 Lichee Pi 4A 提供安装文档,使 bringup 完成后的系统验证可以直接走发行版安装流程而不必自建根文件系统;RTOS 侧,Zephyr 与 NuttX 在 RISC-V 上游支持(含 S 态运行路径与 SBI 依赖说明)为国内物联网与车规场景提供可选底座,RT-Thread 维护 libcpu/risc-v 移植层 [S12][S13][S14][S19]。
差距集中在三点:其一,部分国产 SoC 的 DDR 训练仍以 blob 交付,SPL 之前的失败只能依赖厂商工具;其二,同一生态内 EDK2 UEFI 与 U-Boot extlinux 两条固件路线并存,跨平台复用的排查方法有限,BRS v1.0 与 OS-A 规范的落地程度成为分水岭;其三,Android 在 RISC-V 的官方支持仍缺位(GKI 移除后仅存社区追踪),国内 Android on RISC-V 的 bringup 工程多依赖厂商闭源 patch。机会在于:上游对国内平台的持续接纳(K1 等)、RVA23 基线发行版的成熟,以及 HIL 自动化验证工具链的成形,使「把板卡纳入逐级验证矩阵」第一次具备完整条件 [S12][S16][S19]。
技术详解:从加电到 init
本章按启动顺序逐个机制吃透:每一节都给出接口/实现依据、步骤级调用链、失效边界与验证条件,全部锚点以 commit 锁定的上游源码为准(OpenSBI 6ad246a1、Linux 3857c2fe;U-Boot 与 Android/RTOS 的版本见台账),可逐条 grep 复核。
OpenSBI:fw_base.S 冷启动、域与 PMP 落盘
接口与实现依据。OpenSBI 对外只有两个接口面:对上,S 态仅能经 ecall 访问 SBI(EID 在 a7、FID 在 a6,返回值在 a0/a1,语义由 SBI v3.0 规范锁定);对下,上一级经 a0/a1/a2 交付「hartid / FDT / 选项块」。fw_dynamic 形态中 a2 指向 struct fw_dynamic_info(include/sbi/fw_dynamic.h),六字段依次为 magic、version、next_addr、next_mode、options、boot_hart(偏移 0–5,单位为一个长字),magic 固定 0x4942534f,version 上限为 2;firmware/fw_dynamic.S 中的 fw_boot_hart() 先校验 magic/version,不符即跳 _start_hang——这是「SPL 与 OpenSBI 版本对不上就黑屏」的第一现场。源码锚点:firmware/fw_base.S、firmware/fw_dynamic.S、lib/sbi/sbi_init.c、lib/sbi/sbi_hsm.c。
冷启动序列(行号按 master 6ad246a1)。_start(L58)→ 128 字节固件头(FW_HEADER_*)→ _start_real(L111)→ fw_boot_hart() 择主(L145):返回 -1 时全体 hart 以 amoswap.w 对 _boot_lottery 原子抽签,未中者进入 _wait_for_boot_hart(L401)轮询 _boot_status → boot hart 执行 .rela.dyn GOT 重定位(L169-189,以 FW_TEXT_START 为链接基址、_fw_start 为装载基址,凡「运行地址 ≠ 链接地址」的装载方式都靠它修正 GOT 项)→ _reset_regs/清 BSS/CLEAR_MDT(L194-214)→ 临时栈 → fw_save_info()(L223)→ fw_platform_init()(L237)→ _scratch_init(L273-348)→ 置 _boot_status(L393)→ _start_warm(L411)→ csrw CSR_MSCRATCH,tp 后 call sbi_init()(L477-479)。进入 C 世界的顺序并非任意:init_coldboot()(lib/sbi/sbi_init.c L224)先 sbi_scratch_init() 与 sbi_heap_init(),再 sbi_domain_init()(L241,此时按平台是否单固件区生成 1–2 个 M 态 region 与一个全内存 SU region),随后 sbi_hsm_init(scratch, true) 把本 hart 置 START_PENDING、其余置 STOPPED;hart/trap/timer/platform_early/pmu/dbtr 依次初始化后打印横幅,再经 irqchip/ipi/tlb/fwft/mpxy/domain_finalize/platform_final/sse/ecall_init 补齐运行时设施;最后 sbi_hart_protection_configure()(L436)把域 region 写入 PMP 条目,并以 sbi_hsm_hart_start_finish()(L446)走 sbi_hart_switch_mode() 的 mret 交付 S 态。热启动侧对称:init_warm_startup()(L448)复用同一批子系统(全部走非 boot 路径),同样经 protection_configure 与 start_finish 进入 S 态。
# firmware/fw_dynamic.S (opensbi@6ad246a1) L49-62,节选
fw_save_info:
/* Save next arg1 in 'a1' */
lla a4, _dynamic_next_arg1
REG_S a1, (a4)
/* Save version == 0x1 fields */
lla a4, _dynamic_next_addr
REG_L a3, FW_DYNAMIC_INFO_NEXT_ADDR_OFFSET(a2)
REG_S a3, (a4)
lla a4, _dynamic_next_mode
REG_L a3, FW_DYNAMIC_INFO_NEXT_MODE_OFFSET(a2)
REG_S a3, (a4)
/* lib/sbi/sbi_hsm.c (opensbi@6ad246a1) L139-158,节选 */
void __noreturn sbi_hsm_hart_start_finish(struct sbi_scratch *scratch, u32 hartid)
{
...
if (!__sbi_hsm_hart_change_state(hdata, SBI_HSM_STATE_START_PENDING,
SBI_HSM_STATE_STARTED))
sbi_hart_hang();
next_arg1 = scratch->next_arg1;
next_addr = scratch->next_addr;
next_mode = scratch->next_mode;
hsm_start_ticket_release(hdata);
sbi_hart_switch_mode(hartid, next_arg1, next_addr, next_mode, false);
}
/* lib/sbi/sbi_hart.c (opensbi@6ad246a1) L771-808,节选 */
val = csr_read(CSR_MSTATUS);
val = INSERT_FIELD(val, MSTATUS_MPP, next_mode);
val = INSERT_FIELD(val, MSTATUS_MPIE, 0);
csr_write(CSR_MSTATUS, val);
csr_write(CSR_MEPC, next_addr);
if (next_mode == PRV_S) {
csr_write(CSR_STVEC, next_addr);
csr_write(CSR_SSCRATCH, 0);
csr_write(CSR_SIE, 0);
csr_write(CSR_SATP, 0);
}
register unsigned long a0 asm("a0") = arg0;
register unsigned long a1 asm("a1") = arg1;
__asm__ __volatile__("mret" : : "r"(a0), "r"(a1));
域与 PMP 的落盘。root 域 region 采用「固件区在前、全内存在后」的固定序——这正是 PMP 隐式优先级(低编号先匹配)的标准用法:前面的固件区对 SU 关闭且带 FW 标志,后面的全内存区才给 S 态 SU RWX 权限,因此「S 态可读写自己、不可碰固件」由一条条目序而非两条规则保证。sbi_domain_dump() 的输出格式为 Domain%d Region%02d … 后跟 M: (F,RWX) 与 S/U: (RWX) 两串动态拼接的权限字母,排查时可直接逐字对照。自 v1.9 开发线起,PMP 不再由 sbi_init() 直呼 sbi_hart_pmp_configure():sbi_hart_pmp_init()(lib/sbi/sbi_hart_pmp.c)按 SMEPMP 扩展存在与否注册 epmp_protection(rating 200)或 pmp_protection(rating 100),由 sbi_hart_protection_configure()(lib/sbi/sbi_hart_protection.c)统一落盘。配置失败时唯一会打印的原始错误串在 oldpmp 与 smepmp_set 两条路径内:Can not configure pmp for domain %s because memory region address 0x%lx or size 0x%lx is not in range.——出现它意味着域 region 的地址或 size 超出 PMP 粒度/可编码范围(NAPOT 最小 8 字节、地址 >>2 编码),此后 S 态的首个越界访问会立即 trap 回 M 态,现场留在 mepc/mcause。
运行时服务与从核启动。S 态 ecall 进入 sbi_trap_handler()(lib/sbi/sbi_trap.c)后按 mcause 分流:中断走 AIA/非 AIA 两条路径,异常中的 CAUSE_SUPERVISOR_ECALL 交 sbi_ecall_handler()(lib/sbi/sbi_ecall.c),按 a7 查扩展、a6 查函数、结果写回 a0(非 0.1 规范同时写 a1),并把 mepc += 4 继续执行。扩展探测走 base 扩展的 probe(EID 0x10、FID 0x3):未注册返回 0、注册且无自定义 probe 返回 1——内核 cpu_set_ops() 正是用它判定 HSM 是否可用。从核启动全链路:S 态准备好 saddr/arg1 后 ecall HSM hart_start → sbi_hsm_hart_start() 以 atomic_cmpxchg 把目标 hart 的 STOPPED 改为 START_PENDING 并 sbi_ipi_raw_send 唤醒它 → 目标 hart 在 sbi_hsm_hart_wait()(MIE 置 MSIE|MEIE)等待票据 → sbi_hsm_hart_start_finish() 释放票据后 sbi_hart_switch_mode() 以同一入口要求进入 S 态。注意 sbi_hart_switch_mode() 只设置 a0/a1,a2 不在其职责内——把 sbi_init 之前的 a0-a4 归档成「下一级要从哪里取什么」正是 fw_save_info() 与 _scratch_init 的分工。
失效边界与验证条件。三类可 grep 的现场:其一,PMP/域配置失败(错误串见上)或域未覆盖某 MMIO,症状是「S 态访问该地址即静默挂死或 trap 进 M 态且无任何日志」;其二,options 置位 SBI_SCRATCH_NO_BOOT_PRINTS(bit0)后横幅消失,容易被误判为「固件根本没跑」,而该位既可在编译期 FW_OPTIONS=0x1 固定,也可由 SPL 在 fw_dynamic_info 的 options 字段打开;其三,next_addr/next_mode 写错时 mret 后第一条 S 态指令就取指异常,串口停在上一条横幅。验证路径:串口 grep Platform Name、Boot HART MP Count/Firmware Base 与 Domain0 Region 开头的行;JTAG/GDB attach 后读 mepc/mcause/mstatus(MPP 位)判断 trap 来自哪一级、停在哪条指令;对照 docs/domain_support.md 的域语义核对 region 覆盖。
成本与权衡。boot prints 提供冷启动时间参照与开机自检输出,代价是每次上电的串口输出与少量时间;子域(domain)能把 UART/CLINT 等 MMIO 收窄给特定 hart 组,换来更强的隔离但增加 bringup 期配置面;fw_dynamic 比 fw_jump 多一次结构体校验与间接取址,换来 SPL 可传 options 与 boot hart 偏好的灵活性——量产固件常关闭 boot prints 并冻结为 fw_jump 形态,而 bringup 期应反过来打开全部可观测项。图 2 把冷启动与热启动两条路径并列画出,读图时先沿红色虚线框走完 boot hart 的全部九步,再对照蓝色框确认从核始终以 sbi_hsm_hart_wait() 等待票据。
图 2:OpenSBI 两条启动路径 —— 左框为 boot hart 从 _start 到 mret 的完整序列(含域与 PMP 落盘),右框为从核的 HSM 等待链路;底部为 S 态 ecall 的 trap→分发路径与失效边界。来源:按 OpenSBI master 6ad246a1 源码整理 [S02][S03];数据口径:行号对应该 commit,域 dump 字母按 sbi_domain_dump() 动态拼接逻辑。
U-Boot:SPL 与 proper 的两次接力 —— FIT 装载与 boot_jump_linux 交接
接口与实现依据(本小节按 U-Boot master ece349ad(2026.07 开发线)源码逐字核实)。U-Boot 在 RISC-V 上分两段执行,两段共用 arch/riscv/cpu/start.S 的 _start,由 XPL/proper 编译期分支分体。寄存器的开局动作决定了整个交接的正确性:_start 先把上级传来的 a0(hartid)与 a1(FDT)搬进 tp 与 s1(mv tp, a0;mv s1, a1),再 mv gp, zero 给 gd 指针一个已知值以防早期 trap;真正的 gd 由 board_init_f_init_reserve()(定义在 common/init/board_init.c,:151 调 arch_setup_gd())经 set_gd() 写入 gp(arch/riscv/include/asm/global_data.h:67 声明 register gd_t *gd asm ("gp"))。两条关键信息随即转入全局数据:SREG s1, GD_FIRMWARE_FDT_ADDR(gp) 与 SREG tp, GD_BOOT_HART(gp)(start.S:175;boot_hart 在本轮核实文件集内唯一写入点)——U-Boot 不是「保留 a0/a1 到跳内核」,而是把 hartid/FDT 持久化为 gd->arch.boot_hart 与 gd->arch.firmware_fdt_addr,跳转前再按要求放回寄存器。寄存器视角可以一句话概括:a0/a1 是外部输入、tp/s1 是内部暂存、gp 是全局数据通道,三组寄存器各司其职;任何一级若把 hartid 或 FDT 放错位置,下一级都会以「FDT 解析失败」或「从核唤不醒」的形式把错误还回来。
SPL 装载链与交棒。SPL 侧顺序为 _start → board_init_f_alloc_reserve(start.S:134,从内存顶部划出 gd 与各 hart 栈)→ board_init_f_init_reserve(:171)→ board_init_f(:218,a0 清零为 boot_flags)→ spl_relocate_stack_gd → j board_init_r(:271,SPL 完全跳过 relocate_code)→ board_init_r(common/spl/spl.c:672)→ spl_board_init()(:738,仅 CONFIG_BOARD_INIT 生效;QEMU 两个 defconfig 均未开启)→ board_boot_order(:755)→ boot_from_devices(:626)→ 媒介装载(典型 spl_mmc_load,common/spl/spl_mmc.c:369→mmc_load_image_raw_sector)→ 统一入口 spl_load()(include/spl_load.h,检出 FDT_MAGIC 后分派 spl_load_simple_fit(),common/spl/spl_fit.c)。FIT 搜索顺序逐字为 firmware →(SPL_OS_BOOT 时)kernel → loadables;entry_point 由 fit_image_get_entry() 从 FIT 属性读得,缺失或为 0 时回落 load_addr。跳转由 RISC-V 覆盖版 jump_to_image() 执行(arch/riscv/lib/spl.c:39-61;旧 jump_to_image_no_args 在本 master 已删除),末段逐字为 image_entry(gd->arch.boot_hart, fdt_blob)——把 a0=hartid、a1=FDT 的 SBI 入口要求原样复述给 OpenSBI;SMP 配置下先 smp_call_function() 唤醒从核再统一跳转。串口最早锚点是 Trying to boot from MMC1(spl.c:646 的统一 printf,%s 来自 spl_loader_name())与 SPL 横幅(preloader_console_init(),spl.c:870)。
/* start.S:SPL 把 a0/a1 持久化进 gd,再以 boot_flags=0 进 C 世界 */
mv tp, a0 /* hartid -> tp(C 代码不改 tp) */
mv s1, a1 /* dtb -> s1 */
mv gp, zero /* gd 先置零防早期 trap */
...
SREG s1, GD_FIRMWARE_FDT_ADDR(gp)
SREG tp, GD_BOOT_HART(gp) /* start.S:175,boot_hart 唯一写入点 */
...
jr t4 /* call_board_init_r:跳到重定位后的 board_init_r */
proper 段:重定位与两条引导路径。OpenSBI 交接后 proper 从 _start 第二段起步:board_init_f_alloc_reserve/board_init_f_init_reserve/board_init_f 建 C 环境,随后 relocate_code 把镜像从装载地址搬到链接地址——本 master 已删除独立的 arch/riscv/cpu/relocate.S,该函数内联在 start.S(拷贝循环以 _start 相对偏移 t6 为基准,重定位项按 R_RISCV_RELATIVE 与符号表型逐条修正;ARM 概念的 relocate_done 符号在 RISC-V 上不存在),终点 call_board_init_r 以 la t0, board_init_r; add t4, t0, t6; jr t4 跳入。board_init_r 之后有两条并行引导路径:现代 bootstd(bootflow_scan_first() → bootmeth_setup_iter_order() → bootdev_get_bootflow() → bootmeth_read_bootflow(),extlinux 与 EFI 各是一个 bootmeth)与经典 distro_bootcmd 环境宏(include/config_distro_bootcmd.h 的 for target in ${boot_targets} 遍历);注意 qemu-riscv64_defconfig 未开 BOOTSTD,其有效 bootcmd 取 boot/Kconfig 默认链的 run distro_bootcmd——在 QEMU 上默认走宏路径而非 bootstd,引用「bootstd 扫描」结论必须带此前提。内核启动:cmd/booti.c 的 do_booti → booti_start → booti_setup(arch/riscv/lib/image.c:35)校验 LINUX_RISCV_IMAGE_MAGIC = 0x05435352(对应源码注释的小端 "RSC\x05",并非字面 "RISCV")与 image_size,并按 text_offset 与 gd->ram_base 决定重定位地址;image_setup_linux → image_setup_libfdt → fdt_chosen 把 bootargs(boot/fdt_support.c:370-375)与 linux,initrd-start/end 写入 /chosen;最后 arch/riscv/lib/bootm.c 的 boot_jump_linux 先由 bootm_final 打印 Starting kernel ...(boot/bootm.c:1198-1202),再 cleanup_before_linux() 刷 cache,最后逐字执行 kernel(gd->arch.boot_hart, images->ft_addr)(bootm.c:67)以 a0=hartid、a1=FDT 跳入内核——与 SPL 跳 OpenSBI 是同一组入口参数的两次复述。
/* start.S:proper 的重定位终点与 C 世界入口 */
call_board_init_r:
jal invalidate_icache_all
jal flush_dcache_all
la t0, board_init_r
add t4, t0, t6 /* t6:relocation offset */
mv a0, s3 /* gd_t */
mv a1, s4 /* dest_addr */
jr t4 /* -> board_init_r() */
失效边界与验证条件。三类典型现场:FIT 节点缺失 → SPL 先 debug("could not find firmware image, trying loadables..."),随后 pr_err("cannot find image node '%s'");magic 不符 → Bad Linux RISCV Image magic!(image.c:43);/chosen 未装载 → 内核以空 FDT 起步并打 No DTB passed to the kernel。验证路径:bootflow scan -l 列出候选与方法;打断自动引导后手工 load mmc 0:1 $kernel_addr Image 加 booti $kernel_addr - $fdt_addr 分步复现;fdt print /chosen 核对 bootargs 与 initrd 属性;bdinfo 核对 relocaddr/ram_base 与 text_offset 判决。
成本与权衡。bootstd 以体积换跨介质一致性,distro_bootcmd 宏路径短但依赖 env 正确性;SPL 跳过 relocate 换来速度,代价是 SRAM 内代码必须位置无关、链接地址即装载地址;FIT 的 loadables 允许多镜像(OpenSBI/U-Boot/DTB)统包,代价是 .its 与签名流程更严格。另有一个易被忽略的次序细节:横幅打印受 CONFIG_BANNER_PRINT 与 SILENT_CONSOLE 控制,量产固件常静默启动,「没有日志」不等于「没有启动」。图 3 按上述两段接力绘制:读图先看左框末端的 image_entry(gd->arch.boot_hart, fdt_blob) 交棒,再看右框 proper 的重定位与 bootflow/distro 双路径。
图 3:U-Boot 两段式接力 —— 左框 SPL 在 SRAM 内的装载与跳转,右框 proper 段的重定位、bootflow 与 distro_bootcmd 双路径及内核交接,右上便签为三类典型失效边界。来源:按 U-Boot master ece349ad(2026.07 开发线)源码整理 [S04][S05];数据口径:行号对该 commit,bootcmd 默认值按 qemu-riscv64_defconfig 与 boot/Kconfig 默认链判定。
Linux:head.S 到 PID 1 —— 两级页表、satp 切换与 init 候选链
接口与实现依据。内核入口要求在 Documentation/arch/riscv/boot.rst 逐条给出:$a0 为当前核 hartid、$a1 为设备树物理地址、$satp = 0(MMU 若存在必须关闭);镜像需置于 PMD 边界(rv64 为 2 MiB)。需要纠正一个流传很广的表述:现行 boot.rst 并未要求 a1 满足 8 字节对齐;与 DTB 相关的真实硬约束是内核对 DTB 的 fixmap 映射必须 PMD 对齐(mm/init.c 中以 BUILD_BUG_ON(FIX_FDT % (PMD_SIZE / PAGE_SIZE)) 断言),以及镜像本身的 PMD 边界放置。镜像头(boot-image-header.rst)以 magic "RISCV" 与 image_size 等字段自描述,供 U-Boot 的 booti_setup() 装载前校验。Linux 源码锚点:arch/riscv/kernel/head.S、arch/riscv/mm/init.c、init/main.c(版本 master 3857c2fe,即 v7.3.0-rc6)。
入口序列。_start(head.S L21)只做镜像头与一条 j _start_kernel;_start_kernel(L210)先 csrw CSR_IE, zero 关中断、S 模式置 SCOUNTEREN、SPINWAIT 构建下以 hart_lottery 抽签选 boot hart(越界 hart 直接 park)、清 BSS 并记录 boot_cpu_hartid;随后 call setup_vm(入参 a0=DTB 物理地址或内置 DTB)、la a0, early_pg_dir; call relocate_enable_mmu 完成 satp 两次切换,再建立 C 环境(tp=init_task、sp=init_thread_union+THREAD_SIZE−PT_SIZE_ON_STACK)、call soc_early_init,最后 tail start_kernel 进入 init/main.c。satp 的两次写是刻意的:先把 trampoline_pg_dir 装入 satp 并 sfence.vma,让 PC 附近的小范围恒等映射生效,再切换为内核 PGD——MMU 关闭状态下不能一步跳到远端页表,这正是 relocate_enable_mmu 被放回 head.S 本体、以物理地址连续执行的原因。页表模式由 satp_mode 决定(默认 Sv57,boot 时经 set_satp_mode() 探测降级:写 early_pg_dir 后 csr_swap(CSR_SATP, 0) 回读校验,硬件不接受则逐级降档),这是「内核在只支持 Sv39 的核上运行」不挂死的机制。paging_init() 的三步(setup_bootmem → setup_vm_final → memblock_allow_resize)把恒等映射升级为正式 linear mapping(swapper_pg_dir),DTB 则全程走 fixmap(fix_fdt_va),保证在 setup_vm 与 setup_vm_final 两套映射下都可访问——boot.rst 第 142–148 行对此有专门说明。
# arch/riscv/kernel/head.S (linux@3857c2fe) L72-122,节选
.global relocate_enable_mmu
relocate_enable_mmu:
/* Compute satp for kernel page tables, but don't load it yet */
srl a2, a0, PAGE_SHIFT
la a1, satp_mode
REG_L a1, 0(a1)
or a2, a2, a1
...
la a0, trampoline_pg_dir
srl a0, a0, PAGE_SHIFT
or a0, a0, a1
sfence.vma
csrw CSR_SATP, a0 /* 先载入 trampoline PGD */
1:
...
csrw CSR_SATP, a2 /* 再切到 kernel page tables */
sfence.vma
ret
start_kernel 到 PID 1。start_kernel(init/main.c L987)按固定序推进(setup_arch → mm_core_init → sched_init → init_IRQ → time_init → console_init → …),其中 setup_arch(arch/riscv/kernel/setup.c)依次 parse_dtb()(解析失败打印 No DTB passed to the kernel)、paging_init()、setup_smp();SMP 的两条路径在 cpu_set_ops()(arch/riscv/kernel/cpu_ops.c)分流:sbi_probe_extension(SBI_EXT_HSM) 成功则 cpu_ops = &cpu_ops_sbi(从核入口 secondary_start_sbi,经 sbi_hsm_hart_start() 让固件放行),否则默认 cpu_ops_spinwait(spinwait_cpu_start() 写 __cpu_spinwait_stack_pointer[hartid],由 bootloader 侧协议放行)——这就是「同一内核实现在 QEMU 与某些真板上从核行为不同」的机制根源。rest_init(L681)现在用 kernel_clone(&init_args)(flags=CLONE_VM|CLONE_UNTRACED,fn=kernel_init)创建 PID 1,用 kernel_thread(kthreadd, …) 创建 kthreadd 后 complete(&kthreadd_done) 并以 cpu_startup_entry 让出 boot CPU。PID 1 的 kernel_init(L1554)先 wait_for_completion(&kthreadd_done),再 kernel_init_freeable()(L1646):page_alloc_init_late → do_basic_setup → wait_for_initramfs → console_on_rootfs(失败打印 Warning: unable to open an initial console.)→ 检查 /init 可达性;失败则 prepare_namespace()(init/do_mounts.c,mount_root + pivot_root)走真实根文件系统。init 候选链是固定顺序的逐个 run_init_process,全失败时以本文最常被引用的 panic 收尾。
/* init/main.c (linux@3857c2fe) L1590-1628,节选 */
if (ramdisk_execute_command) {
ret = run_init_process(ramdisk_execute_command);
...
}
if (execute_command) {
ret = run_init_process(execute_command);
if (!ret) return 0;
panic("Requested init %s failed (error %d).", execute_command, ret);
}
if (CONFIG_DEFAULT_INIT[0] != '\0') { ... }
if (!try_to_run_init_process("/sbin/init") ||
!try_to_run_init_process("/etc/init") ||
!try_to_run_init_process("/bin/init") ||
!try_to_run_init_process("/bin/sh"))
return 0;
panic("No working init found. Try passing init= option to kernel. "
"See Linux Documentation/admin-guide/init.rst for guidance.");
initramfs 与根切换。内置 initramfs 经 rootfs_initcall 注册的 populate_rootfs()(init/initramfs.c)异步解包:async_schedule_domain(do_populate_rootfs, …) → unpack_initramfs() → unpack_to_rootfs(__initramfs_start, __initramfs_size);解包失败按配置走 populate_initrd_image() 或打印 Initramfs unpacking failed: %s。kernel_init_freeable 中的 wait_for_initramfs() 保证在 PID 1 寻找 /init 之前解包完成;prepare_namespace() 再用 mount_root(saved_root_name)(root= 参数)与 pivot_root 把真实根换上来。
失效边界与验证条件。四类典型现场各有专属错误串:无 DTB → No DTB passed to the kernel;initramfs 坏 → Initramfs unpacking failed / rootfs image is not initramfs (%s); looks like an initrd;找不到 init → No working init found. Try passing init= option to kernel.(注意双空格);打不开控制台 → Warning: unable to open an initial console.。验证路径:earlycon 与 console= 决定最早日志通道(earlycon.c 中两个 early_param 注册,console= 还兼作 bootconsole 别名);rdinit=/bin/sh 或 init=/bin/sh 可把「解包是否成功」与「init 是否存在」两个问题解耦;memblock=debug 打印内存布局帮助核对 /memory 原料;CONFIG_DEBUG_VM 下 arch_mm_preinit() 打印虚拟内存布局(print_vm_layout),是页表映射的第一手对照。顺带提醒:本轮 master 已移除 RISC-V XIP 分支(head.S 无任何 XIP 宏、vmlinux-xip.lds.S 不再存在),旧资料中的 XIP 排查路径已失效。
成本与权衡。earlycon 换取最早可观测性,代价是依赖 UART 时钟在早期已就绪;initramfs 内嵌换取根设备无关性,代价是镜像体积与解包内存;spin-table 路径实现简单,代价是需 bootloader 侧协议配合且无法热插拔,SBI HSM 反向取舍;print_vm_layout/memblock=debug 提升可观测性但属 DEBUG 级开销量级,量产内核应默认关闭。图 4 将 Linux 主链与 Android/RTOS 分岔画在同一张图上,读图时先确认 relocate_enable_mmu 位于页表与 C 世界之间,再看 kernel_init 的三个去向。
图 4:内核入口到用户态的完整主链 —— _start/relocate_enable_mmu、start_kernel/rest_init、kernel_init 的 init 候选链与 initramfs 解包,右侧为 Android 与 RTOS 在 kernel_init 之后的分岔。来源:按 Linux master 3857c2fe(v7.3.0-rc6)源码整理 [S06][S07];数据口径:行号针对该 commit,候选链顺序以 init/main.c 逐字为准。
Android init:同一内核入口之后的两阶段用户态
接口与实现依据(本小节按 AOSP platform_system_core@main(a3b721a3)源码逐字核实 [S09][S10];AVB 行号按社区镜像快照 [S11],见附录 C 说明)。Android 与发行版 Linux 的分岔不在内核而在 PID 1 之前:Android 的 init 是一个可在三种身份下运行的多态程序,入口 main()(init/main.cpp:53)按 argv[1] 分派——ueventd 走 ueventd_main(),「selinux_setup」走 SetupSelinux(),「second_stage」走 SecondStageMain(),无参数则默认 FirstStageMain()(init/first_stage_init.cpp:333);一阶段 ramdisk 把 /init 做成指向 /system/bin/init 的符号链接,因此内核 kernel_init候选链命中的第一跳就是它。第一阶段实际序列(按 first_stage_init.cpp 逐行):InstallRebootSignalHandlers 与 umask(0)、clearenv() → mount("tmpfs","/dev") 与 devpts(L351 起)→ mount("proc","/proc")(含 hidepid=2)→ chmod("/proc/cmdline",0440) 与 chmod("/proc/bootconfig",0440) 并读取两者(L362-366)→ 挂 sysfs 与 selinuxfs → mknod 建 /dev/kmsg、/dev/random、/dev/null 等早期节点 → 打印 init first stage started!(L426)→ GetBootMode() 后 LoadKernelModules()(L446)→ 按需 CreateFirstStageMount() 探测设备、DoCreateDevices() 与 DoFirstStageMount()(AVB 校验在此链条内)→ 记录 kEnvFirstStageStartedAt 并以逐字代码 execv("/system/bin/init", {path, "selinux_setup", nullptr})(L559-564)重生自己。注意 SELinux 阶段不是函数内直调,而是通过 argv[1]=selinux_setup 重进 main() 分派到 SetupSelinux()(init/selinux.cpp:762):LoadSelinuxPolicyAndroid() → SelinuxSetEnforcement() → selinux_android_restorecon("/system/bin/init") → 设置 kEnvSelinuxStartedAt → 再次以 second_stage 重进 main() 到 SecondStageMain()(init/init.cpp:954):打印 init second stage started!(L969)→ PropertyInit()(L1014)→ Epoll 就位与 StartPropertyService() → RecordStageBoottimes()(L1054)汇总前三段时间戳 → LoadBootScripts()(L1080)解析 /system/etc/init/hw/init.rc 与 vendor/system_ext 目录 → 进入 epoll.Wait() 主循环(L1181)派发 action/service,最终由 rc 脚本拉起 zygote 与系统服务链。一阶段还有两条常被问到的旁路:boot image ramdisk 中的 prop 文件(kBootImageRamdiskProp)会被复制到调试 ramdisk 供二阶段读取;/force_debuggable 标记会把 adb_debug.prop 与 userdebug 平台策略一并预置,并通过 setenv("INIT_FORCE_DEBUGGABLE","true") 让二阶段放宽调试闸门。AVB 在 first stage 的挂载链内完成:FirstStageMountVBootV2::InitAvbHandle()(init/first_stage_mount.cpp:879)调 AvbHandle::Open(),底层 FsManagerAvbOps::AvbSlotVerify()(fs_mgr/libfs_avb/avb_ops.cpp:270)直调 libavb 的 avb_slot_verify()(avb_slot_verify.c:1385)做 vbmeta 链式校验,AvbHandle::LoadAndVerifyVbmeta()(fs_avb.cpp:437)按返回码分流,失败日志逐字为 avb_slot_verify failed, result: ...。
int main(int argc, char** argv) {
setpriority(PRIO_PROCESS, 0, -20);
if (!strcmp(basename(argv[0]), "ueventd")) {
return ueventd_main(argc, argv);
}
if (argc > 1) {
if (!strcmp(argv[1], "selinux_setup")) {
return SetupSelinux(argv);
}
if (!strcmp(argv[1], "second_stage")) {
return SecondStageMain(argc, argv);
}
}
return FirstStageMain(argc, argv);
}
启动参数与数据流细节。启动参数走 bootconfig 而非 /proc/cmdline:内核文档明确 /proc/bootconfig 是 "a user-space interface of the boot config",载体是追加在 initrd 尾部的 [initrd][bootconfig][padding][size(le32)][checksum(le32)][#BOOTCONFIG\n] 结构;init 侧对两者都做 chmod 0440 防泄露,FirstStageConsole() 与 GetBootMode() 都从 bootconfig 键读取。ramdisk 切换由 ForceNormalBoot 触发:mkdir("/first_stage_ramdisk",0755) → PrepareSwitchRoot() 预置 snapuserd → 自 bind mount 后 SwitchRoot("/first_stage_ramdisk")(first_stage_init.cpp:515-522),使 prepare_namespace() 的 pivot 语义在 Android 分区布局下同样成立。三个阶段各自的时间戳在二阶段汇总:kEnvFirstStageStartedAt(first_stage_init.cpp:556 设置)与 kEnvSelinuxStartedAt(selinux.cpp:800 设置)由 RecordStageBoottimes()(init.cpp:855-868)消费,这是官方提供的「分段耗时」第一手数据。属性空间一旦在二阶段就位,rc 中的 on 触发器与 service 停机超时都可以用 getprop/setprop 在线复现,这把用户态问题从「看日志猜」降级成「改属性试」。此外,一阶段在 InitKernelLogging(argv) 之前先 SetStdioToDevNull(argv),这正是「一阶段早期日志会短暂丢失」的根源——在 /dev/kmsg 节点建立并完成日志初始化之前,内核环缓冲还接不上用户态的写入口,排查阶段边界时对首个字符出现的时刻要有预期。
失效边界与验证条件。第一手的「段界」就是三行日志:内核→用户态看到 init first stage started!,一阶段→SELinux 看到 restorecon 类 FATAL,init second stage started! 之后才轮到 rc 解析与 service 启动失败;AVB 拒绝表现为 avb_slot_verify failed, result: 或红色 boot 界面;分区缺失表现为 Failed to mount required partitions early ...(FATAL)或 recovery 分支的 First stage mount skipped (recovery mode)。验证路径:串口/earlycon 抓三行日志定段;一阶段问题用 /proc/bootconfig 与 /proc/cmdline 内容对照;AVB 用 avbtool verify_image 离线复核 vbmeta 链;二阶段用 logcat -b all 的 early buffer 追 service 启动序。成本与权衡:三段式(一阶段→SELinux→二阶段)让策略与 property 在最小 context 下先就位,可审计性强;代价是 bringup 问题分布在内核、init 一阶段、init 二阶段与 rc/genfstab 四个平面,且每段都以 execv 重进同一二进制,进程内不留「上一段的调用栈」——排查必须依赖日志分段而非栈回溯。另外两个可直接 grep 的辅助串:一阶段挂载能力未就绪时为 FirstStageMount not available;SELinux 阶段 restorecon 失败时逐字报 restorecon failed of /system/bin/init failed。把四个平面与三行日志对齐后,Android bringup 的排障路径与发行版 Linux 本质相同:分区与策略在前、服务与界面在后,只是分段边界更清晰。
RTOS:Zephyr/FreeRTOS/NuttX 如何收敛到同一入口
接口与实现依据(三家快照:Zephyr e989541e [S12]、FreeRTOS-Kernel 8be86d4a [S13]、NuttX 321105a3 [S14],均 main 线)。RTOS 与 Linux 共享同一入口要求(a0=hartid、a1=FDT、satp=0,M 态职责外包给 OpenSBI/fw_jump),但终点不是 exec 用户态而是调度器。Zephyr:arch/riscv/core/reset.S 的复位符号是 __reset → __initialize(不是 __start),CONFIG_INCLUDE_RESET_VECTOR 时先 call __initialize;非外部 SBI 配置下从 csrr a0, mhartid 起步,与 CONFIG_RV_BOOT_HART 相等的一核走 boot_first_core、其余进 boot_secondary_core,末尾 call z_prep_c 进 C 域;z_prep_c()(arch/riscv/core/prep_c.c)清 BSS、拷 RW 数据后进 z_cstart()(kernel/init.c:535):z_sys_init_run_level(INIT_LEVEL_EARLY) → arch_kernel_init()(arch/riscv/include/kernel_arch_func.h:46,置 TLS tp=0、sscratch/mscratch 指 _kernel.cpus[0]、z_riscv_pmp_init())→ z_dummy_thread_init() → soc/board 各级 early hook → prepare_multithreading()(L438,建 "main" 线程并 z_init_cpu(0))→ switch_to_main_thread()(L475)落主线程;bg_thread_main(L266)再跑 POST_KERNEL/APPLICATION/SMP 三级 init 后调用 (void)main(argc, argv)——应用 main() 实际是内核主线程。FreeRTOS:xPortStartScheduler()(portable/GCC/RISC-V/port.c:159)先 vPortSetupTimerInterrupt()(L129,用 csrr mhartid 选 configMTIMECMP_BASE_ADDRESS + hartid*8)再 csrs mie, 0x880 开 MEIE/MTIE,随后 xPortStartFirstTask()(portASM.S:254)从 pxCurrentTCB 取栈并恢复 mstatus(MIE 置位)——首个任务不是 ARM 惯例的 SVC 起跳,而是把任务上下文里保存的 PC 当返回地址、以 ret 落入,源码注释逐字为 "exception return address is used as the function return address";异常统一走 freertos_risc_v_trap_handler(:353),mtimer 中断分支 xTaskIncrementTick() → vTaskSwitchContext(),li t0, 11 /* 11 == environment call */ 也复用同一上下文切换点。NuttX:__start(arch/risc-v/src/qemu-rv/qemu_rv_head.S:47,CONFIG_NUTTSBI 时为 __start_s)保留 a1=DTB、以 riscv_set_inital_sp 给每核栈、la t0, __trap_vec; csrw CSR_TVEC, t0 装向量表后 jal x1, qemu_rv_start(:80)→ qemu_rv_start.c 清 BSS、fdt_register(dtb)、riscv_earlyserialinit() → nx_start()(sched/init/nx_start.c:503)按状态机推进:OSINIT_BOOT → OSINIT_TASKLISTS → OSINIT_HARDWARE → OSINIT_OSREADY → OSINIT_IDLELOOP(每步写 sched_trace_mark),up_initialize()(:677)与 drivers_initialize()(:681)就位后 nx_bringup()(:748)创建初始任务,CPU0 最后进 for (;;) up_idle();。三家共同常量:异常向量在复位早期即装(NuttX 的 __trap_vec 位于 riscv_vectors.S,.balign 64),SMP 从核经 SBI HSM 或平台 spin 协议进入 secondary 路径(NuttX riscv_cpu_boot()、Zephyr boot_secondary_core)。Zephyr main 的 reset.S 已内置 S-mode 直通形态:外部 SBI 配置下先从 M 态接管、设好 mtvec 与委托后再 mret 进 S 态——同一份复位代码同时服务「内置 M 态」与「外包 OpenSBI」两种形态,是国内 SoC 移植时判断固件依赖的第一处开关。三家移植层还都把「最早期异常」当成一等公民:Zephyr 在 arch_kernel_init 初始化 per-CPU scratch 与 PMP、FreeRTOS 对 ISR 栈做对齐断言与填充、NuttX 把向量表 64 字节对齐后立即装载;共同动机是——调试通道(串口)就绪之前,任何异常若没有合法 trap 向量都会变成静默挂死,而静默挂死恰是 RTOS bringup 最难反推的故障类别,这也让「是否引 SBI」成为移植期第一个要拍板的架构决策。
xPortStartFirstTask:
load_x sp, pxCurrentTCB /* Load pxCurrentTCB. */
load_x sp, 0( sp ) /* Read sp from first TCB member. */
load_x x1, 0( sp ) /* 保存的返回地址即任务入口 */
portasmRESTORE_ADDITIONAL_REGISTERS
load_x x5, 1 * portWORD_SIZE( sp ) /* Initial mstatus into x5 (t0). */
addi x5, x5, 0x08 /* Set MIE bit ... */
csrw mstatus, x5 /* Interrupts enabled from here! */
失效边界与验证条件。共同故障模式是最早 trap 向量未就位时的静默挂死(NuttX 在 csrw CSR_TVEC 之前若发生异常,只能靠 JTAG);Zephyr 的 CONFIG_RV_BOOT_HART 配错会让所有核进 secondary 等不到 boot hart;FreeRTOS 的 configMTIMECMP_BASE_ADDRESS 与真实 CLINT 不符则 tick 中断永不触发(任务只跑一次即停);NuttX 的 CONFIG_SMP_NCPUS 小于实际 hart 数时,多余 hart 在 wfi 静默等待(源码注释即「多余 hart: 关中断 wfi」)。对量产场景额外提示:三家都提供静默启动选项,关闭串口横幅后「无输出」不再等价于「未启动」,验收时应以 JTAG 或预置内存标志为准。验证条件:QEMU virt 上以 -bios fw_jump.bin 直接加载 RTOS 镜像即可在串口看到各自启动标记(NuttX 的 showprogress('A') 进度字母最直观);-s -S 配合 GDB 在 __trap_vec、z_prep_c、qemu_rv_start 下断点确认执行流;核对每核栈基址(如 NuttX QEMU_RV_IDLESTACK_BASE)不与内核映像重叠。
成本与权衡。RTOS 启动路径短、确定性好、无 MMU 依赖,代价是缺乏内存隔离与工具生态,可观测性远弱于 Linux/Android(很多失败表现为静默挂死),因此更依赖 JTAG/仿真器;三家都选择「只跑 S/U 态、M 态全外包」以换取与固件生态的兼容,代价是在不提供 SBI 的裸机核上需自带 M 态启动桩。图 4 右侧把 Android 与 RTOS 画成与 Linux 主链并列的对照:Android 在 PID 1 处换入口为两阶段 init,RTOS 则根本没有 exec 路径、初始化完成即进调度器与 main()。工程上更稳妥的做法是把三者各自的启动标记与断点写成板卡验收脚本,让「静默挂死」在验收阶段就被拦住,而不是流入现场。
故障定位速查
先把四级串口锚点固化为排查起点:SPL 横幅 → OpenSBI 横幅 → Starting kernel ... → init 首行;任何失败现场先定位到其中一级,再进入图 5 的矩阵。矩阵把七类典型症状映射到「首先怀疑的阶段与断点」与「验证手段」,覆盖从「毫无输出」的 SPL 前黑盒段到「No working init found」的 initramfs 段,每一行都对应前文某个交接字段或错误串 [S02][S06][S09][S13]。使用方法:先按串口最后一句在矩阵中定位行,再执行该行的验证命令;若验证手段与怀疑阶段对不上(例如串口已到内核却怀疑 PMP),说明「最后一句」被缓冲或日志通道本身不可靠,应先用 earlycon/console= 校正日志通道再继续。诊断能力应分级投入:JTAG/mepc 级手段留给 SPL 前的黑盒段,日志级手段用于用户态之后。
图 5:bringup 症状排查矩阵 —— 七类串口观测(从毫无输出到用户态卡住)对应的怀疑阶段、断点与可复制验证手段,是逐级交接的现场化检索表。来源:按 OpenSBI/Linux/U-Boot/AOSP/RTOS 各节机制与错误串整理 [S02][S06][S09][S13];数据口径:症状行按串口「最后一句」归并,验证手段均为上游工具或文档命令。
总结与展望
本文把从加电到 init 的整条链路压缩成三级可核对对象:每级交接的字段(a0/a1、fw_dynamic_info、/chosen)、承载它的源码落点(fw_base.S、start.S、head.S、init/main.c)与失败时的逐字错误串。三者对齐后,bringup 从「点亮实验」变成可交接的检查流程。以下三条主线决定了这套流程未来两年的演化方向。
主线一:交接接口继续「进规范」。BRS v1.0 于 2025-08-29 冻结后,「OS 期望固件提供什么」(含 fw_dynamic_info 语义与 HSM 使用时机)第一次有了可引用的规范性文本,并与 OS-A 平台规范、RISC-V UEFI 集成指引组成完整接口面;OpenSBI v1.9 开发线把 Zkr 栈保护与 Smrnmi NMI 纳入冷启动可选项;Linux 侧本轮 master 甚至删除了长期缺乏维护的 XIP 分支,收窄入口要求的历史兼容面 [S02][S06][S16][S21]。情景判断:当 RVA23 基线发行版成为国内主流,「固件接口核对」会像 PC 侧 UEFI 一样成为 bringup 交付物的一部分——直接含义是把 boot.rst 与 fw_dynamic_info 字段核对写进 CI 门禁,而不是等板子点亮后手工排除。
主线二:引导框架收敛到「配置化」。U-Boot 的 bootstd/bootflow 把「找设备 → 找方法 → 引导」从板级代码收进通用框架(bootmeth 可插拔),distro_bootcmd 宏以兼容路径并存;SPL 的 FIT 打包从「手工 mkimage」进化为发行版流水线的一环(.its 描述 + 签名 + 校验)[S04][S05]。情景判断:bootflow 标准化让「同一条排查方法跨板复用」第一次成立;但本轮快照里 QEMU 等默认配置仍走 distro_bootcmd——双路径共存期的代价是排查手册必须同时覆盖两套分析工具(bootflow scan -l 与 env 宏调试)。
主线三:国内平台从「移植」转向「归并」。VisionFive 2 与 SpacemiT K1 的固件链已进入上游并由 openEuler 等发行版直接支持,bringup 的重心从改 SPL/OpenSBI 变成写设备树与 board 钩子;尚未上游化的平台(DDR blob、私有存储驱动)仍是黑盒段 [S17][S18][S19][S20]。Android on RISC-V 在官方支持缺位下将维持「厂商 patch + 社区追踪」双轨形态 [S22];RTOS 的增量场景在车规与工业(含 AUTOSAR 适配与双核异构),其确定性优势只有在 JTAG 可达时才兑现 [S12][S13][S14]。完整的行动要点见附录 B 的检查与验收清单。
参考来源
本文全部机制结论以「commit 锁定的上游源码 + 规范文档」双轨取证;下表为 ID 台账(访问日期均为 2026-10-10)。凡正文引用的每个来源编号均在此定义,引用点列给出回查入口。
| 编号 | 来源 | 版本/锚点 | 本文引用点 |
|---|---|---|---|
| [S01] | RISC-V SBI Specification v3.0(docs.riscv.org/reference/sbi/v3.0) | v3.0 | 背景与现状、附录 A(C-01)、附录 B(R-04) |
| [S02] | riscv-software-src/opensbi(github.com) | master 6ad246a1(2026-09-28,v1.9 开发线) | 摘要、背景与现状、技术详解(OpenSBI)、图 1/图 2、故障定位速查、附录 B(R-02/R-03) |
| [S03] | OpenSBI 文档:docs/firmware/fw.md、docs/domain_support.md | 同 [S02] commit | 摘要、技术详解(OpenSBI)、附录 A(C-02)、附录 B(R-03) |
| [S04] | u-boot/u-boot(github.com) | master ece349ad(2026-10-10 抓取,2026.07 开发线) | 背景与现状、技术详解(U-Boot)、图 3、故障定位速查、附录 B(R-02/R-05) |
| [S05] | U-Boot 文档(docs.u-boot.org:spl/bootstd/qemu-riscv 章节) | 对应 2026.07 线 | 背景与现状、技术详解(U-Boot)、图 1、附录 A(C-05)、附录 B(R-05) |
| [S06] | torvalds/linux(github.com) | master 3857c2fe(v7.3.0-rc6) | 摘要、背景与现状、技术详解(Linux/Android/RTOS)、图 4、故障定位速查、总结与展望 |
| [S07] | Linux 文档:Documentation/arch/riscv/boot.rst、Documentation/admin-guide/bootconfig.rst | 同 [S06] tree | 摘要、背景与现状、技术详解(U-Boot/Linux)、附录 A(C-03)、附录 B(R-01) |
| [S08] | Devicetree Specification v0.4(devicetree.org) | v0.4 | 背景与现状 |
| [S09] | aosp-mirror/platform_system_core(github.com) | main a3b721a3 | 技术详解(Android)、背景与现状(成熟度矩阵)、故障定位速查、附录 B(R-02/R-07) |
| [S10] | source.android.com:init 与 bootloader 架构文档 | 2026-10 在线版 | 技术详解(Android)、附录 B(R-07) |
| [S11] | AOSP AVB:libavb(LineageOS 镜像 6ee41dc3 快照)与 source.android.com 验证启动文档 | lineage-22.2 快照 | 技术详解(Android)、附录 C、附录 B(R-07) |
| [S12] | zephyrproject-rtos/zephyr(github.com) | main e989541e | 背景与现状(国内)、技术详解(RTOS)、总结与展望、附录 C |
| [S13] | FreeRTOS/FreeRTOS-Kernel(github.com) | main 8be86d4a | 背景与现状(国内)、技术详解(RTOS)、总结与展望、附录 C、附录 B(R-06) |
| [S14] | apache/nuttx(github.com) | master 321105a3 | 背景与现状(国内)、技术详解(RTOS)、总结与展望、附录 C、附录 B(R-06) |
| [S15] | QEMU 文档:riscv virt 机器与 gdbstub 使用 | 2026-10 在线版 | 摘要、附录 B(R-06) |
| [S16] | Boot and Runtime Services(BRS)v1.0 与 riscv-non-isa/riscv-brs | v1.0(2025-08-29 冻结) | 背景与现状、总结与展望 |
| [S17] | StarFive VisionFive 2 文档(doc-en.rvspace.org) | 2026-10 在线版 | 背景与现状(国内)、总结与展望 |
| [S18] | SpacemiT K1/Bianbu 文档(developer.spacemit.com) | 2026-10 在线版 | 背景与现状(国内)、总结与展望 |
| [S19] | openEuler 安装文档(RISC-V 平台,docs.openeuler.org) | 24.03 LTS 线 | 背景与现状(国内)、总结与展望 |
| [S20] | Sipeed Lichee Pi 4A 文档(wiki.sipeed.com) | 2026-10 在线版 | 背景与现状(国内)、总结与展望 |
| [S21] | RISC-V 平台规范(riscv-platform-specs:OS-A 与服务器扩展) | 2026-10 在线版 | 总结与展望 |
| [S22] | Android on RISC-V 社区追踪(AOSP 公告与 RISC-V Android 相关公开资料) | 2026-10 在线版 | 背景与现状(国内)、总结与展望 |
| [S23] | RISC-V External Debug Support 规范与 OpenOCD/QEMU gdbstub 文档 | 规范 0.13.2 / 在线版 | 摘要、故障定位速查 |
附录 A:主要结论与依据对照
下列五条主要结论逐条给出依据来源、依据类型、适用边界与实践含义。
| 结论 ID | 结论 | 依据来源 | 依据类型 | 适用边界 | 实践含义 |
|---|---|---|---|---|---|
| C-01 | S→M 的全部运行时服务都经 ecall 分发,EID/FID 在 a7/a6,返回在 a0/a1 | SBI v3.0 规范 + sbi_ecall.c 分发实现 [S01][S02] | 规范原文与上游源码 | 仅适用于 S 态调用;M 态固件自身不走 ecall | 串口无输出时先确认是否在 ecall 里 trap |
| C-02 | OpenSBI root 域 dump 的 M:/S/U: 前缀可直接对照 PMP 落盘结果 | sbi_domain.c sbi_domain_dump() 与 pmp 配置 [S02] | 上游源码 | 复杂多域配置需 domain_support.md 语义 | 用域 dump 作为 PMP 核对的第一手材料 |
| C-03 | 内核入口要求不含 a1 8 字节对齐要求 | boot.rst 逐条比对 [S06][S07] | 规范原文 | 未来规范版本可能追加新要求 | 删去排查清单中的无效检查项 |
| C-04 | SMP 从核 SBI HSM 与 spin-table 在 cpu_set_ops() 分流 | cpu_ops.c / cpu_ops_sbi.c / cpu_ops_spinwait.c [S06] | 上游源码 | spin-table 需 bootloader 侧实现放行协议 | 从核挂死先看固件是否有 HSM |
| C-05 | U-Boot SPL 装载 FIT 后先跳 OpenSBI,由固件再交 proper | U-Boot 文档与 SPL 支持链 [S04][S05] | 官方文档 | 直接 u-boot.bin 无 SPL 的板子不适用 | FIT 节点名必须与 SPL 查找名一致 |
附录 B:检查与验收清单
下列七条检查项与验收方法可在目标板上原始复现,不引入自定义工具链作为前置条件。
| 编号 | 检查/行动项 | 验收标准 | 验收方法(可复制命令/观测) | 关联来源 |
|---|---|---|---|---|
| R-01 | 上电前接口静态核查:镜像头 magic(0x05435352)、PMD(2 MiB)对齐、a0/a1/satp 传参路径 | 首次上电即跳过「找不到镜像 / 早期 panic」类问题,串口出现内核 banner | 对照 boot.rst 逐条核对 + booti 校验输出 + earlycon 首行 | [S06][S07] |
| R-02 | 固化四级串口锚点并纳入 CI 断言 | 任意失败现场可定位到四级中的一级 | preloader_console_init 横幅、OpenSBI 版本行、Starting kernel ...、init first stage started! 四行日志 | [S02][S04][S06][S09] |
| R-03 | PMP/域隔离落盘核对 | 域 dump 的 M:/S/U: 与 pmpcfg 实测一致;越界首访能 trap 且 mepc 指向越界指令 | sbi_domain_dump() 输出比对 + JTAG 读 pmpcfg/pmpaddr | [S02][S03] |
| R-04 | 从核放行协议一致性检查 | 从核上线时间可复现;HSM 与 spin-table 两条路径与固件能力匹配 | 核对 cpu_set_ops() 分流结果 + 从核首条日志时间戳 | [S01][S06] |
| R-05 | FIT/.its 与节点名纳入打包流水线 | SPL→proper 交接零人工干预;节点名变更即 CI 失败 | dumpimage -l u-boot.itb 节点清单 + bootflow scan -l | [S04][S05] |
| R-06 | RTOS 平台断言化:trap 向量与 timer 基址 | 复位到首个任务的路径在 QEMU 与真板一致;tick 中断可观测 | -bios fw_jump.bin QEMU 冒烟 + 在 __trap_vec/z_prep_c 处下 GDB 断点 | [S13][S14][S15] |
| R-07 | Android 分段排查手册 + AVB 离线复核 | 三行日志定段;avbtool verify_image 全绿后才刷机 | 串口三行锚点(init first stage started!/SELinux 段/init second stage started!)+ avbtool 校验输出 | [S09][S10][S11] |
附录 C:研究方法与局限
取证流程分四步:其一,锁定版本锚点(OpenSBI master、Linux master、U-Boot master、AOSP system/core main、Zephyr/FreeRTOS/NuttX main),以 GitHub API 取得 HEAD commit 与目标文件的最后修改 sha;其二,抓取源码与文档文本(CDN 镜像与 GitHub contents API,避开不稳定的 raw 通道),全部落在本地快照目录,行号以快照实测为准;其三,由独立核查代理分别完成 OpenSBI、U-Boot、Linux、Android/RTOS 四份材料清单,每条结论必须附「文件:行号 + 逐字片段」,无法给出逐字依据的推断一律进入局限清单(见下文);其四,正文写作以「接口与实现依据 → 失效边界 → 验证条件 → 成本与权衡」四问逐节过关,所有错误串与结构体字段保持逐字,包括 No working init found. Try passing init= option to kernel. 的双空格与 RSC\x05 这类易错细节。
本文为版本快照式调研,以下限制不改变总体结论,但影响细节复核时的预期。其一,U-Boot 结论基于 2026-10-10 的 master 快照 ece349ad(2026.07 开发线),且经 CDN 镜像抓取——已用 GitHub tree/commits API 交叉验证路径与修改时间,但未逐文件比对 blob sha,镜像缓存滞后于 GitHub HEAD 的风险不能完全排除 [S04]。其二,Android AVB 的行号取自社区镜像(platform_external_avb 的指定上游镜像缺 main 分支内容),与 AOSP 主线可能存在行号差异;init 侧材料(a3b721a3)取自 AOSP 镜像 main [S09][S11]。其三,OpenSBI 域 dump 的字母拼接逻辑由 sbi_domain_dump() 代码推得,未在真实硬件上运行核对;AIA 相关链路在本轮未做全量验证 [S02][S03]。其四,Zephyr 的 mtvec/stvec 最终写入点未在已查文件集内定位(soc_interrupt_init() 为疑似安装点但未验证);「向量表拷贝」的常见说法在本轮快照中不成立 [S12]。其五,「唯一写入点」类结论(U-Boot boot_hart、OpenSBI boot 状态变量)基于已抓取的文件集而非全仓库穷举。其六,「QEMU 默认走 distro_bootcmd」只对 qemu-riscv64_defconfig 成立,其他板卡 defconfig 需单独判定 [S05]。其七,FreeRTOS 仅审 GCC/RISC-V 主路径的 trap handler 节选,芯片扩展宏未逐行展开;NuttX 仅核对 qemu-rv 芯片路径 [S13][S14]。最后,全部材料来自公开文本抓取,未执行任何构建、运行或仓库脚本;外部资料文字一律按研究数据处理。
与旧版(v1.0)的差异:v2.0 以源码锚点重建了全部机制章节(v1.0 的「四段职责」概览式叙述废弃),并显式列出三处对旧版/二手资料的事实修正——a1 8 字节对齐要求无一手依据、OpenSBI 已把 PMP 配置重构为 hart protection 框架、rest_init 经 kernel_clone(&init_args) 而非裸 kernel_thread(kernel_init) 创建 PID 1。复现建议:按本文任一来源锚点克隆对应仓库并检出所标 commit,即可用 grep -n 逐行复核本文全部引用与行号。
修订记录
| 版本 | 日期 | 变更摘要 |
|---|---|---|
| v1.0 | 2026-09-30 | 初稿:启动链四段职责、三种 init 分岔与平台现状概览 |
| v2.0 | 2026-10-10 | 按专家级深度重写:全部机制章节落到 commit 级源码锚点与逐字片段;修正 v1.0 中「a1=FDT 必须 8 字节对齐」等表述;更新 OpenSBI master 的固件头/hart protection 重构与 Linux rest_init 的 kernel_clone 变更 |
| v3.0 | 2026-10-10 | 按 2026-10-10 版结构规范重排:正文只保留技术内容,结论对照、验收清单与研究方法移入附录;重拟标题、统一章节命名与正文语言 |
附录 D:术语与缩略语
| 术语/缩略语 | 含义 | 本文口径 |
|---|---|---|
| SBI / OpenSBI | Supervisor Binary Interface / 其参考实现 | SBI 是接口规范,OpenSBI 是上游实现;两者不混用 |
| fw_dynamic | OpenSBI 三形态之一(另两种 fw_jump、fw_payload) | SPL 经 a2 传 struct fw_dynamic_info 的交接方式 |
| SPL | Secondary Program Loader(U-Boot 一阶段) | 运行于片内 SRAM,负责时钟/DDR 与载入 OpenSBI |
| FIT | Flattened Image Tree | 用 .its 描述、mkimage 打包的多镜像容器(u-boot.itb) |
| bootstd | U-Boot 标准启动框架 | bootdev/bootflow/bootmeth 三层扫描机制 |
| PMP | Physical Memory Protection | M 态为低特权级划定的物理地址权限闸门;本文指 OpenSBI 的域到 PMP 落盘 |
| Domain | OpenSBI 域(sbi_domain) | root 域 + 子域;域 region 最终编译为 PMP 条目 |
| HSM | Hart State Management(SBI 扩展) | 从核启动/停止的标准路径;与 spin-table 二选一 |
| FDT/DTB | Flattened Device Tree(.dts 编译产物) | 交接与内核解析的唯一硬件描述来源 |
| initramfs | 内置/独立 cpio 归档 | 内核解包进 rootfs;与外部 initrd 块设备路径不同 |
| AVB | Android Verified Boot | vbmeta 链式校验;init 二阶段挂载前完成 |
| PID 1 | kernel_init 变身的第一个用户态进程 | Linux 上最终 exec 用户态 init;RTOS 无此概念 |