一句话结论
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, ®);
即 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 探测。