VirtIO Block デバイスの MMIO レジスタ実装(Phase 2 Week 2)
Apple Silicon ハイパーバイザープロジェクト Phase 2 Week 2。VirtIO 1.2 仕様に基づいた Block デバイスの MMIO レジスタを実装し、ゲスト OS がデバイスを検出できるようにしました。
はじめに
Apple Silicon ハイパーバイザープロジェクト Phase 2 Week 2 では、VirtIO Block デバイスの MMIO レジスタを実装しました。
Phase 2 Week 2 の目標
Phase 2 Week 1 で VirtQueue(Split Virtqueues)を実装した後、Week 2 では以下を目標としました。
- VirtIO MMIO レジスタマップを理解する
- VirtioBlockDevice 構造体を実装する
- MmioHandler trait を実装する
- ゲスト OS がデバイスを検出できるようにする
VirtIO MMIO レジスタとは
VirtIO デバイスは、ゲスト OS とハイパーバイザー間でデバイスの状態やキューの情報をやり取りするために MMIO(Memory-Mapped I/O)レジスタを使用します。
主なレジスタは以下の通りです。
- MAGIC_VALUE (0×00): デバイスが VirtIO であることを示すマジック値(
0x74726976= “virt”) - VERSION (0×04): VirtIO のバージョン(
0x2= VirtIO 1.2) - DEVICE_ID (0×08): デバイスタイプ(
0x2= Block device) - VENDOR_ID (0×0c): ベンダー ID(
0x554D4551= “QEMU”) - STATUS (0×70): デバイスステータス(ドライバーとデバイスの初期化フロー)
- QUEUE_SEL (0×30): 操作対象のキューを選択
- QUEUE_NUM_MAX (0×34): キューの最大サイズ
VirtIO MMIO レジスタマップ
VirtIO 1.2 仕様と QEMU の hw/virtio/virtio-mmio.c を参考に、以下のレジスタマップを定義しました。
mod regs {
pub const MAGIC_VALUE: u64 = 0x00;
pub const VERSION: u64 = 0x04;
pub const DEVICE_ID: u64 = 0x08;
pub const VENDOR_ID: u64 = 0x0c;
pub const DEVICE_FEATURES: u64 = 0x10;
pub const DEVICE_FEATURES_SEL: u64 = 0x14;
pub const DRIVER_FEATURES: u64 = 0x20;
pub const DRIVER_FEATURES_SEL: u64 = 0x24;
pub const QUEUE_SEL: u64 = 0x30;
pub const QUEUE_NUM_MAX: u64 = 0x34;
pub const QUEUE_NUM: u64 = 0x38;
pub const QUEUE_READY: u64 = 0x44;
pub const QUEUE_NOTIFY: u64 = 0x50;
pub const INTERRUPT_STATUS: u64 = 0x60;
pub const INTERRUPT_ACK: u64 = 0x64;
pub const STATUS: u64 = 0x70;
pub const QUEUE_DESC_LOW: u64 = 0x80;
pub const QUEUE_DESC_HIGH: u64 = 0x84;
pub const QUEUE_DRIVER_LOW: u64 = 0x90;
pub const QUEUE_DRIVER_HIGH: u64 = 0x94;
pub const QUEUE_DEVICE_LOW: u64 = 0xa0;
pub const QUEUE_DEVICE_HIGH: u64 = 0xa4;
pub const CONFIG_GENERATION: u64 = 0xfc;
}
レジスタマップの全体サイズは 0×200(512 bytes)です。
VirtioBlockDevice の実装
構造体の設計
pub struct VirtioBlockDevice {
/// ベースアドレス
base_addr: u64,
/// VirtQueue(キューサイズ 16)
queue: VirtQueue,
/// デバイスステータス
status: u32,
/// 選択中のキューインデックス
queue_sel: u32,
/// デバイス Features セレクタ
device_features_sel: u32,
/// ドライバー Features セレクタ
driver_features_sel: u32,
}
- base_addr: MMIO レジスタのベースアドレス(例:
0x0a00_0000) - queue: Week 1 で実装した VirtQueue(サイズ 16)
- status: ドライバーがデバイス初期化時に設定するステータス
- queue_sel: 複数のキューがある場合に選択するインデックス
- device_features_sel / driver_features_sel: Features の選択(将来実装)
MmioHandler trait の実装
Phase 1 で実装した MmioHandler trait を使用して、MMIO レジスタの read/write を処理します。
impl MmioHandler for VirtioBlockDevice {
fn base(&self) -> u64 {
self.base_addr
}
fn size(&self) -> u64 {
0x200 // VirtIO MMIO レジスタ領域のサイズ
}
fn read(&mut self, offset: u64, _size: usize) -> Result<u64, Box<dyn Error>> {
let value = match offset {
regs::MAGIC_VALUE => VIRT_MAGIC as u64,
regs::VERSION => VIRT_VERSION as u64,
regs::DEVICE_ID => VIRTIO_ID_BLOCK as u64,
regs::VENDOR_ID => VIRT_VENDOR as u64,
regs::QUEUE_NUM_MAX => self.queue.size() as u64,
regs::STATUS => self.status as u64,
regs::DEVICE_FEATURES => {
// 最小限の実装: Features なし
0
}
regs::INTERRUPT_STATUS => {
// 割り込みは未実装
0
}
_ => {
// 未実装のレジスタは 0 を返す
0
}
};
Ok(value)
}
fn write(&mut self, offset: u64, value: u64, _size: usize) -> Result<(), Box<dyn Error>> {
match offset {
regs::STATUS => {
self.status = value as u32;
}
regs::QUEUE_SEL => {
self.queue_sel = value as u32;
}
regs::QUEUE_NOTIFY => {
// キュー通知(将来実装)
}
regs::DEVICE_FEATURES_SEL => {
self.device_features_sel = value as u32;
}
regs::DRIVER_FEATURES_SEL => {
self.driver_features_sel = value as u32;
}
regs::INTERRUPT_ACK => {
// 割り込み ACK(将来実装)
}
_ => {
// 未実装のレジスタへの書き込みは無視
}
}
Ok(())
}
}
ポイント:
- 未実装のレジスタは
0を返すか、書き込みを無視 - 最小限の実装でゲスト OS がデバイスを検出できる
- 将来の拡張を考慮した設計
テスト結果
ユニットテスト
VirtioBlockDevice の基本機能と主要なレジスタの read/write をテストしました。
#[test]
fn test_virtio_block_new() {
let device = VirtioBlockDevice::new(0x0a00_0000);
assert_eq!(device.base(), 0x0a00_0000);
assert_eq!(device.size(), 0x200);
}
#[test]
fn test_read_magic_value() {
let mut device = VirtioBlockDevice::new(0x0a00_0000);
let magic = device.read(regs::MAGIC_VALUE, 4).unwrap();
assert_eq!(magic, VIRT_MAGIC as u64);
}
#[test]
fn test_read_version() {
let mut device = VirtioBlockDevice::new(0x0a00_0000);
let version = device.read(regs::VERSION, 4).unwrap();
assert_eq!(version, VIRT_VERSION as u64);
}
#[test]
fn test_read_device_id() {
let mut device = VirtioBlockDevice::new(0x0a00_0000);
let device_id = device.read(regs::DEVICE_ID, 4).unwrap();
assert_eq!(device_id, VIRTIO_ID_BLOCK as u64);
}
#[test]
fn test_read_vendor_id() {
let mut device = VirtioBlockDevice::new(0x0a00_0000);
let vendor_id = device.read(regs::VENDOR_ID, 4).unwrap();
assert_eq!(vendor_id, VIRT_VENDOR as u64);
}
#[test]
fn test_read_queue_num_max() {
let mut device = VirtioBlockDevice::new(0x0a00_0000);
let queue_num_max = device.read(regs::QUEUE_NUM_MAX, 4).unwrap();
assert_eq!(queue_num_max, 16);
}
#[test]
fn test_write_status() {
let mut device = VirtioBlockDevice::new(0x0a00_0000);
device.write(regs::STATUS, 0x0f, 4).unwrap();
assert_eq!(device.status, 0x0f);
let status = device.read(regs::STATUS, 4).unwrap();
assert_eq!(status, 0x0f);
}
#[test]
fn test_write_queue_sel() {
let mut device = VirtioBlockDevice::new(0x0a00_0000);
device.write(regs::QUEUE_SEL, 0, 4).unwrap();
assert_eq!(device.queue_sel, 0);
}
テスト実行結果
$ cargo test
running 32 tests
test devices::uart::tests::test_pl011_uart_new ... ok
test devices::uart::tests::test_uart_read_fr ... ok
test devices::uart::tests::test_uart_read_dr ... ok
test devices::uart::tests::test_uart_write_data ... ok
test devices::uart::tests::test_uart_write_dr ... ok
test devices::virtio::block::tests::test_virtio_block_new ... ok
test devices::virtio::block::tests::test_read_magic_value ... ok
test devices::virtio::block::tests::test_read_version ... ok
test devices::virtio::block::tests::test_read_device_id ... ok
test devices::virtio::block::tests::test_read_vendor_id ... ok
test devices::virtio::block::tests::test_read_queue_num_max ... ok
test devices::virtio::block::tests::test_write_status ... ok
test devices::virtio::block::tests::test_write_queue_sel ... ok
test devices::virtio::queue::tests::test_virtqueue_new ... ok
test devices::virtio::queue::tests::test_virtqueue_new_invalid_size ... ok
test devices::virtio::queue::tests::test_descriptor_flags ... ok
test devices::virtio::queue::tests::test_pop_avail_empty ... ok
test devices::virtio::queue::tests::test_push_and_pop_avail ... ok
test devices::virtio::queue::tests::test_push_used ... ok
test devices::virtio::queue::tests::test_get_set_desc ... ok
test devices::virtio::queue::tests::test_set_desc_invalid_index ... ok
test devices::virtio::queue::tests::test_avail_ring_wrapping ... ok
test result: ok. 32 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
全テスト通過しました。
clippy 実行結果
$ cargo clippy -- -D warnings
Checking hypervisor v0.1.0 (/Users/susumu/product/Building-a-hypervisor)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.35s
警告なしで通過しました。
技術的発見
最小限の実装でデバイス検出が可能
VirtIO Block デバイスは 20 個以上のレジスタを持ちますが、ゲスト OS がデバイスを検出するために最低限必要なのは以下の 6 つだけです。
- MAGIC_VALUE (0×00):
0x74726976(“virt”) - VERSION (0×04):
0x2(VirtIO 1.2) - DEVICE_ID (0×08):
0x2(Block device) - VENDOR_ID (0×0c):
0x554D4551(“QEMU”) - STATUS (0×70): デバイスステータス
- QUEUE_NUM_MAX (0×34): キューサイズ
他のレジスタ(DEVICE_FEATURES、INTERRUPT_STATUS など)は、現時点では未実装でも問題ありません。
将来の拡張を考慮した設計
未実装のレジスタやフィールドには #[allow(dead_code)] を付けることで、clippy の警告を抑制しながら将来の実装に備えています。
#[allow(dead_code)]
mod regs {
pub const MAGIC_VALUE: u64 = 0x00;
pub const VERSION: u64 = 0x04;
// ... 将来使用するレジスタ定義
}
QEMU の実装を参考にした設計
QEMU の hw/virtio/virtio-mmio.c を参考に、レジスタマップとデフォルト値を決定しました。これにより、実際の VirtIO ドライバーとの互換性を確保しやすくなりました。
次のステップ
Phase 2 Week 2 が完了し、VirtIO Block デバイスの MMIO レジスタ実装ができました。
Week 3 の計画
次週(Week 3)では、以下を実装する予定です。
- ディスクイメージの読み書き
VirtioBlkReq構造体(type, sector, data, status)process_queue()メソッドで VirtQueue を処理read_sectors()/write_sectors()メソッド- テスト用ディスクイメージの作成
最終ゴール
Phase 2 の最終ゴールは、Linux カーネルが VirtIO Block デバイスを認識し、ディスクイメージをマウントできるようにすることです。
[ 1.234567] virtio_blk virtio0: [vda] 131072 512-byte logical blocks (67.1 MB/64.0 MiB)
[ 1.456789] VFS: Mounted root (ext4 filesystem) on device 254:0.
まとめ
Phase 2 Week 2 では、VirtIO Block デバイスの MMIO レジスタを実装しました。
実装内容:
- VirtioBlockDevice 構造体(MMIO ハンドラー)
- レジスタ read/write 実装(MagicValue, Version, DeviceID, Status 等)
- ユニットテスト 10 件(全パス)
- clippy 警告なし
技術的発見:
- 最小限の実装でゲスト OS がデバイスを検出できる
- 将来の拡張を考慮した設計(
#[allow(dead_code)]) - QEMU の実装を参考にした互換性の確保
次週は、ディスク読み書きの実装に進みます。