RISC-Vのハイパーバイザー実装、いろいろ既存の実装を眺めているが、以下の実装がかなり読みやすく実装されているように感じたので中身を眺めてみることにした。
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の一番最後の位置をページの先頭としている?
hedeleg
とhideleg
の設定によりすべての仮想化モードの例外と仮想化割り込み信号を仮想マシンに転送するように設定している。
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
の設定だ。
HGATP
はSATP
レジスタと似たようなレジスタなので、仮想モードのページから物理ページへと切り替えるためのベースアドレスだ。ここではハイパーバイザーのルートページテーブルが設定されている。
これにより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(); }