学习: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/a1sepc += 4SBI 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_EXTMULTI_DIS 子类型 。默认 KVM 侧是"全部可用",但对于 DBCN 这类需要转发给用户态 的扩展,KVM 会标 default_unavail,要求用户态显式使能才收得到转发------防止老版本 lkvm 收到无法处理的调用。

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

setup_fdt() 生成的节点里没有 SBI 相关内容,只有 /chosen(bootargs、initrd)、/memory/cpusriscv,isammu-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 探测。

相关推荐
传奇开心果编程3 小时前
【Xilem 0.4 基础语法学与练】第一课:从零到计数器
学习·rust·前端框架
HEJOO93 小时前
深入理解 Java volatile 关键字:原理、用法与常见误区
java·开发语言·spring
曹牧3 小时前
Java:no content to map due to end of input
java·开发语言
阿维的博客日记3 小时前
Example 类全场景实战指南
java·mybatis
两点王爷4 小时前
SpringBoot 项目集成 GeoServer 源码,实现地图服务发布
java·spring boot·后端
David猪大卫4 小时前
【C++修炼】智能指针使用及原理
开发语言·c++·经验分享·笔记·学习·考研·面试
z23456d4 小时前
第四十一天学习心得
学习
Javatutouhouduan4 小时前
Java如何速通性能优化难题?
java·java面试·jvm调优·后端开发·java程序员·java八股文·java性能优化
lemon_sjdk4 小时前
DOM 节点
java·前端·javascript