ポータビリティ
Lambda OS は、64bit の x86 マシンを前提に書きました。ページテーブル、複数コア、ACPI、UEFI、ring 3。どの抽象も、その機械があるものとして書いてよかった。
そこから、**下へ降りました。**riscv64 へ、aarch64 へ、MMU 無しの bare RV32 へ。そして ARMv6-M の Cortex-M0+ へ——MMU も FPU も無く、アトミック命令が一つも無い、SRAM 264 KB の 32bit 部品です。
アトミック命令の無い部品まで降りると、契約を間違った場所で引いたカーネルは壊れます。レジスタの並び、ロック、ページテーブル、compare-and-swap——漏れていた仮定が、いちばん下で書き直しになるからです。
何が共通で、何が差し替わるか
スケジューラ、VM ホスト、ネットワークスタックは、5 つのターゲットで同じソースです。
その中に、チップに合わせて表のサイズを決める comptime 分岐が少しだけあります(タスク数、スタックサイズ、整列——264 KB は 4 GiB ではないので)。ただし、レジスタの並びも、ロックも、ページテーブルも、この境界を越えません。
**アーキ非依存コードは、アーキのファイルを直接インポートしません。**必ず HAL のセレクタを通します。
ファイルシステムはブロックドライバの上に乗るので、riscv64 と aarch64 でだけ動きます。物理フレームの割り当ては x86 だけのものです。
HAL の契約
どのモジュールを使うかは、コンパイル時に決まります。必要なモジュールを実装していないターゲットは、実行時ではなくコンパイル時に落ちます。
5 アーキすべてが持つモジュールは 7 つです。
cpu · console · mmu · intc · trap · gdt · nic
4 つ(smp ioapic pci acpi)は x86 だけ、ブロックドライバ blk は riscv64 と aarch64 だけです。
どれも、そのアーキの実ハードウェアに対応します。
| 関心事 | x86_64 | riscv64 | aarch64 | RV32 | ARMv6-M |
|---|---|---|---|---|---|
| 割り込みコントローラ | APIC / I/O APIC | CLINT | GICv2 | CLINT 直接 | NVIC |
| メモリ保護 | 4 段ページング | Sv39 ページング | TTBR 変換テーブル | PMP 領域 | MPU 領域 |
| 2 コア目の起動 | INIT-SIPI-SIPI | SBI HSM hart_start | PSCI CPU_ON | SIO FIFO | SIO FIFO |
| 時間基準 | TSC(PIT 較正) | rdtime | CNTVCT / CNTFRQ | CLINT の mtime | µs TIMER |
| NIC | e1000 | virtio-net | virtio-net | CYW43439 | CYW43439 |
アーキ非依存コアが、アーキを知らずに済む仕組み
セレクタが正しいモジュールを選ぶ、それだけでは足りません。アーキ非依存コードが、どのアーキの細部にも本当に依存しない——そこまで要ります。やり方は、各境界の契約を、アーキ固有のものを一切運ばない形に絞ることです。
いちばん分かりやすいのがコンテキストスイッチです。レジスタの並びはアーキがいちばん違う部分なので、トラップの境界を、それを一切見せない 1 つの関数に絞ります。
onTrap(sp: usize) usize
アーキのスタブが全レジスタを中断中のスタックに積み、スケジューラにはスタックポインタだけを渡します。スケジューラはそれを保存し、次のタスクのポインタを返す。スタブは pop して戻ります(iretq / sret / eret / 例外リターン)。
**スケジューラは、不透明なスタックポインタを扱うだけで、レジスタフレームを一度も見ません。**フレームの中身は、アーキ層の中に完全に留まります。Cortex-M ではハードウェアが半分を肩代わりします。例外エントリが 8 レジスタを積むので、ハンドラが残り 8 つを積めば、同じ契約が成り立ちます。
だから、同じスケジューラが 5 つのターゲットで動きます。
ARMv6-M — 祖先を共有しない命令セット
ARMv6-M です。aarch64 のポートではありません。あちらは ARMv8-A で、64bit・MMU・EL0/EL1・GIC。ARMv6-M はベンダ名のほかに何も共有しません。Thumb-1 だけ、MMU も FPU も無く、アトミック命令が一切無い。
それでも、同じアーキ非依存のスケジューラと、同じ Lambda C VM が、その上で走りました。スケジューラが譲ったのは、サイズ表だけです。ロジックは、そのまま降りました。
ほかの 3 つと祖先を共有しない命令セットが、それでも同じ契約に収まった。だから新しいターゲットは、OS のフォークではなく、有界な移植——アーキ層を書くだけ——で済みます。
MMU 無し MCU プロファイル
同じアーキ非依存のスケジューラ・VM・アクター層が、bare RISC-V(RV32 / RV64)の上を M-mode で走ります。
- **OpenSBI 無し。**カーネルがそのまま M-mode ファームウェアで、チップの入口アドレスからリセットします。
- **MMU 無し。**フラットで物理のアドレス空間で、隔離はページテーブルでなく PMP 領域でかけます。
- **CLINT 直結のタイマ。**周期割り込みは、ハードウェアの比較レジスタを書いて張ります。ファームウェア呼び出しはありません。
**差し替わるアーキモジュールは 3 つ。**トラップと CPU が M-mode 版になり、MMU が no-op スタブになります。HAL より上は変わりません。
RV32 の経路は、さらにソフトフロートで FPU の無い実チップにまたがります。そこでは M-mode のトラップが、コンパイル時に FP の有無へ合わせます。ターゲットに FPU があるときだけ浮動小数レジスタを退避するので、1 つのトラップの実装が、ハードウェア FP のターゲットと、浮動小数レジスタがそもそも無いチップの両方をまかないます。
バイトコードは、切り離せる成果物
アプリのバイトコードは、カーネルイメージに溶接されていません。既定は焼き込みですが、バイトコードを起動時に配送することもできます。
- UART 越し(XMODEM-CRC 転送)— x86_64 / riscv64 / aarch64。
- ネットワーク越し(TFTP)— x86 のみ。ただしこれは QEMU の
fw_cfgに依存していて、実機の x86 では発火しません。 - **MCU プロファイル(RV32 / ARMv6-M)には、まだ配送経路がありません。**焼き込みだけです。
配送があるターゲットでは、焼き込みでない別のアプリがその場で走ります。優先順はネット取得、次に UART、次に焼き込み。素の起動は何も払わず、バイト同一のままです。
だから、運用者のソフトウェアを、OS イメージとは別の成果物として切り出せます。設計全体を貫く、意図(バイトコード)と機構(カーネル)の分離そのものです。