作って理解する仮想化技術を読む
2章
DTB/DTS
DTBはバイナリ形式,DTSは人間が読める形式
デバイスの情報がツリー形式でまとまっている
MMIOのアドレスなどデバイスの情報を得るために使用する
DTBの値はビッグエンディアンで保存されているためu32::from_be()を使用する
impl内に定数を宣言する書き方知らなかったので使っていきたい
デバイスの操作
UART (PL011) を使用する
DTBで取得したアドレスはデバイスのレジスタに対応していて,そのレジスタの値でデバイス操作が行われる
送信は遅いためバッファリングが行われるが,一度に送りすぎるとバッファに入らなくなるため,確認しながら送信する処理が必要
3章
例外レベルと仮想化支援機構の設定
ハイパーバイザーはEL2で動く
currentelレジスタの2-3ビットが現在の例外レベル
RWでEL2より下の例外レベルでのCPUの動作モードを設定する
権限の変更
eretで元の例外レベルに戻る
SPSR_EL2で元の権限情報,ELR_EL2で戻り先のアドレスを入れて使用する
インラインアセンブリがnever型を返すことを示す方法
code:rs
pub unsafe fn eret() -> ! {
unsafe { asm!("eret", options(noreturn)) }
}
QEMUモニタで逆アセンブルできない
code:_
(qemu) x/i 0x40402430
0x40402430: Asm output not supported on this arch
./tools/build_qemu.shをもう一回実行してみる
Makefileと./configure無いことでビルドに失敗している
これは後から解決したけど、どこにもメモがない
たしかcurlをパイプで流すと受け取ったデータが破損していた場合に途中で止まってしまう現象があってシェルスクリプトを書き換えた覚えがある
メモリ管理
使用しているメモリ領域の予約を行う
DTB領域
ハイパーバイザ自身の領域
ELFヘッダにIteratorトレイトを実装することでfor文で回せるようにしている
スタック領域
ページサイズでalignしてからスタックサイズ分引いている
ハイパーバイザー特有のメモリ仮想化
ゲストがMMIOを利用しようとしたとき,ハイパーバイザーはゲストがデバイスを使用したかのように動作する
実際のアクセスが行われているわけではないのが重要
ページング
一定の領域でメモリを区切って仮想アドレスと物理アドレスを変換するテーブルを利用してメモリを管理する方法
大きな連続領域を確保する場合に物理アドレスが連続でなくても機能する
仮想化には二段階ページングを使用する
二段階ページングとは,仮想マシンの物理アドレスを中間物理アドレスと呼び,それをさらにハイパーバイザーの物理アドレスに変換する方式
OSが管理するページテーブルを使用した変換をStage 1 Translationと呼び,ハイパーバイザが管理するページテーブルを使用した変換をStage 2 Translationと呼ぶ
メモリの属性の設定
アクセスフラグを常に有効にしておく
これはアクセスフラグが立っていない領域にアクセスすると例外が発生するため
まだ例外ハンドラを持っていないため立てておく
ハードウェアが自動的に1をセットする場合もあるがAArch64ではその機能の拡張がなければソフトウェアが担当する
ページテーブルの設定
from_raw_parts_mut()は第一引数の型の可変スライスを返す
テーブルのアドレスをDescriptorのポインタとして扱い,それをnumber_of_tables * 512だけ確保する
code:rs
struct Descriptor(u64);
for d in unsafe { from_raw_parts_mut(table as *mut Descriptor, number_of_tables * 512) } {
d.init();
}
最上位のテーブルは複数必要な場合もある
再帰的にマッピングする関数
テーブルに入っているディスクリプタの数が2段目以降は512個なので,再帰呼び出しのときからは512に指定されている
メモリの特定の区間からディスクリプタのインデックスを計算して,そこからマッピングするサイズが0になるまでマッピングを行う
4章
例外と割り込み
IRQ (Interrupt Request) はArmにおける非同期割り込みの1つ
書籍では同期割り込みを例外,非同期割り込みを割り込みと呼ぶ
AArch64の命令とレジスタ
stp: 2つのレジスタをメモリに格納する命令.8byteの汎用レジスタ2つで16byteがメモリに書き込まれる
x0レジスタ: CのABIで関数呼び出しの第一引数
x30レジスタ: 関数が戻るアドレスが入るレジスタ.リンクレジスタと呼ばれる
例外ハンドラ
ELR_EL2で例外が発生したアドレス,ESR_EL2で例外の原因に関する情報が格納されている
ESR_EL2で原因によって条件分岐し,個別の処理を行う
個別の処理においてページフォルトの原因の解析ではESR_EL2やHPFAR_EL2の情報を使用する
HPFAR_EL2
5章 割り込み
割り込みコントローラー
どのデバイスから発生したか,どのCPUコアに割り振るか,優先順位などを設定する
AArch64向けのGICv3を使用する
構成要素
Distributor
デバイスの割り込みを各CPUコアに振り分ける
Redistributor
CPUコアごとの割り込みを制御する
Interrupt Translation Services
Distributorの設定
AffinityはCPUの住所みたいなもので、上位の番号はどのグループに属するかで、Aff0はグループ内の個別の番号
これを利用して特定のCPUコアを指定できるようになる
CPUによってはクラスタでキャッシュメモリを共有していたり、配置によって別のクラスタに属するコアのRAMにアクセスする場合に時間がかかるというアーキテクチャがあったりする
後者はNUMAと呼ばれる
Affinity Routing: 割り込みをどのCPUコアに発生させるかをレジスタの値を使って設定する
Group1: Non Secure Worldで受け取る
Non Secure Worldとは普通の命令実行環境で,Secure Worldは命令実行環境を隔離するシステム
AffinityをもとにGICD_IROUTER<n>を設定する
割り込みのトリガモード
Edge Trigger: 電圧の変化した瞬間だけ割込みを起こす方式
Level Trigger: 電圧のある間は割り込みを起こし続ける方式
Redistributorの設定
CPUコアごとのデバイスの割り込みを制御する
優先度マスク: 現在のCPUコアがどのくらいの優先度で受け付けるか決める
バイナリポイント: 割り込みの優先度から割り込みを起こすグループを決定する
割り込みの有効化
デバイスの設定
PL011の割り込みの有効化
DTBから割り込み番号,トリガの種類を取得する
Distributorで割り込み番号でグループへの追加,優先度,割り込みの有効化を行う
最後にデバイスの割り込みを有効化する
irq_handlerでUARTの割り込みに対して何もしないと無限に呼ばれる現象があるけど、なぜだろう?
割り込みに対処したということを伝えるような処理を行う必要があるため
具体的にはこの値の読み出しによって解除される
code:rs
ptr::read_volatile((self.base_address + UART_DR) as *const u8)
デッドロックしない場合
code:rs
if interrupt_number == pl011_int_id {
let c = PL011_DEVICE.lock().getc().unwrap().unwrap();
crate::println!("PL011: {}", c as char);
}
デッドロックする場合
code:rs
if interrupt_number == pl011_int_id {
crate::println!("PL011: {}", PL011_DEVICE.lock().getc().unwrap().unwrap() as char);
}
Claude.icon Rustの一時変数のライフタイムルール(temporary lifetime)により、式の中に埋め込まれた MutexGuard はその文全体の評価が終わるまでドロップされません
本質的にはprintln自体が同一デバイスを使用していることにある
なぜ同一デバイスを指しているそれぞれ別の静的変数に対して、同じ式の中でlockしようとするとデッドロックするのか
どちらもメモリの上では同一のMutex<Serial>を指していて片方がロックを取ると別名の変数からアクセスしようとしたときに、ロックがずっとかかっているという状態になるため
code:rs
pub fn init_default_serial_port(device: &'static Mutex<dyn SerialDevice + Send>) {
*SERIAL_DEVICE.lock() = Serial::new(device);
}
6章 Virtio
本ではLegacy Interfaceが解説されている
VirtQueueは3つのキューで構成されている
Descriptor Table
Descriptor Table
ドライバとデバイスでデータのやり取りを行うために使う
各要素はアドレス,長さ,フラグ,次のディスクリプタへのインデックスを持つ構造体になっている
フラグでVIRTQ_DESC_F_WRITEがセットされている場合,アドレスの指す先はデバイス書き込み専用のアドレスとなる
デバイスとドライバが相互でやり取りする場合,1つではドライバの方が書き込みできなくなるためVIRTQ_DESC_F_NEXTを設定して複数のディスクリプタを使用する
Available Ring
デバイスが書き込めるディスクリプタを伝えるリングバッファ
バッファの中には書き込み可能なDescriptorのインデックスが入っている
ドライバがリングバッファのどこまで書いたかインデックスを進める
Used Ring
デバイスが使用したディスクリプタをドライバに伝えるリングバッファ
ファイルシステム(FAT32)の実装
パーティションテーブル
ブロックデバイスは複数のファイルシステムを持つことが出来るので、その分割の管理をするテーブル
ブロックデバイスのMBR(先頭の512byte)にその情報が載っている
LBA (Logical Block Addressing)
base_lbaはパーティションの情報で、0から始まるわけではなくて他のファイルシステムが使用していたら途中から始まる
code:rs
const BYTES_PER_SECTOR_OFFSET: usize = 0xb;
// alignされていないとpanicする
let bytes_per_sector = unsafe { *((bpb_address + BYTES_PER_SECTOR_OFFSET) as *const u16) };
// panicしない
let bytes_per_sector = u16::from_le_bytes([
]);
あとはQEMUの起動時のオプションを変えていないせいでしばらくFAT32のディスクを読み込んでいなくて詰まった
8.3形式はファイル名8byteで拡張子3byte
最初に作ったバイナリが8byteより長いファイル名だったので名称を変えたがエントリとして残っている
どういう仕組み?
起動時に特定のディレクトリの内容をdiskにコピーしていたから、そこから取り除くと無くなった
code:_
7章 仮想マシンの実装
VM構造体を作る
仮想マシンごとに条件分岐をしていると管理が複雑になるため,それぞれに構造体を作る必要がある
VMのID,使用するメモリのベースアドレスや大きさ,MMIOハンドラをフィールドに持つ
この構造体はOSにおけるプロセス構造体に似ている役割を持っている
Linuxのビルド
Linux Kernel単体ではOSをユーザーが操作する方法が提供されない
最小限の対話的な操作が可能な状態で起動するにはLinux KernelとBusyBoxを使用する方法がある
デバイスツリーを仮想マシンに提供する
CPU,メモリ,PL011の情報を書く
Linuxの起動直前まで進んでいて、タイマー割り込みが有効でないためpanicしている状態
8章 Linuxが動作するようにする
前の章の最後ではLinuxの起動時のメッセージが途中まで出て、タイマーの割り込みが有効化できないということでpanicする状態だった
仮想割り込みコントローラをゲストに提供して、ホストで起こった割り込みをゲストに見せる
仕組み
割り込みが起こるとシステムレジスタに設定した例外ベクタに飛ぶ
HCR_EL2のVIというフラグ(Virtual Interrupt)を設定すると、VBAR_EL1に設定されている例外ベクタ(つまりゲストの設定したところ)に飛ぶ
それでも割り込み番号の取得などをゲストが行うことができないので、ソフトウェアから設定する必要があり、ここを実装する
GICv3にはVGICという仮想マシンへのデバイス割り込みを支援してくれる機能があるのでこれを使用する
maintenance_interruptとは
Generic Timer
CNTVOFF_EL2レジスタは仮想タイマーと物理タイマーの時間の差を設定するためにある
仮想タイマーはCNTVOFF_EL2の値を物理タイマーから引いた値を使用している
なぜならハイパーバイザーによって仮想マシンが停止している間も時間は経っているが、仮想マシンは再開した後に停止していたことを知らないため時間の差を作る必要がある
今回は制御がハイパーバイザーに移っている時間が短いためCNTVOFF_EL2を0にして物理タイマーの時間をそのまま使う
interrupt-parent = <0x8005>;をDTSに書くのを忘れていてタイマーの初期化に失敗していた
ChatGPT.icon 原因はゲストDTBの /timer ノードではなく、その interrupts を解決する親GICが指定されていないことです。Linux は /timer を見つけていますが、interrupt-parent が root にも timer にも無いため、interrupts = <...> を GICv3 の PPI として解釈できず、結果としてarch_timer: No interrupt available, giving up Failed to initialize '/timer': -22と出力される