ポータビリティ

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 だけ、ブロックドライバ blkriscv64 と aarch64 だけです。

どれも、そのアーキの実ハードウェアに対応します。

関心事x86_64riscv64aarch64RV32ARMv6-M
割り込みコントローラAPIC / I/O APICCLINTGICv2CLINT 直接NVIC
メモリ保護4 段ページングSv39 ページングTTBR 変換テーブルPMP 領域MPU 領域
2 コア目の起動INIT-SIPI-SIPISBI HSM hart_startPSCI CPU_ONSIO FIFOSIO FIFO
時間基準TSC(PIT 較正)rdtimeCNTVCT / CNTFRQCLINT の mtimeµs TIMER
NICe1000virtio-netvirtio-netCYW43439CYW43439

アーキ非依存コアが、アーキを知らずに済む仕組み

セレクタが正しいモジュールを選ぶ、それだけでは足りません。アーキ非依存コードが、どのアーキの細部にも本当に依存しない——そこまで要ります。やり方は、各境界の契約を、アーキ固有のものを一切運ばない形に絞ることです。

いちばん分かりやすいのがコンテキストスイッチです。レジスタの並びはアーキがいちばん違う部分なので、トラップの境界を、それを一切見せない 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 で走ります。

**差し替わるアーキモジュールは 3 つ。**トラップと CPU が M-mode 版になり、MMU が no-op スタブになります。HAL より上は変わりません。

RV32 の経路は、さらにソフトフロートで FPU の無い実チップにまたがります。そこでは M-mode のトラップが、コンパイル時に FP の有無へ合わせます。ターゲットに FPU があるときだけ浮動小数レジスタを退避するので、1 つのトラップの実装が、ハードウェア FP のターゲットと、浮動小数レジスタがそもそも無いチップの両方をまかないます。

バイトコードは、切り離せる成果物

アプリのバイトコードは、カーネルイメージに溶接されていません。既定は焼き込みですが、バイトコードを起動時に配送することもできます。

配送があるターゲットでは、焼き込みでない別のアプリがその場で走ります。優先順はネット取得、次に UART、次に焼き込み。素の起動は何も払わず、バイト同一のままです。

だから、運用者のソフトウェアを、OS イメージとは別の成果物として切り出せます。設計全体を貫く、意図(バイトコード)と機構(カーネル)の分離そのものです。