FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

RISC-V ハイパーバイザー拡張の勉強 (rvvisorの実装を読む)

RISC-Vのハイパーバイザー実装、いろいろ既存の実装を眺めているが、以下の実装がかなり読みやすく実装されているように感じたので中身を眺めてみることにした。

github.com

RISC-VでCPU実験を攻略された方の実装だが、しかしこれだけのコーディングを短期間で仕上げられるのか、非常に驚いている。まずはビルドから。

すべてRustで記述されている(それだけでもすでにすごいのだが...)、RustのコンパイラはNightly Buildにしておく必要があった。

rustup target add riscv64gc-unknown-none-elf || true

# build hypervisor
cd hypervisor
CC=riscv64-unknown-linux-gnu-gcc cargo build
cd ..

# build guest
cd guest
CC=riscv64-unknown-linux-gnu-gcc cargo build
cd ..

RISC-V版QEMUにPATHを通していないので実行はしていない。しかしソースコードを読めば何となく動作は分かりそうな気がしている。Hypervisor側を眺めてみよう。以下私のメモなので間違っている可能性大いにあり。

まずはrust_hypervisor_entrypoint()がエントリポイントだと思われる。

  • rvvisor/hypervisor/src/hypervisor.rs
#[no_mangle]
pub fn rust_hypervisor_entrypoint() -> ! {
    log::info!("hypervisor started");

    if let Err(e) = init() {
        panic!("Failed to init rvvisor. {:?}", e)
    }
    log::info!("succeeded in initializing rvvisor");
...
}
  • rvvisor/hypervisor/src/hypervisor.rs
pub fn init() -> Result<(), Error> {
    // inti memory allocator
    paging::init();

    // init virtio
    virtio::init();

    // hedeleg: delegate some synchoronous exceptions
    riscv::csr::hedeleg::write((1 << 0) | (1 << 3) | (1 << 8) | (1 << 12) | (1 << 13) | (1 << 15));

    // hideleg: delegate all interrupts
    riscv::csr::hideleg::write(
        riscv::csr::hideleg::VSEIP | riscv::csr::hideleg::VSTIP | riscv::csr::hideleg::VSSIP,
    );
...

paging::init()はページを確保するアドレスを設定している。ここではELFの一番最後の位置をページの先頭としている?

hedeleghidelegの設定によりすべての仮想化モードの例外と仮想化割り込み信号を仮想マシンに転送するように設定している。

stvecにTrapアドレスを設定することで、トラップ時にTrap()にジャンプするように設定されているようだ。

    // stvec: set handler
    riscv::csr::stvec::set(&(trap as unsafe extern "C" fn()));
    assert_eq!(
        riscv::csr::stvec::read(),
        (trap as unsafe extern "C" fn()) as usize
    );

init()が終わると、ゲストプログラムをメモリ上に展開して、ゲストに移動している。ゲストのイメージをファイルに展開しているコードについては省略。

#[no_mangle]
pub fn rust_hypervisor_entrypoint() -> ! {
    log::info!("hypervisor started");
/* ... 中略 ... */
    // TODO (enhnancement): multiplex here
    let guest_name = "guest01";
    log::info!("a new guest instance: {}", guest_name);
    log::info!("-> create metadata set");
    let mut guest = Guest::new(guest_name);
    log::info!("-> load a tiny kernel image");
    guest.load_from_disk();
/* ... 中略 ... */    

おそらくload_from_disk()で大事なのは、sepc()にゲストイメージのエントリポイントを設定している点と、hgatpの設定だ。

HGATPSATPレジスタと似たようなレジスタなので、仮想モードのページから物理ページへと切り替えるためのベースアドレスだ。ここではハイパーバイザーのルートページテーブルが設定されている。

これによりSRETでゲストモードに戻るときにゲストイメージにジャンプする作戦だと思われる。

impl Guest {
    pub fn new(name: &'static str) -> Guest {
        // hgatp
        let root_pt = prepare_gpat_pt().unwrap();
        let hgatp = riscv::csr::hgatp::Setting::new(
            riscv::csr::hgatp::Mode::Sv39x4,
            0,
            root_pt.page.address().to_ppn(),
        );
/* ... 中略 ... */
        
    pub fn load_from_disk(&mut self) {
        let load_size = 1024 * 1024 * 2;
        let buf_page = paging::alloc_continuous(load_size / memlayout::PAGE_SIZE as usize);
/* ... 中略 ... */
            // TODO (enhancement): care about page permissions
            let elf = Elf::from_bytes(buf);
            match elf {
                Ok(Elf::Elf64(e)) => {
                    // change entrypoint
                    self.sepc = e.header().entry_point() as usize;
                    log::info!("-> entrypoint: 0x{:016x}", self.sepc);
                    // copy each section (page to page)
                    for s in e.section_header_iter() {
/* ... 中略 ... */

最後に、switch_to_guest()でジャンプしている。

pub fn switch_to_guest(target: &Guest) -> ! {
    // hgatp: set page table for guest physical address translation
    riscv::csr::hgatp::set(&target.hgatp);
    riscv::instruction::hfence_gvma();
    assert_eq!(target.hgatp.to_usize(), riscv::csr::hgatp::read());

    // hstatus: handle SPV change the virtualization mode to 0 after sret
    riscv::csr::hstatus::set_spv(riscv::csr::VirtualzationMode::Guest);

    // sstatus: handle SPP to 1 to change the privilege level to S-Mode after sret
    riscv::csr::sstatus::set_spp(riscv::csr::CpuMode::S);

    // sepc: set the addr to jump
    riscv::csr::sepc::set(&target.sepc);

    // jump!
    riscv::instruction::sret();
}