
说实话搞这一轮项目之前我对RISC-V虚拟化的了解还停留在一堆PPT和规格书里。直到我真正把Bao hypervisor从一个纯ARM的思维定式中拔出来硬塞到Banana Pi BPI-SM10这块RVA23开发板上才算把“RISC-V虚拟化”这一串字眼变成了实打实的运行日志。整个过程从踩着ARM经验进场到被RISC-V的启动链和CSR折腾到怀疑人生再到看着Guest Linux在Bao上面跑出shell前后花了近一个月。这篇文章就把我踩过的路、填过的坑、以及几个关键设计决策原原本本记录下来给准备在自己板子上搞Bao移植的朋友当一份参考。1. 项目背景Bao、RVA23与BPI-SM10一场“门当户对”的实验1.1 先把几个名词掰开揉碎Bao是一个极其精简的微内核type-1 hypervisor代码量相比Xen、KVM这类庞然大物小了几个量级但安全边界和隔离能力做得非常硬核它的设计目标是在汽车、航空等安全等级要求很高的场景里充当底层可信计算基。Bao最初主要面向ARMv8-A但官方代码树里早就有了RISC-V 64位的移植分支只是原生的参考平台比较少多数人都在QEMU RISC-V virt上跑真实硬件的案例并不多。RVA23是RISC-V基金会定义的一个application profile可以简单理解成针对通用操作系统和复杂软件栈的一套“配置套餐”。它包含了RV64基础整数指令集并把V矢量扩展、A原子操作、F/D浮点等模块确定为strong requirement在2023年后发布的很多RISC-V SoC上都把它作为卖点。对hypervisor来说RVA23还有个关键点它明确要求虚拟化扩展H扩展相关的指令和CSR在硬件里实现这决定了像Bao这样的虚拟机监视器能不能在芯片上直接跑起来。Banana Pi BPI-SM10是香蕉派推出的一块RISC-V单板机核心SoC采用SpacemiT K1看厂家资料它正好是符合RVA23 profile的八核处理器带矢量扩展板载内存和接口都很齐全。对我这种“想拿真实RISC-V硬件验证hypervisor”的人而言这板子属于当前市场上“能买到、能跑Linux、还能做虚拟化实验”的最佳选择之一。1.2 为什么一定要做这次移植市面上不是有现成的RISC-V hypervisor吗比如Xen、KVM都有RISC-V版为什么非要去碰Bao我的理由很简单第一Bao强调形式化验证和最小可信计算基适合做隔离安全的研究第二Bao在RISC-V上的移植仍然比较“原生”很多平台适配工作都得自己写这恰恰是理解RISC-V虚拟化最有效的方式第三RVA23芯片的普及速度很快国内外的开发板越来越多但成品hypervisor适配却明显滞后。如果能在BPI-SM10这块主流的RVA23板上做出一个可复用的Bao移植方案后面再换到其他同Profile芯片上会省大量力气。所以整个项目的目标非常明确让Bao在BPI-SM10上以HS模式RISC-V的Hypervisor特权层运行成功启动至少一个Guest Linux并且保证Guest与Host之间的内存隔离和中断隔离正常。这可能听起来没那么炫酷但真正动手发现从工具链配置到最终跑通每一步都藏着不少容易踩平的坑。2. 环境准备开发板、交叉工具链和源码工程2.1 BPI-SM10的硬件情况与串口连接先大概说下这块板子。BPI-SM10板载SpacemiT K1 SoC八核心RVA23架构主频能到2GHz附近支持V矢量扩展板上有LPDDR4内存、USB、HDMI、千兆以太网、PCIe这些常用外设。我手上的版本配了16GB内存对嵌入式开发板而言相当阔绰也方便后面切分给多个虚拟机用。硬件调试的第一步永远是串口。BPI-SM10的调试串口号是固定的我用USB转TTL线接在板子上的UART排针位置波特率默认是1152008N1格式。这套板子默认烧了厂家提供的Bootloader和Linux镜像开机后通过串口能看到U-Boot的启动日志这一点很重要因为后续Bao加载和调试都要靠这条串口输出。连接串口后用GNU screen或者minicom都可以我用的是screenscreen /dev/ttyUSB0 115200开机后观察到的启动流程是SoC内部ROM先从eMMC或SD卡加载第一级引导程序之后进入OpenSBIM-mode再交给U-BootS-mode最后由U-Boot引导Linux内核。这个流程对后续Bao的安置方式影响很大后面单独讲。2.2 交叉编译工具链的选择与安装RISC-V的交叉编译工具链有很多发行版我推荐直接用riscv64-linux-gnu系列Debian/Ubuntu的软件源里就有安装很快sudo apt install gcc-riscv64-linux-gnu binutils-riscv64-linux-gnu但如果要编译Bao本身还需要RISC-V的裸机工具链或带Newlib的工具链。Bao的代码编译时依赖marchrv64imac或者带V扩展的参数用一般的Linux工具链也能凑合但要小心链接脚本和启动代码对工具链的行为比较敏感。我更推荐用的是SiFive官方提供的Freedom Tools工具链或者一些教程里常推荐的riscv64-unknown-elf-系列避免后续出现.align和重定位相关的奇怪报错。如果不想折腾直接使用Bao官方文档推荐的riscv64-unknown-linux-gnu-gcc也可以关键是保证整个工程用同一套CROSS_COMPILE前缀。我用的是自编译的riscv64-unknown-elf工具链版本是GCC 13实测下来编译Bao零警告。2.3 获取Bao源码和目录结构Bao的源码托管在GitHub上直接克隆git clone https://github.com/bao-project/bao-hypervisor.git --recursive其中--recursive是为了拉取BMI等子模块没有它后面编译可能失败。Bao的源码结构不是特别复杂关键目录主要有hypervisor/arm64/和hypervisor/riscv64/这两个架构相关的核心代码以及platform/目录下各种单板支持文件。RISC-V移植相关的核心在hypervisor/arch/riscv64/下面我之前在QEMU上跑过但真实硬件上的平台分支并没有什么现成的因为BPI-SM10显然不是Bao官方支持的板子。这意味着需要自己写一个新的platform目录这是整个移植过程中工作量最大的部分。3. 核心移植工作平台适配与Hypervisor配置3.1 RISC-V虚拟化层里Bao到底做了什么在深入改代码之前最好先理解Bao在RISC-V上的位置。RISC-V有四个特权级M机器、HSHypervisor有时也叫S with H、VS虚拟机监控器运行下的管理程序、VU虚拟用户。带有H扩展的SoC硬件上就会支持HS和VS模式切换。Bao运行在HS模式而每台Guest Linux运行在VS模式里。每次Guest尝试执行特权指令或者触发中断时会陷入HS模式的BaoBao负责仲裁、模拟、再转交给Guest。RISC-V的H扩展提供了hstatus、hedeleg、hideleg、hgatp二级页表基址寄存器等CSRBao就是靠这些机制来拦截Guest资源访问同时维护不同Guest之间的隔离。所以移植Bao到真实板子真正要动手的关键点有三个获取正确的硬件平台信息UART地址、中断控制器类型和基址、实现Bao在当前特权级的异常入口和上下文切换、配置好虚拟机描述符里的内存区域和启动参数。三者缺一不可任何一个没对齐表现就是开机后毫无输出或者卡死在未知地址。3.2 新增BPI-SM10平台支持文件在Bao源码里增加一个新平台我的做法是仿照已有的RISC-V platform目录结构新建platform/bananapi-bpi-sm10/目录。目录里至少需要提供硬件初始化和串口输出两部分。Bao有一个platform_arch结构体需要填充UART地址、定时器频率、中断控制器基地址等信息。SpacemiT K1的UART地址从官方设备树里可以查到常见的是0x30000000附近但每块板子批次可能不一样。我强烈建议拿到板子后先用Linux内核启动时的设备树内容核对这些地址不要依赖网上的固件信息因为交叉编译的OpenSBI和U-Boot版本不一致地址可能被重映射。在platform.c里UART初始化通常要处理三件事设置GPIO复用为UART功能、配置波特率分频、使能发送和接收中断。Bao在启动阶段打印日志全部依赖UART如果这边不对后面所有调试都是盲人摸象。新平台的入口代码一般要参考同系列的RISC-V platform写法定义好platform_init()、uart_init()、uart_putc()这些函数然后用PLATFORM_SP和对应宏把平台注册进工程。3.3 虚拟机和内存布局的配置Bao通过config.c文件描述虚拟机集合。这个文件通常位于platform/bananapi-bpi-sm10/config.c里面定义了几个关键结构体内存区域mem_range、虚拟机vm_config、虚拟CPUvcpu_config。最简单的情况是划分两个内存区间一段留给Bao自身另一段给Guest Linux。我给Guest划分了1GB内存而Bao自身占用低端的256MB。配置时要注意所有地址必须严格和实际RAM布局对应尤其不能和Bootloader/OpenSBI占用的区域冲突否则Guest里的Linux会在非常诡异的位置跑飞然后报一串无法理解的page fault。一个简化后的关键片段长这样struct mem_range mem_ranges[] { {.va 0x80000000, .pa 0x80000000, .size 0x10000000}, /* 256MB for Bao */ {.va 0x40000000, .pa 0x40000000, .size 0x40000000}, /* 1GB for Guest */ };注意这里Guest的内存地址我故意没有从Bao的地址之后连续分配而是放在了低地址因为这颗SoC的Linux启动时会对DDR首块区域有特殊要求直接沿用QEMU的连续布局会触发DMA和定时器初始化失败。这也是我在真实硬件上碰到的第一个大坑后面再细说。3.4 编译Bao时的细节Bao使用Makefile管理构建平台通过环境变量传递。我的编译命令如下make PLATFORMbananapi-bpi-sm10 ARCHriscv64 CROSS_COMPILEriscv64-unknown-elf-第一次编译大概率不会一次通过。常见问题包括has no asm goto support工具链太老换GCC 12以上。找不到platform_bananapi_bpi_sm10.h需要在include路径中增加新建的头文件。链接时出现地址越界检查链接脚本里的内存起点RISC-V的裸机程序通常默认从0x80000000开始但Bao被OpenSBI加载时实际起点可能不同必要时需在Makefile里覆盖LD_ADDR。成功编译后在build/riscv64/bananapi-bpi-sm10/下会生成bao.elf和bao.bin。bao.bin是给Bootloader直接加载的裸二进制镜像。4. 启动链路OpenSBI、U-Boot与Bao的分工配合4.1 特权级跃迁的方式选择这是整个移植中我最头疼、也最值得复盘的部分。BPI-SM10的U-Boot默认运行在S-mode而Bao需要运行在HS-mode。S-mode理论上并不能直接跳到更高特权级所以“让U-Boot像加载Linux一样直接booti到Bao”的意图是行不通的。RISC-V的严格分层要求从M-mode切换到HS-mode。我可选的方案有两个一是重新编译OpenSBI把Bao作为payload合并进去让OpenSBI在初始化完成后直接切换到HS模式并跳转Bao二是用U-Boot的命令行go直接跳转但前提是U-Boot本身编译为M-mode运行。我对厂家默认固件做了些检查发现它用的是OpenSBI U-Boot的组合于是选择了更稳妥的方案一也最符合RISC-V系统的常规做法OpenSBI作为M-mode固件下一阶段可以是U-Boot也可以是Bao。实际操作时我在OpenSBI的编译配置里指定FW_PAYLOADy并把FW_PAYLOAD_PATH指向Bao的镜像路径make CROSS_COMPILEriscv64-unknown-elf- PLATFORMgeneric FW_PAYLOADy FW_PAYLOAD_PATH/path/to/bao.bin这样OpenSBI在完成PLIC、Timer、IPI等M-mode服务初始化后会自动进入HS-mode并跳转到Bao的入口地址。Bao启动过程中如果需要SBI服务比如让Guest通过SBI进行系统调用仍然会通过OpenSBI的接口来完成所以这套组合并不会切断M-mode基础设施。4.2 设备树与内存保留区的修改虽然Bao本身不完全依赖设备树但Guest Linux在被Bao启动时肯定需要通过某种方式拿到设备信息。我的做法是先让Bao自己解析一份经过裁剪的设备树然后在虚拟机初始化时把内存和设备树地址传给Guest。具体实现上Bao的RISC-V启动流程里会从一个固定物理地址读取设备树这个地址必须在config.c里预先声明并且和实际加载位置一致。为了不让Guest Linux把Bao自己占用的内存误当成可用RAM我还需要修改传给Guest的设备树在/reserved-memory节点中把Bao的256MB区间预留出来。这一步非常容易漏漏了之后最典型的症状是Guest Linux启动到一半突然在页表分配阶段卡死因为Bao自己的代码和数据正被Guest的Linux物理内存管理子系统一把捞走互相踩踏。一个粗粒度的保留节点如下reserved-memory { #address-cells 2; #size-cells 2; ranges; bao_reserved: bao80000000 { reg 0x0 0x80000000 0x0 0x10000000; no-map; }; };实际调试时我并没有直接修改源设备树文件而是在U-Boot加载Bao之前用U-Boot的fdt命令临时给设备树打补丁或者是在OpenSBI FW_PAYLOAD模式下用fdt_overlay机制。哪种方式都可以关键是记得做这一步。4.3 镜像制作与启动实测OpenSBI把Bao编译进payload之后最终镜像是一个FIT或裸的二进制。我通过厂家提供的烧录工具把OpenSBIU-Boot的固件先烧到SD卡上然后再把Bao镜像放到FAT分区里当然也可以直接把Bao编入OpenSBI payload这样启动时根本不需要再单独加载镜像。我的手忙脚乱之处在于厂家默认的U-Boot会尝试从FAT分区加载Linux内核而我并不想让它进入Linux而是希望它直接跳到Bao。因此需要修改U-Boot的启动环境变量bootcmd或者用串口进入命令行手动执行引导setenv bootargs consolettyS0,115200 root/dev/mmcblk0p2 ro load mmc 0:1 0x80200000 bao.bin go 0x80200000如果前面OpenSBI和Bao的平台配置都正常串口上应该能看到Bao的启动日志然后看到它加载Guest Linux的内核、设备树最终进入Guest的登录shell。这里有个容易混淆的地方U-Boot的go从S-mode跳转但在OpenSBI已经把Bao作为M-mode payload的情况下这步其实不成立。所以更推荐直接用OpenSBIU-BootLinux原本的启动链把Bao镜像放到一个已知内存地址并由U-Boot交给OpenSBI的SBI服务去启动避免特权级越过头的错误。4.4 首次成功运行的日志长什么样成功的时候Bao会打印一串平台初始化信息比如hypervisor版本、虚拟机数量、每个VM的内存范围和入口地址。紧接着会有几条包含VM 0: vcpu 0 starts at ...的信息。之后便能看到Linux内核自带的那一串熟悉的启动日志。如果中途卡住可以根据Bao日志最后输出的位置判断是在哪个阶段。通常如果它压根没打印那是Bao自身或串口初始化的问题如果打印了Bao的信息但Guest Linux不启动则多半是设备树或内存布局问题如果Guest启动一半崩溃大概率是中断或时钟模拟没配置好。5. 调试实录我踩过的最深的几个坑5.1 串口打印空白却无日志项目刚开始那几天最常遇到的情况是按复位键后板子一段输出都没有连U-Boot都不打印。这个一开始我以为是Bao编译错了后来排查发现是串口转接板的电平问题BPI-SM10的调试串口是1.8V电平普通USB转TTL模块的3.3V/5V电平会把信号识别成异常状态导致只出乱码或者干脆什么都不出。解决方式很简单换一个1.8V电平兼容的串口模块或者在线上加电平转换。嵌入式老玩家可能觉得这种坑根本不值得写但恰恰是这种“低级”问题最容易在深夜卡住人。5.2 Guest Linux中无法使用矢量扩展BPI-SM10的SoC支持RVA23意味着默认支持V矢量扩展。但我一开始在Bao配置文件里没有给Guest的vCPU开放对应的hstatus扩展位导致Guest里一执行向量指令就触发“Illegal Instruction”异常程序直接崩溃。后来查Bao的RISC-V实现发现虚拟机的启动上下文里会明确设置一些CPU能力位需要把V扩展对应的bit位加上。修改方法很直观在虚拟CPU初始上下文里设置hstatus.VS和必要的初始CSR值让硬件知道Guest允许使用矢量寄存器。另外Bao自己也要确保切换vCPU时能正确保存和恢复矢量寄存器这部分在不同版本里成熟度不一样我顺带打了个小补丁。5.3 PLIC中断没有正确送达Guest另一个典型问题出现在调试Guest Linux网络驱动的时候千兆网卡的中断始终不触发Guest里ifconfig能起来但一ping就卡死。排查一圈后发现是Bao的PLIC虚拟化没有正确配置。RISC-V的PLIC平台级中断控制器是所有核共享的外设Bao需要维护一张中断源到虚拟机的映射表。默认配置里我把所有中断都绑定给了HostGuest自然永远收不到。修正办法是给Bao增加一个PLIC控制接口将网卡的中断号重新分配给Guest。由于Bao本身不提供像KVM那样完善的设备模拟框架这一步需要自己写简单的寄存器转发层。如果你只想做最小验证建议Guest里的Linux先禁用网卡驱动只用console登录减少中断模拟带来的麻烦。5.4 启动时Guest “卡死在内存初始化”这个坑在3.3节提过本质是我最初沿用了QEMU的连续内存布局Guest Linux在内存扫描时把自己附近的保留区域错误地当成常规RAM。解决方式就是把Bao内存和Guest内存安排在不同的物理DDR区域同时在设备树reserved-memory里显式声明。说实话这个做法有点反直觉因为大多数hypervisor倾向于让Guest内存紧挨着Hypervisor的内存方便页表映射但在BPI-SM10这种带特定DMA约束的SoC上反而不如物理分离干净。6. 移植完成后的状态与几点心得6.1 最终跑起来的效果目前这块BPI-SM10上的Bao可以稳定启动一个Guest LinuxGuest内能执行大部分普通计算任务i2c、uart、定时器都能正常工作串口输入输出顺畅。并发跑两个Guest也没有明显问题因为每个Guest都有独立内存和独立vCPUBao的调度切换顺序执行代码短小精悍的特性体现得很明显。我还在Guest里交叉编译跑了一个带V矢量指令的矩阵乘测试验证了RVA23的矢量能力从Guest到Host再到硬件的整条通路是通的。这个结果对项目本身是个鼓舞毕竟不是所有RISC-V开发板都能轻松把完整虚拟化栈验证到指令级。6.2 性能与隔离性的主观感受由于手头没有现成的benchmark框架我只做了粗略的时间对比。Bao上跑Linux的启动时间比裸机Linux慢了大约一两秒这主要是早期的experimental实现里还有不少额外trap处理和全量寄存器切换导致后续可以通过精简映射和优化VM退出路径来压缩。内存隔离方面我用一个Guest访问另一个Guest的物理地址Bao的页表机制会直接拒之门外报错信息指向非常明确说明隔离的base已经立住了。性能优化的下一步很明确把Bao的RISC-V trap入口改成更精简的汇编路径减少每次VM exit时的无效上下文保存另外给PLIC加一个直接优先级映射避免每次中断都经过软件转发。6.3 给后来者的一句总结如果你也要做类似的事情我的最大建议就是先把启动链路搞透彻再碰代码。RISC-V的多级特权模型比ARM复杂HS到底怎么从M-mode进来、S-mode的U-Boot又能在哪一步介入这些搞不清楚后面的所有调试都会变成玄学。第二点是别迷信QEMU上的经验真实SoC的设备树、PLIC地址和DMA行为差异极大任何地址都需要和板厂SDK反复核对。Bao的代码量虽小但它对平台假设极其严格稍有偏差就是“无声死机”。这种时候一台好的逻辑分析仪和一个能打印的UART就是你最好的朋友。