最近前々から興味のあったOS開発をしています
せっかく自作するんだから、ということでBootloaderも含めてフルスクラッチから作成しており、Pure Rustで書かれたaarch64向けオペレーティングシステムです
bootloaderから自作しよう!となるとネット上の情報も少なく、またPure RustでのOS開発というのも依然ニッチなジャンルです
さらにそこにaarch64向けというフィルタも加えると更に有益な情報は少なくなります
体感としてもこれは実感していて、ドキュメントが少なくあったとしても結局 クソ長い退屈な 1次情報である仕様書/規格書に辿り着く、
やっと分かりやすそうな情報にたどり着いたと思ったらx86_64向けの内容だったという事がよくあります
この記事では、そんなニッチなジャンルにも同胞がいる事を信じて、実際に自分が直面した問題の中で一番厄介だった事例を紹介、解決策の解説をしてみます
有識者の方はバンバンまさかり投げてください投げないと呪うお待ちしています!
この記事が対象としている人:
- UEFI環境で動くブートローダを書きたい人
- RustでOS開発をしたい人
- aarch64向けのOS開発をしたい人
- OS開発にQEMUを使ってる人
開発環境
本題に入る前に開発環境について
私物のPCがmacなのでmacOS(Sequoia 15.5)で開発しています
Rustはrustupで管理しているnightly版、QEMUは9.2.3(記事を書いている時点)でnixで管理しています
またビルドやQEMUの起動は後述する様にRustで起動スクリプトを書いています
Display output is not active
ある日、最小構成のbootloaderが完成しkernelを呼び出そうとしたところ、QEMUウィンドウにDisplay output is not activeと表示されました
この表示を見たのは初めてではなく、仮想環境バックエンドにQEMUを使用している[UTMという仮想環境アプリで新しくVMを作成した時などにちょくちょく見かけていました
UTMの場合だと処理に時間がかかっている場合が殆どで暫く待っていると操作できるようになります
なので今回のケースでも待ってれば動き始めるだろ~、と暫くよそ事をしつつQEMUプロンプトでinfo registers
などしながら様子を見ていました
ですがいつまで経っても処理が進まない、というかinfo registers
で確認する限りkernelのエントリポイントとは全く関係ないアドレスをループしているように見えます
これはデバッグの際に気づいたのですが、特に奇妙だったのはx86_64向けにビルドするとカーネルのエントリポイントに正常にジャンプしている点でした
UEFIを直に触った事のある人は共感できるかもしれませんが、bootloaderの時点でCPUアーキテクチャの違いを意識することってほぼほぼ無いんですよね
特に自分のように必要最低限の事をするだけなのであればUEFIが提供する抽象化されたインターフェースとやりとりするだけで済みます
なのになぜかアーキテクチャ固有のバグが発生している..
(ネタバレをすると、アーキテクチャ固有の問題だった!、というわけでも無いんですよねこれが)
この時自分がまず疑ったのは
- bootloaderのkernelファイルの解析(ELF Parser)処理に間違いがある?
- bootloaderがカーネルのデータを正しいアドレスに配置していない?
- bootloaderでexit_boot_servicesが正常に終了しなかった?
- bootloaderからエントリポイントを呼び出すRust構文が間違っている?
- kernel側で何か例外が発生した?
の5点でした
因みにこの時点でx86_64向けビルドを試していれば最低でも1番は原因から除外することができます
1番が原因なのであればx86_64向けビルドでも失敗するはずですからね
なぜ試さなかったのか😭
2, 3番も除外できそうですがファームウェアの実装がおかしい可能性が微粒子レベルで存在します
プロジェクト構成
osoプロジェクト(リポジトリの名前です こだわりポイントは顔文字に見える所です0w0)は、いくつかの主要コンポーネントを持つモジュラーアーキテクチャ?になっています
osoレポジトリ自体はCargo Workspaceになっています
Cargo Workspaceにするメリットは地味ですが、ビルドアーティファクトがワークスペース直下のtargetディレクトリに作られる点です
xtaskを書くときにちょっと楽になります
oso/
├── oso_loader/ # UEFIブートローダー
├── oso_kernel/ # kernel実装
├── oso_bridge/ # 共有コードとインターフェース
├── oso_proc_macro/ # proc macroの定義
├── oso_proc_macro_logic/ # proc macroの主要処理とテスト
├── xtask/ # ビルドスクリプトとユーティリティ
├── target/ # ビルド成果物
└── Cargo.toml # Cargo workspaceの定義
osoでは自分の技術的好奇心を満たすためproc macroを多用しています
低レイヤー開発の一環としてWebスクレイピングとかできるのでとても精神に良いです
多用するとproc macro関連のコードが増え、テストしたい部分も増えるのでそういった部分はoso_proc_macro_logicに抽出しています
またxtaskクレートでは各クレートのビルド、bootloader, kernelのマウント、qemuの起動をする処理を書いています
xtaskクレートはuefi-rsのxtaskクレートを参考にしました
bootloaderの実装
ブートローダーの主要フローです
#[unsafe(export_name = "efi_main")]
pub extern "efiapi" fn efi_image_entry_point(
image_handle: UnsafeHandle,
system_table: *const SystemTable,
) -> Status {
// Rustラッパー機能 & ユーティリティのセットアップ
init(image_handle, system_table,);
// TODO: proper error handling
let (kernel_entry, graphic_config,) = app().expect("error arise while executing application",);
// uefiブートサービスを終了
exit_boot_services();
// カーネルエントリポイントにジャンプ
exec_kernel(kernel_entry, graphic_config,);
// ここには到達しないはず?
Status::EFI_SUCCESS
}
fn app() -> Rslt<(u64, FrameBufConf,),> {
// カーネルをメモリ上に展開 カーネルのエントリーポイント(not 先頭アドレス)を返す
let kernel_addr = kernel()?;
// NOTE: gpu driverを実装するのでもう使わない
let graphic_config = graphic_config()?;
Ok((kernel_addr, graphic_config,),)
}
カーネルエントリーポイント
デバッグ様に簡略化されたカーネルエントリーポイントはoso_kernel/src/main.rs
で以下のように定義されています
#[unsafe(no_mangle)]
#[cfg(target_arch = "aarch64")]
pub extern "C" fn kernel_main(fbc: FrameBufConf,) {
// NOTE: Disable IRQ(interrupt request)
unsafe {
asm!("msr daifset, #2");
}
// NOTE: stops program for debugging purpose
wfi();
}
調査
なぜカーネルが呼び出されないのか、正直見当もつかなかったので愚直に可能性を潰して回りました
1. ELFの解析
まず、kernelのELFファイルが正しく解析されていることを確認する必要があります
ELF Header
readelf
を使用するとelfファイルの情報を確認することができます-h
オプションをつけてelf headerの情報のみ閲覧することができます
❯ readelf -h target/oso_kernel.elf
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: AArch64
Version: 0x1
Entry point address: 0x40010120
Start of program headers: 64 (bytes into file)
Start of section headers: 5072 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 4
Size of section headers: 64 (bytes)
Number of section headers: 12
Section header string table index: 10
このコマンドの出力と自作parserの返り値を比較すればelf headerの解析に誤りが無いか確認出来そうです
やり方は色々あると思いますが、自分は以下のようにproc macroを利用してreadelf -h
の解析結果をheader
と比較することで確認しています
実装の詳細は無駄に長いので興味のある方は実際にリポジトリを確認していただきたい(あわよくばおかしな処理をしていたら指摘してほしい)のですが、要点だけ説明するとtest_elf_header_parse!
マクロ内で、parserの結果とreadelf
の結果をassert_eq!
しています
Program Header
プログラムヘッダーも調査しました
❯ readelf -l target/oso_kernel.elf
Elf file typje is EXEC (Executable file)
Entry point 0jx40010120
There are 4 prjogram headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000040000040 0x0000000040000040
0x00000000000000e0 0x00000000000000e0 R 0x8
LOAD 0x0000000000000000 0x0000000040000000 0x0000000040000000
0x0000000000000120 0x0000000000000120 R 0x10000
LOAD 0x0000000000000120 0x0000000040010120 0x0000000040010120
0x0000000000000010 0x0000000000000010 R E 0x10000
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x0
Section to Segment mapping:
Segment Sections...
00
01
02 .text
03
これも同様にproc macroを使ってassertionしています
elf headerの方と比べてreadelf
の出力が少し複雑ですが、proc macro内では当然標準ライブラリが使えるので、大仰な処理を自分で書く必要もありませんarray_chunks
超便利
どうやらProgram Headerの解析も正しくできてそうです
ということは原因は他にありそうです
2. カーネルデータの配置
ELF Parserの解析が期待通り行われたという事はそれ以降の処理におかしな部分があるんだろうと考え、カーネルデータを読み込み指定されたアドレスに配置する処理を次に疑いました
ではエントリーポイントの実際のコードを確認してみましょう.text
セクションをdumpします
❯ readelf --hex-dump=.text target/oso_kernel.elf
Hex dump of section '.text':
0x40010120 df4203d5 01000014 7f2003d5 ffffff17 .B....... ......
次にコードを走らせQEMUプロンプトで以下のコマンドを実行、出力を確認します
(qemu) x /10i 0x40010120
0x40010120: d50342df msr daifset, #2
0x40010124: 14000001 b #0x40010128
0x40010128: d503207f wfi
0x4001012c: 17ffffff b #0x40010128
0x40010130: 00000000 udf #0
0x40010134: 00000000 udf #0
0x40010138: 00000000 udf #0
0x4001013c: 00000000 udf #0
0x40010140: 00000000 udf #0
0x40010144: 00000000 udf #0
出力を見る限りだと、期待通りのアドレスに期待通りのセクションが展開されていますね
3. UEFIブートサービスの終了処理
exit_boot_services
の実装は以下の様になっています
impl BootServices {
pub fn exit_boot_services(&self,) {
let mem_ty = MemoryType::BOOT_SERVICES_DATA;
let mut buf = MemoryMapBackingMemory::new(mem_ty,).expect("failed to allocate memory",);
let status = unsafe { self.try_exit_boot_services(buf.as_mut_slice(),) };
if !status.is_success() {
todo!("failed to exit boot service. reset the machine");
}
}
unsafe fn try_exit_boot_services(&self, buf: &mut [u8],) -> Status {
let mem_map = self.get_memory_map(buf,).expect("failed to get memmap",);
let status =
unsafe { (self.exit_boot_services)(image_handle().as_ptr(), mem_map.map_key,) };
core::mem::forget(mem_map,);
status
}
}
この辺の実装はuefi-rsの実装をパクッて大いに参考にしています
処理の流れは
- 最新のメモリマップを取得
try_exit_boot_services
を実行しブートサービスの終了を試みる- 終了処理が正常に行われなかった場合パニック!(永久ループ)
の様になっています
ここで一つ、Bootloaderを書いたことがある人にとって違和感があるかもしれない点を説明します
世にあるUEFI Bootloaderの実装を見ると多くの場合ブートサービスを終了させる処理の部分をループにしているかと思います
これは「exit_boot_services
がその時点での最新のmap_keyを必要とする為、一度終了処理を試してそれが失敗したら最新のmap_keyを取得して再度exit_boot_services
を試みる」という処理をしている為です
んでここからは自己流なのですが、最新のmap_keyが必要なら初めからそれを取得していればループする必要無くない?、ということで上記のコードではループせずに単にmap_key取得→ブートサービス終了の流れで処理しています
exit_boot_services
のデバッグ方法ですが、oso_loaderでのprintln!
(及びprint!
)マクロはブートサービスを利用しています
panic_handlerも内部でprintln!
マクロを使っているので間接的にブートサービスに依存しています
なのでpanic_handlerはブートサービス終了後は使えません
ではどうやってデバッグするのかというと
#[inline(always)]
pub fn wfi() -> ! {
loop {
unsafe {
#[cfg(target_arch = "aarch64")]
asm!("wfi");
#[cfg(target_arch = "x86_64")]
asm!("hlt");
}
}
}
#[inline(always)]
pub fn wfe() -> ! {
loop {
unsafe {
#[cfg(target_arch = "aarch64")]
asm!("wfe");
#[cfg(target_arch = "x86_64")]
asm!("hlt");
}
}
}
この様にインラインアセンブリを書いてプログラムを強制停止できる様にします
wfiニーモニックは割り込みが発生するまでCPUをおすわりさせます
wfeニーモニックはイベントがあるまでCPUをおすわりさせます
とだけ書くと結局wfiとwfiって何が違うのん?イベントって何?となるので簡単に説明すると
- wfe一旦CPUをスリープさせ、ハードウェアから何かしらの信号を受け取るとCPUをスリープから復帰させます
それ以上のことはしません - wfiは一旦CPUをスリープさせ、割り込みが発生したらCPUをスリープから復帰して割り込みの処理も行います
自分も詳しいことよく分かってないので有識者の方いたら教えて欲しいです
wfi
とwfe
関数はCPUアーキテクチャ以外の環境に拠らないのでoso_bridge
で定義してカーネルコードでも使えるようにしています
強制停止させるだけならインラインアセンブリは必要ないですが、デバッグ時に目で見てわかるラベルとしてインラインアセンブリを書いています
個人的な使い分けですが、デバッグにはwfi
、パニックにはwfe
を使う様にしています
こういうのどう呼べば良いのかよくわかりませんがarmのドキュメントに則って”ヒント命令”と呼ぶことにしましょう
このヒント命令をexit_boot_services
を呼び出した直後にセットして実行してみたのですが、どうやらexit_boot_services
は正常に実行された様子
#[unsafe(export_name = "efi_main")]
pub extern "efiapi" fn efi_image_entry_point(
image_handle: UnsafeHandle,
system_table: *const SystemTable,
) -> Status {
init(image_handle, system_table,);
let (kernel_entry, graphic_config,) = app().expect("error arise while executing application",);
exit_boot_services();
// ココ!————————————————————————————————————————————————---
wfi();
exec_kernel(kernel_entry, graphic_config,);
Status::EFI_SUCCESS
}
(qemu) info registers
CPU#0
PC=0000000046134170 X00=0000000000000001 X01=0000000046173ca0
X02=0000000000000000 X03=000000004795a300 X04=000000004795a350
X05=0000000000000004 X06=000000004793e70c X07=0000000000000400
X08=0000000000000000 X09=0000000000000000 X10=0000000000000003
X11=0000000000000000 X12=0000000000000001 X13=000000004793e6bf
X14=0000000000000000 X15=0000000000000000 X16=00000000470f4f0c
X17=0000000000000003 X18=0000000000000000 X19=0000000000000000
X20=0000000000000000 X21=0000000046d21000 X22=0000000046d21730
X23=0000000046d1fb6a X24=0000000046bc8518 X25=000000004617b018
X26=0000000046d21000 X27=0000000003051007 X28=000000000000ffff
X29=000000004793ea90 X30=0000000046134168 SP=000000004793e8b0
PSTATE=600003c5 -ZC- EL1h FPCR=00000000 FPSR=00000000
Q00=0000000000000001:0000000000000322 Q01=ffffff80ffffffc8:000000004793e8e0
Q02=0000000000000000:0000000000000000 Q03=0000000000000000:0000000000000000
Q04=0000000000000000:0000000000000000 Q05=0000000000000000:0000000000000000
Q06=0000000000000000:0000000000000000 Q07=0000000000000000:0000000000000000
Q08=0000000000000000:0000000000000000 Q09=0000000000000000:0000000000000000
Q10=0000000000000000:0000000000000000 Q11=0000000000000000:0000000000000000
Q12=0000000000000000:0000000000000000 Q13=0000000000000000:0000000000000000
Q14=0000000000000000:0000000000000000 Q15=0000000000000000:0000000000000000
Q16=0000000000000000:0000000000000000 Q17=0000000000000000:0000000000000000
Q18=0000000000000000:0000000000000000 Q19=0000000000000000:0000000000000000
Q20=0000000000000000:0000000000000000 Q21=0000000000000000:0000000000000000
Q22=0000000000000000:0000000000000000 Q23=0000000000000000:0000000000000000
Q24=0000000000000000:0000000000000000 Q25=0000000000000000:0000000000000000
Q26=0000000000000000:0000000000000000 Q27=0000000000000000:0000000000000000
Q28=0000000000000000:0000000000000000 Q29=0000000000000000:0000000000000000
Q30=0000000000000000:0000000000000000 Q31=0000000000000000:0000000000000000
(qemu) x /10i 0x46134160
0x46134160: 9400022e bl #0x46134a18
0x46134164: 94000262 bl #0x46134aec
0x46134168: 14000001 b #0x4613416c
0x4613416c: d503207f wfi
0x46134170: 17ffffff b #0x4613416c
0x46134174: d10683ff sub sp, sp, #0x1a0
0x46134178: a91977fe stp x30, x29, [sp, #0x190]
0x4613417c: f9001be8 str x8, [sp, #0x30]
0x46134180: 910183e8 add x8, sp, #0x60
0x46134184: f9001fe8 str x8, [sp, #0x38]
(qemu) q
ということは問題はまた別の場所にあるようです
4. カーネルへのジャンプの検証
カーネルエントリーポイントへのジャンプは関数ポインタを使用しています
pub fn exec_kernel(kernel_entry: u64, _graphic_config: FrameBufConf,) {
// これ要る?
let kernel_entry = kernel_entry as *const ();
#[cfg(target_arch = "riscv64")]
type KernelEntry = extern "C" fn();
#[cfg(target_arch = "aarch64")]
type KernelEntry = extern "C" fn();
#[cfg(target_arch = "x86_64")]
type KernelEntry = extern "sysv64" fn();
let entry_point = unsafe { core::mem::transmute::<_, KernelEntry,>(kernel_entry,) };
entry_point();
// 失敗したら到達する
wfi();
}
ひとまずentry_point
実行直前までの処理が期待通り行われているかチェックします
wfi();
entry_point();
}
これを実行してQEMUプロンプトで確認してみましょう
(qemu) info registers
CPU#0
PC=000000004613407c X00=0000000000000001 X01=00000000461735a0
X02=0000000000000000 X03=000000004795a300 X04=000000004795a350
X05=0000000000000004 X06=000000004793e80c X07=0000000000000400
X08=0000000000000000 X09=0000000000000000 X10=0000000000000003
X11=0000000000000000 X12=0000000000000001 X13=000000004793e7bf
X14=0000000000000000 X15=0000000000000000 X16=00000000470f4f0c
X17=0000000000000003 X18=0000000000000000 X19=0000000000000000
X20=0000000000000000 X21=0000000046d21000 X22=0000000046d21730
X23=0000000046d1fb6a X24=0000000046bc8518 X25=000000004617b018
X26=0000000046d21000 X27=0000000003051007 X28=000000000000ffff
X29=000000004793ea90 X30=0000000046134074 SP=000000004793e9b0
PSTATE=600003c5 -ZC- EL1h FPCR=00000000 FPSR=00000000
Q00=0000000000000001:0000000000000316 Q01=ffffff80ffffffc8:000000004793e8e0
Q02=0000000000000000:0000000000000000 Q03=0000000000000000:0000000000000000
Q04=0000000000000000:0000000000000000 Q05=0000000000000000:0000000000000000
Q06=0000000000000000:0000000000000000 Q07=0000000000000000:0000000000000000
Q08=0000000000000000:0000000000000000 Q09=0000000000000000:0000000000000000
Q10=0000000000000000:0000000000000000 Q11=0000000000000000:0000000000000000
Q12=0000000000000000:0000000000000000 Q13=0000000000000000:0000000000000000
Q14=0000000000000000:0000000000000000 Q15=0000000000000000:0000000000000000
Q16=0000000000000000:0000000000000000 Q17=0000000000000000:0000000000000000
Q18=0000000000000000:0000000000000000 Q19=0000000000000000:0000000000000000
Q20=0000000000000000:0000000000000000 Q21=0000000000000000:0000000000000000
Q22=0000000000000000:0000000000000000 Q23=0000000000000000:0000000000000000
Q24=0000000000000000:0000000000000000 Q25=0000000000000000:0000000000000000
Q26=0000000000000000:0000000000000000 Q27=0000000000000000:0000000000000000
Q28=0000000000000000:0000000000000000 Q29=0000000000000000:0000000000000000
Q30=0000000000000000:0000000000000000 Q31=0000000000000000:0000000000000000
(qemu) x /10i 0x46134070
0x46134070: 94000215 bl #0x461348c4
0x46134074: 14000001 b #0x46134078
0x46134078: d503207f wfi
0x4613407c: 17ffffff b #0x46134078
0x46134080: d10683ff sub sp, sp, #0x1a0
0x46134084: a91977fe stp x30, x29, [sp, #0x190]
0x46134088: f9001be8 str x8, [sp, #0x30]
0x4613408c: 910183e8 add x8, sp, #0x60
0x46134090: f9001fe8 str x8, [sp, #0x38]
0x46134094: 94000211 bl #0x461348d8
(qemu) q
wfi
しています
と言うことはここまでの処理は正常に実行されているとみなして良さそうです
このwfi
を取り除くと元のバグが発生している状態になります
MMU
さてここまで思い当たる原因を片っ端から調査しましたが、カーネルが実行されない原因は分からずじまいでした
何か見落としはないか何度もデバッグ、テストを繰り返しましたが、カーネルのエントリポイントにジャンプ出来ない事以外何も問題ありませんでした
ここから3週間ほど無意味にコードを眺める・ふて寝するなどしましたが一向に進展しませんでした
仕様書は隅々まで読もう!(無理)
行き詰まっている間もGPT君に聞いたりGoogle先生に聞いたりしていました
調べていくうちにMMUという機能(厳密にはハードウェアの事)があると知りました
MMUは仮想メモリと物理メモリを対応付ける機能を提供します
つまり仮想メモリ機能はMMUによって実現されています
どうやらブートサービスからカーネルを呼び出す際、CPUが使うキャッシュやMMUが原因でエラーが発生することがある様です
具体的なことは調べても出てこなかったので、ひとまずUEFI仕様書でMMUの記述がないか探しました
ありました
- The MMU is enabled and any RAM defined by the UEFI memory map is identity mapped (virtual address equals physical address). The mappings to other regions are undefined and may vary from implementation to implementation
大事な点は、
- MMUはUEFIによって必ず有効化される
- UEFIが管理するメモリ領域はidentity mapping(仮想アドレスと物理アドレスが同じ)になっている
- その他のメモリ領域はEFI仕様では定めないので実装による
根本原因:MMU構成の違い
先に結末を言うと、このMMUが原因でした
自分が使っているx86_64向けの環境ではカーネルのエントリポイントの仮想アドレスが物理アドレスと等しかったため呼び出しに成功
aarch64向けの環境ではそうでなかった為、カーネルのエントリポイントとは関係ないところをループしていた
こんなん知ってないとわからん
自分がとった具体的な解決策は、恐らく一番単純な方法で、カーネルを呼び出す直前でMMUを無効化しました
解決策
以下が実装です
let entry_point = unsafe { core::mem::transmute::<_, KernelEntry,>(kernel_entry,) };
#[cfg(target_arch = "aarch64")]
unsafe {
// 全てのデータアクセスが完了するまで待機
asm!("dsb sy");
// 念の為キャッシュを全削除
asm!("ic iallu"); // 命令キャッシュを全て無効にする
asm!("dsb ish"); // ↑が完了するまで待機
asm!("isb"); // キャッシュクリア後に再度命令を読み込む
// ↑既にキャッシュを読み込んでいるかもしれないため、リロードする必要がある
// SCTLR_EL1を編集してMMUを無効化
asm!(
"mrs x0, sctlr_el1", // 現在のMMUの状態をx0レジスタに読み込む。有効になってるはず
"bic x0, x0, #1", // x0レジスタにx0レジスタの持つ値の最下位ビットをクリアした値をセット
// この値は、MMUが無効である状態を表す
"msr sctlr_el1, x0", // x0の値を反映。MMUを実際に無効化している行
"isb", // システムの状態を変更したので命令をリロードする
out("x0") _
);
}
// Jump to kernel with MMU disabled
entry_point();
// 失敗したら到達する
wfi();
}
なんか色々やってますが、ここでやっているのはMMUを無効化して、それがきちんとそれ以降の命令で反映される様にしてからカーネルを呼び出しています
ネタがわかって仕舞えば呆気ないですね
SCTLR_EL1について
システム制御レジスタ(SCTLR_EL1)は、MMUを含む基本的なシステム動作を制御します
その内、0ビット目はMMUの有効無効を表します
ビット0をクリアすることで、他の機能はそのままにMMUを無効化します
これにより、仮想アドレスが物理アドレスとして扱われます
同期バリア
ARMアーキテクチャは、MMU操作に不可欠ないくつかの同期バリアを提供しています
dsb
(データ同期バリア)
- 処理を続行する前にすべてのメモリアクセスが完了することを保証
- “dsb sy”はすべてのメモリ操作に影響
isb
(命令同期バリア)
- パイプラインをフラッシュし、すべての前の命令が完了したことを保証
- システムレジスタを変更した後に不可欠
ic iallu
(統合ポイントまでのすべての命令キャッシュを無効化)
- すべての命令キャッシュを無効化
- コードが古い変換でキャッシュされている可能性がある場合に必要
ここでいうパイプラインというのはキャッシュが置いてあるバッファのような物、という理解で大丈夫だと思います(有識者教えて)
これらのバリア命令によって、先ほど示したコードのようにカーネルにジャンプする前にMMU状態の変更が完全に適用されることを保証できます
カーネル側の考慮事項
ここからはまだ自分でも実装していないのですが、ブートローダーでMMUを無効化したので、MMUを利用する為にはカーネル側で有効化する必要があります
メモリマッピングも制御出来るようにしたいですが、その辺はページングを実装するときに気にする事なんですかね?
この辺はまた1から調べてみなければ..
結論
この記事で強調されたMMU構成の問題は、遭遇する可能性のあるハードウェアに由来する問題の一例に過ぎないでしょう
組み込み業務に従事しているとかでない限りこの辺りの知識はそもそも知らないです
なのでまずは調べることが大事になるのですが、何を・どう調べれば良いのかもわからないことが多いです
またニッチなジャンルをニッチな方向性で開発しているとやっと見つけたノウハウも役に立たないことがあります
そんな時に大事だと思うのは
- アーキテクチャの違いは重要:ハードウェア動作の根本的な違いにより、あるアーキテクチャで動作するものが別のアーキテクチャでは失敗する事はよくあります
自分が得た情報はどのアーキテクチャに対する情報なのかは確認しましょう(敗北済み) - 一次情報は重要:そんなの当たり前じゃんってなっちゃいますが、一次情報って分かる人が分かる人に向けて書いている節があるので、自分が全く馴染みのない分野の一次情報ってかなり読みにくいです
UEFI仕様書やELF仕様書はまだ読みやすいですが、ARMリファレンスなんかはMDNみたいなノリで読もうとすると沈没します
ですが理解できれば最も確実で手の届きやすい頼もしい情報源であることも事実
AIでもなんでも頼って一次情報を使い倒しましょう - 体系的なデバッグ大事:ベアメタルレベルで作業する場合、体系的なデバッグ技術は最も価値のあるツール
要は慣れなんですが、エラーが発生した場合はまず状況にあったデバッグ環境を整えてやることが最終的に近道であることが多いなーって思います - ドキュメントは少ない:Rustでのaarch64 OS開発のような特殊な組み合わせでは、複数のソースから情報を集める必要があることがよくあります
こんな感じでしょうか
最後に長くなりましたが、この記事が他のOS開発者が同様の落とし穴を避け、armアーキテクチャでのRustOS開発の道標の一つになることを願っています
参考文献
- ARM Architecture Reference Manual – ARMアーキテクチャのリファレンス(読みたくない)
- UEFI Specification – UEFI仕様書(最近やっとスラスラ読めるようになった)
- Rust UEFI Book –
uefi-rs
クレートの人達が整備するドキュメント(入門にはちょうど良さそう) - OSDev Wiki – OS開発のコミュニティwiki(基本何言ってるかわかんない)
- ELF Specification – elf仕様書(pdfなのつらい)
コメント