学习:RV lkvm SBI

一句话结论

lkvm 起的 Guest 里根本没有 OpenSBI 。SBI 信息不来自任何固件文件,而是分三处拼出来的:主体由 Host 的 KVM 内核模块实现 (arch/riscv/kvm/vcpu_sbi*.c),部分调用转发给 lkvm 用户态模拟 ,可见的扩展集合由 lkvm 通过 KVM_SET_ONE_REG 在启动前配置 。设备树在其中几乎不扮演角色------Guest 靠 ecall 探测 SBI,而不是读 DTB。


1. 为什么这里没有 OpenSBI

裸机 / QEMU 的路径是:ZSBL → OpenSBI(M-mode) → Linux(S-mode),SBI 由 M-mode 固件提供。

kvmtool 的路径完全不同------它直接把内核镜像丢进 Guest,Guest 跑在 VS-mode ,Guest 的 ecall 被硬件直接送到 HS-mode 的 KVM 模块手里。所以 KVM 自己就是 Guest 的"SBI 实现者"。kvmtool 的 patch 里也明确写着 "Firmware loading is not implemented currently... booting kernel directly without any bootloader"。

2. SBI 信息的三个来源

① 绝大部分:Host KVM 内核模块内建

arch/riscv/kvm/vcpu_sbi.c 里有一张扩展表,Host 内核编译时就固定了:

复制代码
static const struct kvm_riscv_sbi_extension_entry sbi_ext[] = {
    { KVM_RISCV_SBI_EXT_V01,  &vcpu_sbi_ext_v01  },   /* legacy, 依赖 CONFIG_RISCV_SBI_V01 */
    { KVM_RISCV_SBI_EXT_MAX,  &vcpu_sbi_ext_base },   /* Can't be disabled */
    { KVM_RISCV_SBI_EXT_TIME, &vcpu_sbi_ext_time },   /* set_timer → vcpu timer */
    { KVM_RISCV_SBI_EXT_IPI,  &vcpu_sbi_ext_ipi  },   /* → IRQ_VS_SOFT */
    { KVM_RISCV_SBI_EXT_RFENCE, ... }, { _SRST }, { _HSM },
    { _PMU }, { _DBCN }, { _STA }, { _EXPERIMENTAL }, { _VENDOR },
};

版本号和实现 ID 是 KVM 自己定义的常量,与 Host 上的 OpenSBI 版本无关:

复制代码
#define KVM_SBI_IMPID 3
#define KVM_SBI_VERSION_MAJOR 0
#define KVM_SBI_VERSION_MINOR 2

即 Guest 里 sbi_get_spec_version() 拿到的 0.2,是 KVM 报给它的,不是 Host 固件的版本。

② 内核处理不了的:转发给 lkvm

kvm_riscv_vcpu_sbi_forward() 把调用打包成 KVM_EXIT_RISCV_SBI 退出到用户态:

复制代码
run->exit_reason = KVM_EXIT_RISCV_SBI;
run->riscv_sbi.extension_id = cp->a7;
run->riscv_sbi.function_id  = cp->a6;
run->riscv_sbi.args[0..5]   = cp->a0..a5;

lkvm 侧 kvm_cpu_riscv_sbi() 处理:

复制代码
case SBI_EXT_0_1_CONSOLE_PUTCHAR:
    ch = vcpu->kvm_run->riscv_sbi.args[0];
    term_putc(&ch, 1, 0);          /* 打到 lkvm 的终端 */
    vcpu->kvm_run->riscv_sbi.ret[0] = 0;
    break;
case SBI_EXT_0_1_CONSOLE_GETCHAR:
    ...  term_getc() : SBI_ERR_FAILURE;

返回后 kvm_riscv_vcpu_sbi_return() 把 ret[0]/ret[1] 写回 a0/a1 并 sepc += 4。SBI experimental / vendor 两类扩展是无条件转发给用户态的(KVM 无从知晓厂商语义)。较新的 kvmtool 还加了 SBI SUSP 的测试用 handler(睡 5 秒再恢复 Guest)。

③ 可见扩展集合:lkvm 主动下发配置

lkvm 提供 --disable-sbi-legacy / -time / -ipI / -rfence / -srst / -hsm / -pmu / -dbcn / -susp / -sta / -experimental / -vendor 等开关,存入 cfg.arch.sbi_ext_disabled[],在 kvm_cpu__arch_init() 里对每个 vCPU 下发:

复制代码
reg.id   = RISCV_SBI_EXT_REG(KVM_REG_RISCV_SBI_MULTI_DIS, i);
reg.addr = (unsigned long)&masks[i];
ioctl(vcpu->vcpu_fd, KVM_SET_ONE_REG, &reg);

即 ONE_REG 接口 KVM_REG_RISCV_SBI_EXT 的 MULTI_DIS 子类型 。默认 KVM 侧是"全部可用",但对于 DBCN 这类需要转发给用户态 的扩展,KVM 会标 default_unavail,要求用户态显式使能才收得到转发------防止老版本 lkvm 收到无法处理的调用。

3. 关于设备树:这里不是 SBI 的载体

setup_fdt() 生成的节点里没有 SBI 相关内容,只有 /chosen(bootargs、initrd)、/memory、/cpus(riscv,isa、mmu-type)、中断控制器、/smb 下的设备。

原因很直接:SBI 是运行时 ABI,靠 ecall 探测,不是靠 DTB 描述 。Guest 内核启动时调 sbi_get_spec_version() / sbi_probe_extension(),这些调用正好被上面 ① 的 KVM base 扩展接住。

唯一沾边的是 SBI base 扩展的 GET_MVENDORID / MARCHID / MIMPID------这些值来自 vCPU 的 CSR,可以用 --custom-mvendorid / --custom-marchid / --custom-mimpid 覆盖。

4. 时序

复制代码
KVM_CREATE_VCPU
   └─ kvm_arch_vcpu_create() → kvm_riscv_vcpu_sbi_init()
        逐个 probe(),填 ext_status[](AVAILABLE / UNAVAILABLE)
                ↓
lkvm: KVM_SET_ONE_REG(SBI_EXT / MULTI_DIS)    ← --disable-sbi-xxx 生效
                ↓
KVM_RUN
   └─ Guest ecall
        ├─ 命中 in-kernel handler → 内核直接处理(timer/IPI/rfence/HSM...)
        └─ 无 handler → KVM_EXIT_RISCV_SBI → lkvm 模拟 → 返回

注意 kvm_riscv_vcpu_sbi_init() 的注释写得很清楚:"This must be the last thing to be initialized"------所有 SBI 配置必须在 vCPU 首次 RUN 之前完成,之后改是要走 reset 语义的。

5. 与 Host SBI 的真实关系

维度 是否受 Host 影响
Guest 看到的 SBI 版本号 / IMPID 否,KVM 常量决定
Guest 可用的扩展列表 主要由 Host 内核 CONFIG_* 和 kvmtool 开关决定
部分扩展的底层实现 是 ,例如 RFENCE 的 SBI_EXT_RFENCE_REMOTE_SFENCE_VMA 最终调 Host 侧的 sbi_remote_hfence_vvma();PMU 依赖 CONFIG_RISCV_PMU_SBI
控制台字符输出 由 lkvm 的终端接管,与 Host 串口无关

换句话说:Guest 的 SBI 是 KVM 虚拟出来的一个"假固件",只有落到真实硬件操作那一步时才借用 Host 的 SBI。

6. 想看实际效果

复制代码
# 关掉某个扩展,看 Guest 是否降级
lkvm-static run -k ./Image -i ./rootfs.img --disable-sbi-hsm -c 2 --debug

# Guest 里看内核探测到的 SBI
dmesg | grep -i sbi        # 或 /sys/firmware/...

--debug 下 lkvm 会把未处理的 SBI 调用(extension_id/function_id/args 全部打印出来)写进 debug fd,这是排查"SBI 调用没人接"最直接的手段。


一句话总结 :lkvm 场景没有 OpenSBI------SBI 由 Host KVM 模块内建实现(版本/实现 ID 是 KVM 自己的常量),处理不了的调用经 KVM_EXIT_RISCV_SBI 转发给 lkvm 用户态模拟,Guest 可见的扩展集合由 lkvm 通过 KVM_REG_RISCV_SBI_EXT 的 ONE_REG 接口在 vCPU 启动前裁剪;设备树不承载 SBI 信息,Guest 靠 ecall 探测。

相关推荐
Sarvartha4 小时前
final 关键字
java·开发语言
2601_952047794 小时前
单一策略深度教程:用轻易云把集成任务的报错实时推送到钉钉机器人
java·机器人·钉钉
Joy T5 小时前
Spring AI 项目中的 pom.xml、application.yml 与 Nacos:从依赖管理到运行配置
spring·nacos·pom.xml·profile·参数配置·application.yml
数据狐(Datafox)5 小时前
淘宝图片搜索 API 落地实战:基于以图搜货搭建跨境电商选品系统
java·大数据·微服务
李日华大战鸡红7 小时前
FOC状态空间方程模型推导(学习记录)
python·学习·线性代数
Escalating_xu7 小时前
【C 语言】数据在内存中的存储:补码、大小端、整型陷阱与 IEEE 754 全解析
java·c语言·网络
灯澜忆梦7 小时前
【面向对象编程C++】| 基础语法
java·c++·算法
一个风轻云淡7 小时前
GCC 和 GDB命令简单解读
java·linux·前端