From ffe7c50b7c1b0cbc917157cfcf792bc43b263e81 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 25 Oct 2026 11:15:43 +0000 Subject: [PATCH 1/2] =?UTF-8?q?Foundation:=20nommu=20=E3=81=AE=E3=82=B9?= =?UTF-8?q?=E3=83=AD=E3=83=83=E3=83=88=E6=96=B9=E5=BC=8F=E3=82=92=E3=82=A2?= =?UTF-8?q?=E3=83=AA=E3=83=BC=E3=83=8A=E6=96=B9=E5=BC=8F=E3=81=A8=E5=86=8D?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E5=8F=AF=E8=83=BD=E3=81=AA=20WAX=20=E3=81=AB?= =?UTF-8?q?=E7=BD=AE=E3=81=8D=E6=8F=9B=E3=81=88=E3=82=8B=20(#175)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ページアロケータとプロセスごとのアリーナ、WAX v3 の RELOCATABLE(relocation table と heap_hint)、 llvm-src32 の --pie/--heap-hint を追加し、Foundation の全プログラムを --pie でビルドする。 Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_011HTdnvAqSYxxtyGHv9j9d3 --- AGENTS.md | 2 +- doc/foundation_spawn_profile.md | 2 +- doc/foundation_vm_plan.md | 14 +- doc/ja/spec_WAX_v3.md | 37 +++- doc/spec_Foundation.md | 55 +++--- doc/spec_Foundation_sgubplay.md | 2 +- doc/spec_Foundation_tinybasic.md | 4 +- doc/spec_Foundation_wavplay.md | 2 +- doc/spec_WAX_v3.md | 46 ++++- examples/foundation-bmp/README.md | 2 +- examples/foundation-fb/README.md | 2 +- foundation/README.md | 44 ++--- foundation/boot/build.py | 67 +++---- foundation/boot/main.c | 8 +- foundation/include/foundation/kernel.h | 26 +-- foundation/kernel/mm.c | 136 ++++++++++--- foundation/kernel/mm.h | 29 ++- foundation/kernel/proc.c | 170 ++++++++++------ foundation/kernel/wax.c | 185 +++++++++++++++++- foundation/kernel/wax.h | 27 ++- foundation/tests/bmpview/test_bmpview.py | 2 +- foundation/tests/chip8/test_guest.py | 2 +- .../tests/console/test_console_guest.py | 2 +- foundation/tests/fb/test_fb.py | 2 +- foundation/tests/libsgc/test_libsgc.py | 2 +- foundation/tests/panic/test_panic.py | 2 +- foundation/tests/pec/test_pec_guest.py | 2 +- foundation/tests/perf/profile_spawn.py | 2 +- foundation/tests/sgc/guest.c | 14 +- foundation/tests/sgc/test_sgc.py | 2 +- foundation/tests/sgub/test_sgub.py | 2 +- foundation/tests/shell/cpu_features.a | 16 -- foundation/tests/shell/test_shell.py | 40 +--- foundation/tests/tinybasic/test_tinybasic.py | 2 +- foundation/tests/wav/test_wav.py | 2 +- foundation/tests/wax/build.py | 14 ++ foundation/tests/wax/host.c | 19 +- foundation/tests/wax/main.c | 45 ++++- foundation/tests/wax/pie.c | 47 +++++ foundation/tests/wax/test_wax_loader.py | 123 +++++++++++- memory/2026-10-25-issue-175-nommu-arena.md | 25 +++ tools/llvm-src32/README.md | 1 + tools/llvm-src32/driver.py | 35 +++- tools/llvm-src32/test_toolchain.py | 6 +- tools/llvm-src32/v3.py | 53 ++++- tools/wax/wax3.py | 21 +- 46 files changed, 1041 insertions(+), 302 deletions(-) delete mode 100644 foundation/tests/shell/cpu_features.a create mode 100644 foundation/tests/wax/pie.c create mode 100644 memory/2026-10-25-issue-175-nommu-arena.md diff --git a/AGENTS.md b/AGENTS.md index e67318e..10f92bc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -60,7 +60,7 @@ - PCG scroll: `cargo test --test vdp_pcg_scroll` - Foundation WAX ローダ(ホスト): `python3 -m unittest discover -s foundation/tests/wax`。testbench での実行は `tools/llvm-src32/test_toolchain.py` の `test_foundation_wax_loader` - Foundation シェル(testbench、PCG 画面の比較): `python3 -m unittest discover -s foundation/tests/shell`(`src32_testbench` と Clang が必要) -- Foundation 仮想記憶(`mm` 層。V のある CPU では仮想記憶、`FND_MM=slot` ではスロット): `cd tools/llvm-src32 && python3 -m unittest test_toolchain.ToolchainTests.test_foundation_vm_kernel`。シェルのテストは `python3 -m unittest discover -s foundation/tests/shell` と `FND_MM=slot python3 -m unittest discover -s foundation/tests/shell` の両方。カーネルがユーザーアドレスを直接参照せず `mm_copy_*` を使う契約は MEMORY.md を参照 +- Foundation 仮想記憶(`mm` 層。V のある CPU では仮想記憶、`FND_MM=nommu` ではスロット): `cd tools/llvm-src32 && python3 -m unittest test_toolchain.ToolchainTests.test_foundation_vm_kernel`。シェルのテストは `python3 -m unittest discover -s foundation/tests/shell` と `FND_MM=nommu python3 -m unittest discover -s foundation/tests/shell` の両方。カーネルがユーザーアドレスを直接参照せず `mm_copy_*` を使う契約は MEMORY.md を参照 - Foundation CPU 統計(ホスト、カウンタ wrap と user/kernel/idle の帰属): `python3 -m unittest discover -s foundation/tests/cpu`(ホスト C コンパイラが必要) - Foundation ディスク起動(BIOS → stage1 → FNDBOOT、testbench): `python3 -m unittest discover -s foundation/tests/disk`(`src32_testbench` と Clang が必要) - FS2 ホストツール: `python3 -m unittest discover -s tools/fs2` diff --git a/doc/foundation_spawn_profile.md b/doc/foundation_spawn_profile.md index 8871f5e..1b01fcb 100644 --- a/doc/foundation_spawn_profile.md +++ b/doc/foundation_spawn_profile.md @@ -158,7 +158,7 @@ VM では静的バッファ増加分の物理ページも消費する。 - `python -m unittest discover -s foundation/tests/disk_cache -v`: 1 test / 7シナリオ通過。 - `python -m unittest discover -s foundation/tests/shell -v`: VMで24件通過。 -- 同じシェルテストを `FND_MM=slot` で実行: 24件通過。 +- 同じシェルテストを `FND_MM=nommu` で実行: 24件通過。 - `python -m unittest discover -s foundation/tests/disk -v`: 7件通過(BIOS→stage1→kernel→shellを含む)。 - 計測用イメージの cached/VM、pipeline/VM、cached/slot で両echoの正常終了とR1=42停止を確認。 - `git diff --check` と変更したPythonの `py_compile` 通過。 diff --git a/doc/foundation_vm_plan.md b/doc/foundation_vm_plan.md index 2c54a73..d609789 100644 --- a/doc/foundation_vm_plan.md +++ b/doc/foundation_vm_plan.md @@ -103,7 +103,7 @@ |---|---|---| | `mm_hw` | `CPU_FEATURES` に V | ページ表による本物の仮想記憶 | | `mm_swap` | V なし(新規) | 全プロセスが同じ仮想窓を共有し、切替時にカーネルが像を出し入れする | -| `mm_slot` | V なし(現行) | 物理スロット固定。互換のため当面残す | +| `mm_slot` | V なし(旧) | 物理スロット固定。**2026-10-25 に nommu(アリーナ方式)で置き換えた**(下の「状況」を参照) | **エミュレーションの限界**: Extension P は「アドレスの許可判定」しかなく、アドレスの変換はできない。U モードの仮想アドレスは常に物理アドレスなので、フォルトで「別の物理ページへ読み替える」ことはできない。したがって実現できる範囲は次のとおり。 @@ -174,9 +174,19 @@ §7 の手順 3〜4 を実装しました。仕様は `doc/spec_Foundation.md` §5.5、コードは `foundation/kernel/mm.c`/`mm.h` です。 -- **mm 層**:`mm_slot` と `mm_vm` は `mm.c` の 2 つの実装で、`mm_init()` が最初の spawn で `CPUID` を見て選びます(`-D FND_MM_SLOT_ONLY` で固定)。カーネルがユーザーの仮想アドレスを直接参照する場所をなくし、`proc_user_range_ok` と `mm_copy_*` に集めました。§8 の確認事項は、ポインタ検証が `proc_user_range_ok`、ユーザーバッファの複写が `ipc.c`・`proc.c`・`blk.c`・`trap.c` の数十か所だったので、そこをすべて置き換えました。 +- **mm 層**:`mm_slot`(現在は nommu のアリーナ方式。下の「nommu」参照)と `mm_vm` は `mm.c` の 2 つの実装で、`mm_init()` が最初の spawn で `CPUID` を見て選びます(`-D FND_MM_NOMMU_ONLY` で固定)。カーネルがユーザーの仮想アドレスを直接参照する場所をなくし、`proc_user_range_ok` と `mm_copy_*` に集めました。§8 の確認事項は、ポインタ検証が `proc_user_range_ok`、ユーザーバッファの複写が `ipc.c`・`proc.c`・`blk.c`・`trap.c` の数十か所だったので、そこをすべて置き換えました。 - **決めたこと(§6 の Q)**:Q2 2 段・4 KiB、Q3 領域は使わない、Q4 S は変換しない、Q5 ハードウェア walk、Q7 スワップなし。 - **§10.1 からの変更**:ヒープはページ境界から始め、`mem_grow` はページを足すだけ(領域を使わない)。DMA は `blk_io` だけで、64 KiB のバウンスバッファを使います。 - **仕様案の限界として残したもの**:権限はページ単位です。Foundation のイメージは `--page-align` で W^X を保ちます(`.rodata` の R 専用化は未実装)。MMIO の付与もページ単位で、ページ範囲が届く先の検査を追加しましたが、同じページの別レジスタには触れます。需要ページング、COW、WAL の共有ページ、7 個を超えるプロセスは未実装です。 - **Q9(`sys_info` の能力ビット)**:まだ足していません。`mm_swap` を実装するときに足します。 - **ツールチェーン**:`cpt32_cpu_features()`(`CPUID`)と、`cpt32_mtsr`/`mfsr` で SR `0x40..0x44` が使えます。パイプライン CPU は `CPUID` の暗黙の R1/R2 を interlock しないので、コンパイラは `CPUID` の後に NOP を 3 つ置きます。 + +## 13. nommu(アリーナ方式)への置き換え(2026-10-25) + +V のない CPU の `mm_slot` を、nommu Linux のように**プロセスごとに連続した 1 つのアリーナ**を割り当てる方式に置き換えました。§9 の `mm_swap`(像の出し入れ)は採らず、再配置可能な実行形式で「同じアドレスにリンクされたアプリを同時に動かせない」を解決します。仕様は `doc/spec_Foundation.md` §5.2、画像形式は `doc/spec_WAX_v3.md` §3.1 です。 + +- 4 KiB ページのビットマップ(`[0x400000, RAM 末尾)`、最大 256 MiB)から、プロセスの全ブロック・WAL・スタック・ヒープ分の連続ページを first fit で取ります。固定アドレスの画像(v2 や `--protected`)は、その範囲を要求して取れなければ `FND_E_ADDRESS_CONFLICT` です。 +- Foundation の全プログラムは `--pie`(再配置可能な WAX v3)です。カーネルは試しの基底で配置を計画してから確保し、`wax_rebase` で本番の基底に動かして、relocation table を適用します。 +- `mem_grow` は nommu の `brk` と同じで、アリーナを動かせません。ヒープがアリーナの末尾にあり、上のページが空いているときだけ伸びます。大きなヒープが要る画像は `--heap-hint` で最初に求めます。 +- 保護は Extension P の 8 領域(MPU)で、切替は従来どおりです。物理メモリを共有しない保証(ゼロ化・プール)もスロットのときと同じです。 +- 残る制約:断片化は圧縮(アリーナの移動)で解消できません。プロセスは 7 個までのままです(スロットの数ではなくなったので、増やせます)。 diff --git a/doc/ja/spec_WAX_v3.md b/doc/ja/spec_WAX_v3.md index d1eac19..ed9a980 100644 --- a/doc/ja/spec_WAX_v3.md +++ b/doc/ja/spec_WAX_v3.md @@ -4,7 +4,7 @@ translation_of: ../spec_WAX_v3.md --- # WAX/WAL v3 — CPU 保護とライブラリシンボル -Revision 3.0、2026-10-21。状態: **仕様策定済み・Foundation カーネルが読み込み可能**。 +Revision 3.1、2026-10-25。状態: **仕様策定済み・Foundation カーネルが読み込み可能**。 v3 のバイナリ形式と基本 OS ローダー契約を定義します。 「必須」「禁止」は規範要件です。[英語版](../spec_WAX_v3.md)。 @@ -55,7 +55,7 @@ header は固定 160 byte。空 table でも entry_size は規定値です。 | 4 | 2 | version = 3 | | 6 | 2 | type: 1 WAX、2 WAL | | 8 | 2 | arch = 1 (SRC32) | -| 10 | 2 | flags: bit0 DEBUG_PRESENT、bit1 PROTECTED、他は 0 | +| 10 | 2 | flags: bit0 DEBUG_PRESENT、bit1 PROTECTED、bit2 RELOCATABLE(§3.1)、他は 0 | | 12 | 4 | header_size = 160 | | 16 | 4 | file_size = 実ファイル長 | | 20 | 4 | call_abi = 1(§6) | @@ -85,7 +85,10 @@ header は固定 160 byte。空 table でも entry_size は規定値です。 | 116 | 4 | os_min_minor | | 120 | 4 | debug_offset: 不在なら 0 | | 124 | 4 | debug_size: 不在なら 0 | -| 128 | 32 | reserved | +| 128 | 4 | reloc_table_offset: ファイル先頭からの位置。RELOCATABLE のときだけ非ゼロ | +| 132 | 4 | reloc_count: RELOCATABLE のときだけ非ゼロ | +| 136 | 4 | heap_hint: プログラムが求めるヒープの byte 数(8 の倍数)。無ければ 0。RELOCATABLE のときだけ指定可 | +| 140 | 20 | reserved | header、各 table、string table、非空 payload、debug のファイル範囲は 相互に重ならず、file_size 内に収まる必要があります。開始 offset はすべて 4 の倍数。 @@ -139,6 +142,34 @@ WAX entry は 4-byte 整列、先頭命令 4 byte 全体が初期化済み RX stack top/size は非ゼロの 8 の倍数、減算で underflow せず、image 予約範囲と重複不可。 OS はスタック全体をゼロ化します。WAL の stack/entry フィールドはすべて 0 です。 +### 3.1 再配置可能な実行形式 + +flags の RELOCATABLE を立てた WAX は、nommu システムの FLAT 形式のように、 +どのアドレスにでも読み込めます。`base_addr`、block の address、entry、スタックは +**リンク時**の配置で、ローダーは最大 alignment の倍数で、すべての範囲が RAM に収まる +`delta` を選び、image 全体を `base_addr + delta` に置いて relocation table を適用します。 +再配置可能にできるのは WAX だけです(WAL は構造上すでに位置独立)。 + +relocation table は `reloc_table_offset` にある u32 を `reloc_count` 個並べたもので、 +address の厳密な昇順です。各エントリは上位 30 bit がリンク時アドレス(4 byte 整列)、 +下位 2 bit が種類です。 + +| 種類 | 意味 | +|---:|---| +| 0 | R/RW block にある 32 bit big-endian のデータ語。`delta` を足す | +| 1 | RX block にある `LDIH rd, hi` とそれに続く `LDIL rd, lo`(rd は同じ)。`hi:lo` の 32 bit 値はリンク時アドレスで、`delta` を足して両方の即値を書き直す | + +それ以外の種類は予約で、拒否します。各エントリは、対応する権限の block の +初期化済み領域に完全に収まらなければなりません。entry や image-relative offset、 +import slot は配置後の origin からの相対なので、エントリは要りません。 +再配置はプロセスの実行前、スタックのゼロ化より前に行います。 + +再配置可能な WAX のスタックは最下位の block の直下にあり(`user_stack_top == base_addr`)、 +予約範囲 `[base_addr - user_stack_size, 最上位 block の終端)` は 1 つの連続領域として +まとめて動きます。ローダーは初期の `R30` に配置後の origin(`base_addr + delta`)を入れ、 +位置独立な開始コードが image を見つけられるようにします。ローダーは最上位 block の上に +ヒープ用のゼロ化メモリを足してよい(`heap_hint` を参照)。それは image には含まれません。 + ## 4. 依存とシンボル ### 4.1 Dependency entry(16 byte) diff --git a/doc/spec_Foundation.md b/doc/spec_Foundation.md index ae107fd..79fe122 100644 --- a/doc/spec_Foundation.md +++ b/doc/spec_Foundation.md @@ -116,11 +116,11 @@ S モードの R30 は、常にスタブのアドレスでなければなりま ## 5. メモリと保護 -メモリ管理には 2 つのバックエンドがあります。カーネルは最初のプロセスを起動するときに `CPUID` の `CPU_FEATURES` を読み、Extension V(bit 9、[spec_CPU_vm.md](spec_CPU_vm.md))があれば**仮想記憶**(§5.5)、なければ**スロット**(§5.2〜§5.4)を使います。システムコール・IPC・ハンドルの ABI(§6〜§10)はどちらでも同じです。以降、§5.2〜§5.4 はスロットバックエンドの規則で、仮想記憶で変わる点は §5.5 にまとめます。 +メモリ管理には 2 つのバックエンドがあります。カーネルは最初のプロセスを起動するときに `CPUID` の `CPU_FEATURES` を読み、Extension V(bit 9、[spec_CPU_vm.md](spec_CPU_vm.md))があれば**仮想記憶**(§5.5)、なければ **nommu**(MMU のない Linux と同じ考え方。§5.2〜§5.4)を使います。システムコール・IPC・ハンドルの ABI(§6〜§10)はどちらでも同じです。以降、§5.2〜§5.4 は nommu の規則で、仮想記憶で変わる点は §5.5 にまとめます。 -### 5.1 物理配置(v1、Main RAM 16 MiB で全スロットが揃う) +### 5.1 物理配置(Main RAM 16 MiB の例) -カーネルは実装されている Main RAM の容量を BMC の `MAIN_RAM_SIZE`([BMC §1.5](spec_BMC_DMAC.md))から読みます。0(容量不明)なら 16 MiB とみなします。配置は容量によらず次の表で固定で、16 MiB を超える RAM は v1 では使いません。 +カーネルは実装されている Main RAM の容量を BMC の `MAIN_RAM_SIZE`([BMC §1.5](spec_BMC_DMAC.md))から読みます。0(容量不明)なら 16 MiB とみなします。カーネルより下の配置は容量によらず次の表で固定で、`0x00400000` からこの RAM の末尾まで(最大 256 MiB)がユーザーメモリです。 | 範囲 | 用途 | |---|---| @@ -128,26 +128,18 @@ S モードの R30 は、常にスタブのアドレスでなければなりま | `0x00010000–0x000EFFFF` | カーネルイメージ | | `0x000F0000–0x000FFFFF` | カーネルスタック(頂上 `0x00100000`) | | `0x00100000–0x003FFFFF` | カーネルヒープ。起動中は `0x00200000` 以降を FNDBOOT の staging に使う(§11) | -| `0x00400000–` | ユーザースロット(§5.2) | +| `0x00400000–` | ユーザーメモリ(§5.2) | -### 5.2 ユーザースロット +### 5.2 ユーザーメモリ(nommu) -v1 の実行形式は固定アドレスで、MMU もありません。そのため、ユーザーメモリは固定のスロットに分けます。 +アドレス変換がないので、ユーザーのアドレスは物理アドレスそのものです。カーネルはユーザーメモリを 4 KiB のページに分けて管理し、プロセスごとに**連続したページの範囲(アリーナ)**を 1 つ与えます。アリーナにはそのプロセスの全ブロック、読み込む全 WAL、スタック、ヒープが入ります。固定のスロットはありません。 -| スロット | 範囲 | 標準の用途 | -|---:|---|---| -| 0 | `0x00400000–0x0047FFFF` | init | -| 1 | `0x00480000–0x004FFFFF` | console | -| 2 | `0x00500000–0x0057FFFF` | disk | -| 3 | `0x00580000–0x005FFFFF` | fs | -| 4 | `0x00600000–0x007FFFFF` | shell | -| 5 | `0x00800000–0x00BFFFFF` | アプリ A(ツールの既定) | -| 6 | `0x00C00000–0x00FFFFFF` | アプリ B | - -- 実在する RAM の外にあるスロット(末尾が Main RAM の容量を超えるもの)は使えません。そこへの spawn は `FND_E_NO_MEMORY` です。たとえば 8 MiB ではスロット 0〜4 だけが使えます。 -- 1 つのプロセスは、`base_addr` を含むスロットを 1 つだけ占有します。全ブロック、ヒープ、スタックは、そのスロットの中に収まっていなければなりません(収まらなければ `FND_E_FORMAT`)。 -- 占有中のスロットへの spawn は `FND_E_ADDRESS_CONFLICT` になります。 -- 「標準の用途」はツールの既定値を示すだけで、カーネルは用途を検査しません。 +- **再配置可能な実行形式**([WAX v3 §3.1](spec_WAX_v3.md))は、空いている連続ページのうち最も低い場所に置きます。必要な大きさは、イメージ・WAL・スタックの予約範囲と、ヒープの予約(実行形式の `heap_hint`、なければ 128 KiB)の合計を 4 KiB に切り上げたものです。カーネルは読み込むときに、実行形式の持つアドレスへ置き場所との差を足します(Linux の nommu の FLAT 形式と同じです)。同じ画像を何個でも同時に起動できます。置ける連続範囲がなければ `FND_E_NO_MEMORY` です。 +- **固定アドレスの実行形式**(v2 と、再配置可能でない v3)は、`base_addr` からスタックの上端までの予約範囲をそのまま 4 KiB 境界に広げた範囲を取ります。その範囲のページが 1 つでも使用中なら `FND_E_ADDRESS_CONFLICT`、実在する RAM やユーザーメモリの外にかかるなら `FND_E_NO_MEMORY` です。WAL は、その範囲の中に置きます(§9.5)。 +- **ヒープ**:最も高いブロックの末尾から始まり、アリーナの末尾か、その上にある次の予約(スタックなど)までです。ヒープがアリーナの最後にあるとき、`mem_grow` は直上の空きページを同じアリーナに加えて伸ばします(Linux の nommu の `brk` と同じで、**動かすことはできません**)。直上を別のプロセスが使っていれば `FND_E_NO_MEMORY` です。大きなヒープが要るプログラムは、`heap_hint` で最初から予約してください。 +- **解放**:プロセスが終了すると、書き込んだ範囲(保護領域のうちカーネルが所有するもの)をゼロにしてから、アリーナ全体をプールへ返します。空きページは常にゼロです。 +- コードをカーネルのイメージにリンクして動かす `fnd_proc_create_builtin` は、旧スロットの範囲(`fnd_slot_base`/`fnd_slot_end`)の指定を受け付け、その範囲の上端 `ram_size` byte を同じ方法で取ります。ほかの用途ではスロットを使いません。 +- プロセスの数は 7 個のままです(メモリの区画とは無関係な、プロセス記録の上限)。 ### 5.3 保護領域の割り当てと切り替え @@ -335,7 +327,7 @@ v1 の実行形式は固定アドレスで、MMU もありません。そのた - 4 = RTC 秒の上位 32 bit - `debug_write`:カーネルのデバッグコンソール(UART)へ出力します。1 回あたり 256 bytes まで書け、それを超える分は切り詰めます。 - `sys_info` の sel: - - 0 = `fnd_sysinfo`(32 bytes)。順に `abi_major`、`abi_minor`、`tick_hz`、`ram_bytes`、`free_bytes`、`proc_count`、`boot_drive`、予約。`ram_bytes` は §5.1 の Main RAM 容量、`free_bytes` は実在するスロットのうち動いているプロセスが占めていないものの合計 + - 0 = `fnd_sysinfo`(32 bytes)。順に `abi_major`、`abi_minor`、`tick_hz`、`ram_bytes`、`free_bytes`、`proc_count`、`boot_drive`、予約。`ram_bytes` は §5.1 の Main RAM 容量、`free_bytes` は nommu ではアリーナに使われていないページの合計(仮想記憶では未使用のフレームの合計) - 1 = プロセス一覧。1 件 48 bytes で、順に `pid`、`parent`、`state`(0 = 実行可能、1 = BLOCKED、2 = 終了済み)、`priority`、`exit_code`、`mem_bytes`、`threads`、`fault_epc`、`name[16]` - 2 = CPU 統計。16 bytes のヘッダ(`elapsed_cycles`、`kernel_cycles`、`idle_cycles`、`clock_hz`)に、プロセスごとの 8 bytes(`pid`、`user_cycles`)が続きます。すべて u32、カウンタは 2^32 で wrap します。レコード順は sel 1 と同じで、終了記録も含み、再利用時は新 PID のカウンタを 0 にします。PeC timer ch1 を IRQ なしの 48 MHz free-running clock としてカーネルが予約します。スケジューラの起動時から計測し、HALT 中の経過も idle に含みます。使用率はスナップショット間の符号なし差分で計算し、間隔は約89秒未満とします。 - `kernel_cycles` は C の trap ハンドラ入口から scheduler 終了までの概算です。アセンブリの context save/restore と C 計測関数の境界外の命令は、前後の user/idle に含まれます。厳密な OS オーバーヘッドとは一致しません。`user_cycles` はプロセスの全スレッドの合計、syscall 処理時間は kernel 側です。ch1 による計測自体の実行コストもあります。 @@ -412,8 +404,8 @@ v1 の実行形式は固定アドレスで、MMU もありません。そのた CPU が Extension V を報告するとき(`CPU_FEATURES` bit 9)、カーネルは `VCTL.EN` を 1 にして、U モードの命令フェッチ・ロード・ストアをページ表で変換します。S モードは変換しません(カーネルは物理アドレスのまま)。設計の理由は [foundation_vm_plan.md](foundation_vm_plan.md) にあります。 - **アドレス空間**:プロセスごとに 2 段のページ表(4 KiB ページ)を持ちます。プロセスを切り替えるとき、`PTBR` を書きます(TLB は全消去されます。ASID はありません)。保護領域(§5.3)は使いません。同じプロセスの中のスレッド切り替えでは何もしません。 -- **物理フレーム**:`0x00400000` 以降、Main RAM の末尾までを 4 KiB のフレームとして管理します(最大 256 MiB)。先頭の 64 KiB は `blk_io` の転送用バッファに予約します。フレームは付与する直前にゼロにし、プロセスの終了時に(ページ表も含めて)すべて返します。スロットの表(§5.2)は使いません。 -- **配置**:イメージは `0x00001000` 以上 `0x10000000` 未満のどこにでも置けます(この範囲を超える・ページ 0 を含む場合は `FND_E_FORMAT`)。WAL もこの範囲に first fit で置きます。リンクアドレスが同じイメージを複数同時に起動できるので、`FND_E_ADDRESS_CONFLICT` は返りません。スロットの範囲に置かれた既存のイメージもそのまま動きます。 +- **物理フレーム**:`0x00400000` 以降、Main RAM の末尾までを 4 KiB のフレームとして管理します(最大 256 MiB)。先頭の 64 KiB は `blk_io` の転送用バッファに予約します。フレームは付与する直前にゼロにし、プロセスの終了時に(ページ表も含めて)すべて返します。アリーナ(§5.2)は使いません。 +- **配置**:イメージは `0x00001000` 以上 `0x10000000` 未満のどこにでも置けます(この範囲を超える・ページ 0 を含む場合は `FND_E_FORMAT`)。WAL もこの範囲に first fit で置きます。リンクアドレスが同じイメージを複数同時に起動できるので、`FND_E_ADDRESS_CONFLICT` は返りません。再配置可能な実行形式([WAX v3 §3.1](spec_WAX_v3.md))は、仮想記憶ではリンクしたアドレスのまま読み込みます(アドレス空間がプロセスごとなので、動かす必要がありません)。固定アドレスの既存のイメージもそのまま動きます。 - **権限はページ単位**:1 ページには 1 組の権限しかありません。ブロックの境界が同じページにある場合、そのページは両方の権限の和になります(例:RX のコードと RW のデータが同じページなら RWX)。Foundation のイメージ(サービス・シェル・コマンド・libfnd の WAL)は、llvm-src32 の `--page-align` でコード(RX)の次の RW データを 4 KiB 境界から始め、WAX v3 ではそのブロックの整列も 4096 にします。このためコードのページは書けず、データのページは実行できません(W^X)。`--page-align` を付けないイメージは、ページを共有する分だけ権限が広がります。`.rodata` は現在 RW データの中にあり、R 専用ブロックには分けていません。 - **領域の上限**:保護領域がないので、MMIO の付与や `mem_grow` が `FND_E_REGION_LIMIT` になることはありません。ただしイメージ自体は、ローダーの規則どおり 8 個の領域(隣接して権限が同じブロックをまとめたもの)までです。 - **ヒープ**:最も高いブロック末尾の次のページ境界から始まり、次の予約(スタック・WAL・MMIO の付与)の手前のページ境界、または `0x80000000` の手前まで伸ばせます。`mem_grow` は新しいページを足すだけで、物理的に連続している必要はありません。 @@ -432,7 +424,7 @@ CPU が Extension V を報告するとき(`CPU_FEATURES` bit 9)、カーネ 2. 進行中の `blk_io` があれば IDC に ABORT を発行し、完了を待つ。DMA が完了するまで、転送先のメモリを解放してはならない。 3. 全ハンドルを閉じる。これにより `PEER_GONE` と `CLIENT_GONE` が届く。 4. IRQ の割り当てとサービス登録を解除する。 -5. 付与した範囲をゼロにし、スロットを解放する(仮想記憶ではページ表とフレームを返す。§5.5)。 +5. 付与した範囲をゼロにし、アリーナを返す(仮想記憶ではページ表とフレームを返す。§5.5)。 6. `wait` している側を起こす。 終了したプロセスの記録は、`sys_info` で読めるように、PROCESS ハンドルが閉じられるまで残します。 @@ -442,9 +434,9 @@ CPU が Extension V を報告するとき(`CPU_FEATURES` bit 9)、カーネ `proc_spawn` と §11 の同梱モジュールの起動は、どちらも次の規則で WAX を読み込みます。[WAX v3 §4.1・§7](spec_WAX_v3.md) が OS に委ねている選択と配置の方針も、ここで定めます。 - **受け付ける形式**:PROTECTED の WAX v2 と、type が WAX の v3。v3 では、すべての画像(WAX と WAL)が `call_abi = 1`、`os_id = foundation`、`os_major = 1`、`os_min_minor` ≤ カーネルの minor(現在 0)でなければなりません。検証と束縛は WAX v3 §2〜§7 に従います。 -- **スロット**(スロットバックエンド。仮想記憶では §5.5 の範囲):`base_addr` を含むスロット(§5.2)に、WAX の予約範囲(最も低いブロックから最も高いブロックの末尾まで、穴を含む)、スタック、読み込む全 WAL が収まらなければなりません。収まらなければ `FND_E_FORMAT` です。 +- **置き場所**(nommu は §5.2、仮想記憶は §5.5):WAX の予約範囲(最も低いブロックから最も高いブロックの末尾まで、穴を含む)、スタック、読み込む全 WAL は、1 つの範囲(固定アドレスの実行形式では予約範囲を 4 KiB 境界に広げたもの、再配置可能な実行形式ではアリーナ)に収まらなければなりません。収まらなければ `FND_E_FORMAT` です。 - **提供者の選択**:候補は、カーネルに渡されたライブラリ一覧のうち、検証に通る WAL で `image_id` と `abi_major` が要求と一致するものです。その中で `abi_minor` が最大のものを選び、同じなら一覧で先にあるものを選びます。選んだ WAL の `abi_minor` が、同じ ID へのすべての要求の `min_minor` の最大値に満たなければ失敗します。 -- **WAL の配置**:依存の帰りがけ順(依存される WAL が先)に、スロット先頭からの first fit で置きます。各 WAL の基底 B は、その WAL のブロック整列の最大値に揃えた、WAX の予約範囲・スタック・配置済みの WAL と重ならない最も低いアドレスです。 +- **WAL の配置**:依存の帰りがけ順(依存される WAL が先)に、その範囲の先頭からの first fit で置きます。各 WAL の基底 B は、その WAL のブロック整列の最大値に揃えた、WAX の予約範囲・スタック・配置済みの WAL と重ならない最も低いアドレスです。 - **上限**:1 回の起動で画像は 8 個(WAX と WAL 7 個)まで、1 画像あたりブロックは 32 個、依存は 8 件までです。超えると `FND_E_LIMIT` です。 - **エラー**:WAX v3 §7 の論理分類を次のように対応させます。 @@ -455,7 +447,8 @@ CPU が Extension V を報告するとき(`CPU_FEATURES` bit 9)、カーネ | DEPENDENCY_MISSING、DEPENDENCY_CONFLICT、DEPENDENCY_CYCLE、SYMBOL_MISSING、SYMBOL_MISMATCH | `FND_E_DEPENDENCY` | | REGION_LIMIT | `FND_E_REGION_LIMIT` | | RESOURCE_LIMIT | `FND_E_LIMIT` | -| スロットが使用中(スロットバックエンドのみ) | `FND_E_ADDRESS_CONFLICT` | +| 固定アドレスの範囲が使用中(nommu のみ) | `FND_E_ADDRESS_CONFLICT` | +| 連続した空きページがない(nommu) | `FND_E_NO_MEMORY` | **ライブラリの置き場と渡し方(2026-10-22 決定)**: @@ -573,7 +566,7 @@ stage1 は、`kernel_load` = `0x00010000`、`kernel_size` ≤ `0xE0000`(§5.1 - ABI ヘッダ `foundation/include/foundation/{abi.h,errors.h,ipc.h}` に、本書の番号と構造体を定義します。 - ラッパーは `cpt32_syscall(n, 0, a1, a2, a3, a4)` で書きます。 -- ユーザープログラムは `tools/llvm-src32` の `--protected` で、スロットの origin とスタックを指定してビルドします。 +- ユーザープログラムは `tools/llvm-src32` の `--wax3 --pie`(どこにでも読み込める再配置可能な WAX v3)でビルドするのが標準です。固定アドレスにしたいときは `--protected` や `--origin` で origin とスタックを指定します。 - 起動コードの終端にある `SYSCALL 0` は Foundation では `sys_info` にあたり、終了処理になりません。Foundation 向けの起動コードは `proc_exit` を呼ばなければなりません。`tools/llvm-src32` の `--wax3`(`--os foundation`、既定)が作る WAX v3 の起動コードは、`main` の戻り値で `proc_exit` を呼びます。`--protected`(WAX v2)の起動コードは今も `SYSCALL 0` なので、v2 のプログラムは自分で `proc_exit` を呼んで終わる必要があります。 - 共有ライブラリは `--wal` で WAL として作り、使う側は `--import-wal` で WAL を指定して WAX v3 にします([WAX v3 §6](spec_WAX_v3.md) の call ABI 1)。 @@ -609,10 +602,10 @@ sol の言語の意味と、関数の呼び出し規約・フレーム(R26 の | R30 | IRQ ディスパッチャ用に予約 | ユーザーの GP(UGP)。生成コードは変更しない | | プログラムの終端 | `HALT` | `0 fnd_proc_exit` 相当(`SYSCALL 0x11`、R1 = 0) | | `!asm` 内の `HALT`、`IRET`、`MFSR`、`MTSR` | 使える | エラー(行頭の命令名で判定する。安全のための保証ではなく、誤用を防ぐための検査) | -| 既定の配置 | origin 0、rodata `0x20000`、変数 `0x100000`、スタック 1 MiB | origin はスロット 5 `0x00800000`。rodata はコード直後、変数は rodata 直後(いずれも 4-byte 境界)。スタック頂上はスロット末尾 `0x00C00000`、スタック容量は既定 256 KiB(`!force_stack_size` で変更できる) | +| 既定の配置 | origin 0、rodata `0x20000`、変数 `0x100000`、スタック 1 MiB | origin は旧スロット 5 の `0x00800000`。rodata はコード直後、変数は rodata 直後(いずれも 4-byte 境界)。スタック頂上は旧スロット 5 の末尾 `0x00C00000`、スタック容量は既定 256 KiB(`!force_stack_size` で変更できる) | | 出力 | 従来の WAX または raw | PROTECTED WAX。ブロックの権限はコードが RX、rodata が R、変数が RW。ヒープはスタックの下端を越えない | -- 明示した `--org`、`!rodata_base`、`!var_base`、`!stack_top` は既定より優先します。ただし配置がスロットに収まらない場合は、コンパイル時にエラーにします。 +- 明示した `--org`、`!rodata_base`、`!var_base`、`!stack_top` は既定より優先します。ただし配置が `0x00C00000` までに収まらない場合は、コンパイル時にエラーにします。 - solc は権限とスタックを `tools/asm/asm.py` の指令で渡します。`.PROTECTED stack_top, stack_size` で PROTECTED WAX を選び、`.PERM rx`/`.PERM r`/`.PERM rw` で以降のブロックの権限を指定します([CPU 仕様 §7.1](spec_CPU.md))。 - 省略可能な major は、ディレクティブと同じ行に書かなければなりません。次の行の数値はトップレベルのコードです。 @@ -667,7 +660,7 @@ end - ハンドルを転送すると、送信側でそのハンドルが閉じられる。 - サーバーが終了すると `call` が `PEER_GONE` になる。 - クライアントが終了すると `CLIENT_GONE` が 1 回だけ届く。 -- **終了処理**:READ の途中で kill しても、転送が完了または中止するまでメモリが解放されない。spawn と exit を繰り返しても、ハンドル、スロット、カーネルヒープが漏れない。 +- **終了処理**:READ の途中で kill しても、転送が完了または中止するまでメモリが解放されない。spawn と exit を繰り返しても、ハンドル、アリーナのページ、カーネルヒープが漏れない。 - **sol**: - `!abi_compat` の不一致、未知の名前、配置違反、`irqN` 関数、`!asm` 内の特権命令が、すべてエラーになる。 - 生成した WAX が PROTECTED で、権限とスタックが §13.2 のとおりになっている。 diff --git a/doc/spec_Foundation_sgubplay.md b/doc/spec_Foundation_sgubplay.md index c676436..a9b365b 100644 --- a/doc/spec_Foundation_sgubplay.md +++ b/doc/spec_Foundation_sgubplay.md @@ -66,6 +66,6 @@ python -m unittest discover -s foundation/tests/sgub -v ``` ホスト試験にはCコンパイラ(`HOST_CC`で指定可能)、Foundation実行試験には -Clangと `src32_testbench` が必要です。同じ試験を `FND_MM=slot` でも実行します。 +Clangと `src32_testbench` が必要です。同じ試験を `FND_MM=nommu` でも実行します。 試験はv1/v2/v3、実Furnaceのpool付き出力、完了・繰り返し・停止・再オープン、 不正ファイルと未対応targetの事前拒否、callbackの出力エラーを確認します。 diff --git a/doc/spec_Foundation_tinybasic.md b/doc/spec_Foundation_tinybasic.md index 7de0665..fd704b9 100644 --- a/doc/spec_Foundation_tinybasic.md +++ b/doc/spec_Foundation_tinybasic.md @@ -53,7 +53,7 @@ CLS は CONSOLE の役割があれば画面を消します。実行中の無限 プログラム行・AST・文字列・制御スタックは `fnd_mem_alloc` で確保した約1.9 MiB の私有領域に置きます。 固定物理アドレス、PCG/PeC/IDC/DMAC MMIO、独自 IRQ ハンドラや HALT は使用しません。 -仮想記憶と `FND_MM=slot` のどちらでも、標準コマンドのスロット5で動きます。 +仮想記憶と `FND_MM=nommu` のどちらでも、標準コマンドのスロット5で動きます。 ## ビルドと検証 @@ -61,7 +61,7 @@ CLS は CONSOLE の役割があれば画面を消します。実行中の無限 python foundation/boot/build.py target/foundation-boot python foundation/run.py --root examples/tinybasic-c/foundation-root python -m unittest discover -s foundation/tests/tinybasic -v -# 同じテストを環境変数 FND_MM=slot でも実行する +# 同じテストを環境変数 FND_MM=nommu でも実行する ``` 起動ディスクには `/bin/tinybasic.wax` が自動的に入ります。 diff --git a/doc/spec_Foundation_wavplay.md b/doc/spec_Foundation_wavplay.md index b134fa2..84a5a26 100644 --- a/doc/spec_Foundation_wavplay.md +++ b/doc/spec_Foundation_wavplay.md @@ -56,7 +56,7 @@ cargo build --release --bin src32_testbench python -m unittest discover -s foundation/tests/wav -v ``` -`FND_MM=slot` でも同じテストを実行します。WindowsではTEMP/TMPを書き込み可能な場所へ設定してください。 +`FND_MM=nommu` でも同じテストを実行します。WindowsではTEMP/TMPを書き込み可能な場所へ設定してください。 ホストテストは符号・エンディアン・レート変換・チャンク境界を検証します。 ゲストテストはFoundation上の実コマンドとDAC APIを実行し、左右の分離、再生完了、停止・再オープン、 権限拒否、FIFO満杯・再試行を検証します。 diff --git a/doc/spec_WAX_v3.md b/doc/spec_WAX_v3.md index bc2943b..a7d5af2 100644 --- a/doc/spec_WAX_v3.md +++ b/doc/spec_WAX_v3.md @@ -1,6 +1,6 @@ # WAX/WAL v3 — protected executables and library symbols -Revision 3.0, 2026-10-21. Status: **specified; loaded by the Foundation kernel**. This document +Revision 3.1, 2026-10-25. Status: **specified; loaded by the Foundation kernel**. This document defines the v3 wire format and the baseline OS loader contract. MUST, MUST NOT and MAY are normative. [Japanese translation](ja/spec_WAX_v3.md). @@ -34,7 +34,7 @@ conversion. The baseline supports eager binding, function and object exports, private per-process WAL instances and an acyclic dependency graph. Lazy binding, -weak/global/interposed symbols, TLS, relocations, automatic constructors, +weak/global/interposed symbols, TLS, symbol relocations, automatic constructors, finalizers, runtime load/unload and cross-process sharing are not defined. Libraries needing setup/teardown expose ordinary explicit functions. @@ -54,7 +54,7 @@ unless explicitly called image-relative. Reserved fields MUST be zero. | 4 | 2 | version = 3 | | 6 | 2 | type: 1 WAX, 2 WAL | | 8 | 2 | arch = 1 (SRC32) | -| 10 | 2 | flags: bit 0 DEBUG_PRESENT, bit 1 PROTECTED; others zero | +| 10 | 2 | flags: bit 0 DEBUG_PRESENT, bit 1 PROTECTED, bit 2 RELOCATABLE (§3.1); others zero | | 12 | 4 | header_size = 160 | | 16 | 4 | file_size = exact file length | | 20 | 4 | call_abi = 1 (§6) | @@ -84,9 +84,12 @@ unless explicitly called image-relative. Reserved fields MUST be zero. | 116 | 4 | os_min_minor | | 120 | 4 | debug_offset: zero when absent | | 124 | 4 | debug_size: zero when absent | -| 128 | 32 | reserved | +| 128 | 4 | reloc_table_offset: file-relative; nonzero iff RELOCATABLE | +| 132 | 4 | reloc_count: nonzero iff RELOCATABLE | +| 136 | 4 | heap_hint: bytes of heap the program asks for, multiple of 8; zero when absent; only with RELOCATABLE | +| 140 | 20 | reserved | -Header, tables, string table, nonempty block payloads and optional debug are +Header, tables (including the relocation table), string table, nonempty block payloads and optional debug are pairwise disjoint file ranges wholly inside `file_size`. All start offsets are multiples of 4. They may appear in any order after the header. Padding is zero. Debug is opaque, NOLOAD; DEBUG_PRESENT is set iff debug_size > 0. @@ -142,6 +145,39 @@ lie in initialized RX bytes. Stack top and size MUST be nonzero multiples of 8, without underflow, and the stack MUST NOT overlap any image reservation. The OS zeros the entire stack before use. WAL stack and entry fields are zero. +### 3.1 Relocatable executables + +A WAX with flag RELOCATABLE can be loaded at any address, as in a nommu +system's FLAT format. `base_addr`, block addresses, the entry and the stack are +the **link-time** layout; the loader may place the whole image at +`base_addr + delta` for any `delta` that is a multiple of the maximum block +alignment and keeps every range inside RAM, and then applies the relocation +table. Only WAX can be relocatable. + +The relocation table is `reloc_count` u32 entries at `reloc_table_offset`, +sorted by strictly increasing address. Each entry is a link-time address in +the high 30 bits (4-byte aligned) and a kind in the low 2 bits: + +| Kind | Meaning | +|---:|---| +| 0 | a 32-bit big-endian data word in an R or RW block; add `delta` | +| 1 | an `LDIH` rd, hi followed by `LDIL` rd, lo (same rd) in an RX block; the 32-bit value `hi:lo` is a link-time address; add `delta` and rewrite both immediates | + +Other kinds are reserved and MUST be rejected. Every entry MUST lie wholly in +the initialized bytes of one block of the matching permission. The entry +point, image-relative offsets, and the import slots are relative to the +placed origin and need no entry. Relocation happens before the process runs +and before the stack is cleared; it does not apply to WALs (which are +position independent by construction). + +The stack of a relocatable WAX lies immediately below the lowest block +(`user_stack_top == base_addr`), so the whole reservation stays one +contiguous range `[base_addr - user_stack_size, highest block end)` that +moves as a unit. The loader sets the initial `R30` to the placed origin +(`base_addr + delta`), which lets position-independent start code find the +image. A loader MAY give the process extra zeroed memory above the highest +block for its heap (see `heap_hint`); that memory is not part of the image. + ## 4. Dependencies and symbol tables ### 4.1 Dependency entry (16 bytes) diff --git a/examples/foundation-bmp/README.md b/examples/foundation-bmp/README.md index 5528128..6a50a9c 100644 --- a/examples/foundation-bmp/README.md +++ b/examples/foundation-bmp/README.md @@ -12,4 +12,4 @@ python foundation/boot/build.py target/fb-demo python foundation/run.py --root target/fb-demo/root ``` -シェルで `bmpview /sample.bmp` を実行します(`--root` の中身はディスクのルートに置かれます)。検証は `python -m unittest discover -s foundation/tests/bmpview -v`(`FND_MM=slot` でも)。`/dev/fb0` の仕様は [Foundation API §9.1](../../doc/spec_Foundation_API.md#91-カーネル-vdp-ビットマップドライバ-devfb0) です。 +シェルで `bmpview /sample.bmp` を実行します(`--root` の中身はディスクのルートに置かれます)。検証は `python -m unittest discover -s foundation/tests/bmpview -v`(`FND_MM=nommu` でも)。`/dev/fb0` の仕様は [Foundation API §9.1](../../doc/spec_Foundation_API.md#91-カーネル-vdp-ビットマップドライバ-devfb0) です。 diff --git a/examples/foundation-fb/README.md b/examples/foundation-fb/README.md index 230269c..181d800 100644 --- a/examples/foundation-fb/README.md +++ b/examples/foundation-fb/README.md @@ -16,4 +16,4 @@ APIは ``。通常の `fnd_file_open`、`fnd_write`、`fnd_read openは排他で、複製ハンドルは位置と寿命を共有します。最後のcloseまたはプロセス終了時、表示設定・CLUT・重なったPCG文字領域をopen時へ戻します。描画中のコンソール出力はこの復元で失われるため避けてください。`ST_SYNC` は表示完了を待ちません。 -検証: `python -m unittest discover -s foundation/tests/fb -v`。`FND_MM=slot` でも実行します。仕様は [Foundation API §9.1](../../doc/spec_Foundation_API.md#91-カーネル-vdp-ビットマップドライバ-devfb0)。 +検証: `python -m unittest discover -s foundation/tests/fb -v`。`FND_MM=nommu` でも実行します。仕様は [Foundation API §9.1](../../doc/spec_Foundation_API.md#91-カーネル-vdp-ビットマップドライバ-devfb0)。 diff --git a/foundation/README.md b/foundation/README.md index f49aa9a..eb8106d 100644 --- a/foundation/README.md +++ b/foundation/README.md @@ -44,7 +44,7 @@ fnd_con_write("\033[5;10Hposition\033[K"); /* 5行10桁、行末まで消去 */ カーソル移動・保存/復元、画面/行の消去、16色と高輝度化・反転、カーソル表示切替に対応します。色や解析状態は画面全体で共有されるため、終了前に `\033[0m`、カーソルを隠した場合は `\033[?25h` を送ってください。256色・RGB色、下線・点滅、スクロール領域、代替画面、端末応答は未対応です。詳細は [API §8.1.1](../doc/spec_Foundation_API.md#811-ansi-出力サブセット) を参照してください。 -検証: `python -m unittest discover -s foundation/tests/console -v`。ホスト C コンパイラと、ゲスト検証には Clang・llvmlite・`src32_testbench` が必要です。`FND_MM=slot` でも実行します。 +検証: `python -m unittest discover -s foundation/tests/console -v`。ホスト C コンパイラと、ゲスト検証には Clang・llvmlite・`src32_testbench` が必要です。`FND_MM=nommu` でも実行します。 ## C版 TinyBASIC @@ -59,7 +59,7 @@ fnd_con_write("\033[5;10Hposition\033[K"); /* 5行10桁、行末まで消去 */ - **トラップ**(§3.4): IRQ、SYSCALL、U モードの例外を `kernel_trap` で振り分けます。カーネルは割り込み禁止のまま、カーネルスタック1本で動きます。U モードで fault が起きたら、そのプロセスを終了させます。終了コードは CAUSE を i32 として扱った値で、fault の EPC も記録します。idle で例外が起きた場合、およびカーネル内で例外が起きた場合は `kernel_panic` になります。`kernel_panic` は PCG の文字面へ直接、原因・EPC・tick・実行中のプロセス・最後に保存したレジスタを描いてから UART へ出力し、停止します([仕様 §3.4](../doc/spec_Foundation.md))。 - **スケジューラ**(§10.1): 優先度は DRIVER / SERVICE / APP / IDLE の4段で、同じ優先度の中は FIFO のラウンドロビンです。tick は PeC timer ch0 の 100 Hz(IRQ4、1 tick = 10 ms)、タイムスライス(クオンタム)は 1 tick(10 ms)です。より高い優先度のスレッドが起きたら、カーネルからユーザーへ戻る時点で切り替えます。idle は S モードのスレッドで、`HALT` を繰り返します。 -- **プロセスと保護**(§5): 7つのユーザースロットを使います。別プロセスへ切り替えるときは、§5.3 の順(全 FLAGS=0 → START/END → FLAGS)で8領域を書き換えます。終了時は全スレッドを DEAD にし、付与した RAM をゼロにします。ユーザーポインタは §5.4 の手順で検証します。 +- **プロセスと保護**(§5): プロセスごとに連続したページの範囲(アリーナ)を割り当てます(V のない CPU。V のある CPU ではページ表)。別プロセスへ切り替えるときは、§5.3 の順(全 FLAGS=0 → START/END → FLAGS)で8領域を書き換えます。終了時は全スレッドを DEAD にし、付与した RAM をゼロにします。ユーザーポインタは §5.4 の手順で検証します。 - **システムコール**(§7, §9): `sys_info`(sel 0〜3。sel 3 は Main RAM・VRAM・PCMRAM の容量)、`yield`、`sleep`(`FND_FOREVER` を含む)、`thread_exit`、`proc_exit`、`time_get`、`debug_write` を実装しています。ABI で定義済みでも未実装のものは `FND_E_UNSUPPORTED`、未定義の番号と context byte が 0 でない呼び出しは `FND_E_BAD_CALL` を返します。フェーズ2の部分は次の節です。 ## フェーズ2: ハンドル、IPC、プロセスの起動 @@ -70,7 +70,7 @@ fnd_con_write("\033[5;10Hposition\033[K"); /* 5行10桁、行末まで消去 */ - **同梱モジュール**(§11): `boot/main.c` が FNDBOOT のモジュール表(console、disk、fs、init)を `fnd_proc_spawn_module` で起動します(argv = [名前]、cwd = `/`、役割なし)。 - `thread_create`、`thread_exit`(最後のスレッドの終了でプロセスも終了)を実装しています。まだないもの:`irq_bind`、`irq_ack`(`FND_E_UNSUPPORTED`)。`blk_io` は disk サービス向けに実装済みです。 -`fnd_proc_create_builtin()` は、カーネルイメージにリンクしたユーザーコードからプロセスを作ります。WAX から作る方法は次の節を参照してください。このとき `[code_start, code_end)` が R/X、スロット上端の `ram_size` byte が R/W(スタックとデータ)になります。そのためユーザーコードでは、グローバル変数・文字列リテラル・範囲外の関数呼び出しを使えません(`examples/kernel-trap-c` と同じ制約)。システムコールは `` のマクロで呼ぶので、インライン展開されます。 +`fnd_proc_create_builtin()` は、カーネルイメージにリンクしたユーザーコードからプロセスを作ります。WAX から作る方法は次の節を参照してください。このとき `[code_start, code_end)` が R/X、旧スロット上端の `ram_size` byte が R/W(スタックとデータ)になります。そのためユーザーコードでは、グローバル変数・文字列リテラル・範囲外の関数呼び出しを使えません(`examples/kernel-trap-c` と同じ制約)。システムコールは `` のマクロで呼ぶので、インライン展開されます。 | ファイル | 内容 | |---|---| @@ -79,7 +79,7 @@ fnd_con_write("\033[5;10Hposition\033[K"); /* 5行10桁、行末まで消去 */ | `include/foundation/kernel.h` | カーネルをリンクするイメージ(起動コード・デモ・テスト)向けの S モード API | | `kernel/sched.c` | スレッド、優先度キュー、tick、sleep、idle | | `kernel/proc.c` | プロセス、起動(`proc_spawn`、WAX の読み込み)、`mem_grow`、`sys_info`、終了処理 | -| `kernel/mm.c`, `mm.h` | メモリ管理層。スロット(保護領域の切り替え、ポインタ検証)と仮想記憶(Extension V:フレーム表、ページ表、ユーザーメモリの複写)の 2 つのバックエンド([仕様 §5.5](../doc/spec_Foundation.md)) | +| `kernel/mm.c`, `mm.h` | メモリ管理層。nommu(ページアロケータ、プロセスごとのアリーナ、保護領域の切り替え、ポインタ検証)と仮想記憶(Extension V:フレーム表、ページ表、ユーザーメモリの複写)の 2 つのバックエンド([仕様 §5.5](../doc/spec_Foundation.md)) | | `kernel/trap.c` | `kernel_trap` / `kernel_panic`、IRQ、システムコール | | `kernel/wax.c`, `wax.h` | WAX ローダの本体(検証、依存解決、WAL の配置、領域の計画、束縛) | | `kernel/ipc.c`, `include/foundation/ipc.h` | ハンドル、IPC、サービス名、`wait`、`proc_kill`(§6〜§9) | @@ -105,9 +105,9 @@ fnd_con_write("\033[5;10Hposition\033[K"); /* 5行10桁、行末まで消去 */ ## WAX ローダ -`fnd_proc_spawn_wax(name, priority, image, size, libs, lib_count, grants, grant_count)` は、カーネルメモリ上の WAX を、その `base_addr` を含むユーザースロットに読み込んで起動します([仕様 §9.2・§9.5](../doc/spec_Foundation.md))。 +`fnd_proc_spawn_wax(name, priority, image, size, libs, lib_count, grants, grant_count)` は、カーネルメモリ上の WAX を、アリーナに読み込んで起動します(固定アドレスの画像は `base_addr` を含む範囲を、再配置可能な画像は空きページを取り、再配置して読み込みます)([仕様 §9.2・§9.5](../doc/spec_Foundation.md))。 -- 形式は PROTECTED の [WAX v2](../doc/spec_WAX.md) と [WAX/WAL v3](../doc/spec_WAX_v3.md) です。v3 では、依存する WAL を `libs` から選び(同じ ID と major の中で最大の minor)、スロット内に first fit で置き、import スロットに記述子(アドレスと origin)を書きます。 +- 形式は PROTECTED の [WAX v2](../doc/spec_WAX.md) と [WAX/WAL v3](../doc/spec_WAX_v3.md) です。v3 では、依存する WAL を `libs` から選び(同じ ID と major の中で最大の minor)、アリーナ内に first fit で置き、import スロットに記述子(アドレスと origin)を書きます。 - 保護領域は region 0 がスタック、その後が全画像のブロックをアドレス順に並べたものです。隣接していて権限が同じブロックだけをまとめ、8 を超えると `FND_E_REGION_LIMIT` です。 - `grants` は MMIO の付与(§9.2 の `grants`、§10.3)で、画像の後の region になります(§5.3 の手順 4)。§10.3 の窓に収まらないものは `FND_E_ACCESS`、R(1)と RW(3)以外の perm は `FND_E_BAD_ARG` です。VRAM と PCMRAM は 1 プロセスにしか付与できず、動いている別のプロセスが持っていれば `FND_E_BUSY` です(§10.3 は誤りのコードを定めていないので、カーネルのコードから選んだ)。付与した範囲は §5.4 のポインタ検証の対象外で、終了時にゼロにもしません。 - 起動は原子的です。検証、依存解決、配置、領域とシンボルの確認がすべて通るまで、メモリにもプロセス表にも書きません。 @@ -118,7 +118,7 @@ fnd_con_write("\033[5;10Hposition\033[K"); /* 5行10桁、行末まで消去 */ ## シェルと起動の構成 -起動すると、カーネルが console(スロット 1)、disk(スロット 2)、fs(スロット 3)、init(スロット 0)を起動し、init が `/bin/shell.wax`(スロット 4)を起動します。シェルは [シェル仕様](../doc/spec_Foundation_shell.md) 0.2 のディスパッチャで、`cd`、`set`、`exit`、`help` 以外のコマンドは `PATH`(既定 `/bin`)から `.wax` を探し、`fnd_proc_exec` でスロット 5 に起動して終了を待ちます。 +起動すると、カーネルが console、disk、fs、init を起動し、init が `/bin/shell.wax` を起動します(どれも再配置可能な WAX で、空いているアリーナに置かれます)。シェルは [シェル仕様](../doc/spec_Foundation_shell.md) 0.2 のディスパッチャで、`cd`、`set`、`exit`、`help` 以外のコマンドは `PATH`(既定 `/bin`)から `.wax` を探し、`fnd_proc_exec` で起動して終了を待ちます。 - **libfnd の共有**:シェルと `/bin` のコマンドは、libfnd を WAL「fnd」1.1(`/lib/fnd.1.wal`)として import する WAX v3 です。コマンドは 1 つ約 9 KiB です(静的リンクでは約 38 KiB でした)。init と各サービスは、fs より先にカーネルが起動するので、libfnd を静的にリンクした PROTECTED WAX v2 です。 - **できること**:字句と構文(§3、§4)、変数(§5.1)、外部コマンドの解決と起動(§7。パス指定、`PATH`、大文字小文字を区別しない名前)、リダイレクトの解析と `2>&1`、行編集(§9.2)、エラーメッセージ(§11.1)、fault で終わったプロセスの報告。プロンプトは cwd と `> ` です。 @@ -126,7 +126,7 @@ fnd_con_write("\033[5;10Hposition\033[K"); /* 5行10桁、行末まで消去 */ - **ファイル**:`/bin` と `/lib` はブートディスク(FS2)上のファイルで、fs サービスが `/` として見せます(次の節)。`boot/build.py` が `OUT/root` に書き出し、それをディスクイメージに入れます。 - **起動イメージ**:`boot/main.c` は FNDBOOT(§11)のモジュール表(console、disk、fs、init の順)を spawn します。console の MMIO の付与と各モジュールの `caps` はモジュール表に書いてあります。ディスク起動では stage1 が読み込んだ FNDBOOT を使い、`foundation.wax` では `boot/build.py` がカーネルを含まない FNDBOOT を `0x00200000` の block として足します(stage1 が置く場所と同じ。次の節)。シェルの再起動は init が行うので、モジュールを起動した後は staging 領域を `proc_spawn` が使います。 - **console**:`services/console` が PCG(既定はSCREEN_MODE 1、80×30、8×8。`mode`で切替可能)と PeC のキーボードを MMIO で扱います(API 仕様 §8)。`irq_bind` がないので、キーは IPC の合間と 1 tick(10 ms)ごとに FIFO から読み、256 キーまで先行入力をためます。`CON_READ_KEY` の `NOWAIT`(`fnd_con_pollkey`、`top` が使う)は待たずに答えます。`CON_SET_COLOR` は ANSI 順の16色に対応します。 -- `uname`、`free`、`ps`、`top` と起動時の表示は `sys_info` を使います。`ram_bytes` は BMC が報告する Main RAM の容量(§5.1、報告がなければ 16 MiB)、`free_bytes` は仮想記憶では未使用の物理フレームの合計、スロット方式では実在するユーザースロットのうち動いているプロセスが占めていないものの合計です。12 MiB のスロット方式では、サービス・init・シェル・実行中のコマンドで全スロットを使うため、`free` の `0 KiB free` は正常です。`free` は sel 3 で VRAM と PCMRAM の容量も表示、`boot_drive` はディスク起動では stage1 から渡された drive、`foundation.wax` では 0 です。 +- `uname`、`free`、`ps`、`top` と起動時の表示は `sys_info` を使います。`ram_bytes` は BMC が報告する Main RAM の容量(§5.1、報告がなければ 16 MiB)、`free_bytes` は仮想記憶では未使用の物理フレームの合計、nommu ではページプールの空きページの合計です(アリーナの末尾の余り=ヒープの予備は、プロセスが持っているので含みません)。`free` は sel 3 で VRAM と PCMRAM の容量も表示、`boot_drive` はディスク起動では stage1 から渡された drive、`foundation.wax` では 0 です。 ```sh python foundation/run.py @@ -243,10 +243,10 @@ python -m unittest discover -s foundation/tests/wax -v `tests/wax/main.c` は testbench 上の実行テストです。ローダの境界条件を細かく作るため、`tests/wax/build.py` が `*.asm` を `tools/asm` でアセンブルして v3 の WAX/WAL にし、`v2app.c` を `--protected` でビルドして、全部を `images.h` に書き出します。C から v3 を作る場合のテストは次の節です。 -- `app`/`app2`(スロット5と6):`test.math` と `test.base` を記述子経由で呼びます。モジュール間呼び出しでの R30 の保存と復元、コールバック、64-bit 引数の順序、2 つのスロットに置いた繰り返し import、オブジェクトの import、SYSCALL 後のレジスタを確認し、成功すると 42 で終了します。WAL はプロセスごとに別の基底に置かれ、呼び出し回数のカウンタも別々です。 +- `app`/`app2`(固定アドレス 5 と 6 のスロット相当):`test.math` と `test.base` を記述子経由で呼びます。モジュール間呼び出しでの R30 の保存と復元、コールバック、64-bit 引数の順序、2 つのスロットに置いた繰り返し import、オブジェクトの import、SYSCALL 後のレジスタを確認し、成功すると 42 で終了します。WAL はプロセスごとに別の基底に置かれ、呼び出し回数のカウンタも別々です。 - `poke_write`/`poke_exec`:RX への書き込みと RW の実行が、それぞれ `0x80050002` と `0x80050004` で fault になること。 - `v2app`:C で書いた PROTECTED WAX v2。 -- 失敗する起動(依存がない、minor が古い、PROTECTED でない、壊れた画像、不正な優先度)が何も変えないこと。終了後に領域がゼロになり、同じスロットへ 2 回再起動できること。 +- 失敗する起動(依存がない、minor が古い、PROTECTED でない、壊れた画像、不正な優先度)が何も変えないこと。終了後に領域がゼロになり、同じ範囲へ 2 回再起動できること。再配置可能な画像(`pie`)を 3 つ同時に起動して別々のアリーナに置かれ、終了後にアリーナがゼロになりプールへ戻ること。 - `spin` は CPU を使い続けます。`src32_testbench` は idle スレッドの `HALT` をプログラムの終了とみなすためです。 ```sh @@ -254,7 +254,7 @@ python foundation/tests/wax/build.py target/foundation-wax python tools/llvm-src32/driver.py foundation/tests/wax/main.c foundation/kernel/sched.c \ foundation/kernel/proc.c foundation/kernel/mm.c foundation/kernel/trap.c foundation/kernel/ipc.c foundation/kernel/fb.c foundation/kernel/sgu.c foundation/kernel/sgc.c foundation/kernel/blk.c foundation/kernel/wax.c \ -I sdk/cpt32/include -I foundation/include -I target/foundation-wax \ - --origin 0x10000 --stack-top 0x100000 -D FND_MM_SLOT_ONLY -O1 -o target/foundation-wax/test.wax + --origin 0x10000 --stack-top 0x100000 -D FND_MM_NOMMU_ONLY -O1 -o target/foundation-wax/test.wax src32_testbench /absolute/path/target/foundation-wax/test.wax 42 --cpu pipeline --frames 90 ``` @@ -282,14 +282,14 @@ src32_testbench /absolute/path/target/foundation-wax/test.wax 42 --cpu pipeline - 出力の見積もりは `cpt_code_bound`。heap・libfnd・作業領域は使いません。 - テストは `python3 -m unittest foundation.tests.code.test_code`(ホストは Clang、Foundation 上は `src32_testbench` も必要)です。 -`tests/wal/` は、`tools/llvm-src32` の `--wal`、`--wax3`、`--import-wal`([ツールチェーンの README](../tools/llvm-src32/README.md#wax-v3--wal))で C から作った画像を、カーネルで実行します。`build.py` が `test.base`(WAL)、それを import する `test.math`(WAL、1.2)、両方を import する `test.app`(WAX v3、スロット5と6)を作り、`images.h` に書き出します。`app.c` は、WAL どうしの呼び出し、WAL 内部の関数ポインタ、WAL の静的データがプロセスごとに別であること、オブジェクトの import、文字列定数のアドレス、64-bit の引数と戻り値を確認し、すべて通れば 42 で終了します(起動コードが `proc_exit` を呼ぶ)。 +`tests/wal/` は、`tools/llvm-src32` の `--wal`、`--wax3`、`--import-wal`([ツールチェーンの README](../tools/llvm-src32/README.md#wax-v3--wal))で C から作った画像を、カーネルで実行します。`build.py` が `test.base`(WAL)、それを import する `test.math`(WAL、1.2)、両方を import する `test.app`(WAX v3、固定アドレス 5 と 6 のスロット相当)を作り、`images.h` に書き出します。`app.c` は、WAL どうしの呼び出し、WAL 内部の関数ポインタ、WAL の静的データがプロセスごとに別であること、オブジェクトの import、文字列定数のアドレス、64-bit の引数と戻り値を確認し、すべて通れば 42 で終了します(起動コードが `proc_exit` を呼ぶ)。 ```sh python foundation/tests/wal/build.py target/foundation-wal python tools/llvm-src32/driver.py foundation/tests/wal/main.c foundation/kernel/sched.c \ foundation/kernel/proc.c foundation/kernel/mm.c foundation/kernel/trap.c foundation/kernel/ipc.c foundation/kernel/fb.c foundation/kernel/sgu.c foundation/kernel/sgc.c foundation/kernel/blk.c foundation/kernel/wax.c \ -I sdk/cpt32/include -I foundation/include -I target/foundation-wal \ - --origin 0x10000 --stack-top 0x100000 -D FND_MM_SLOT_ONLY -O1 -o target/foundation-wal/test.wax + --origin 0x10000 --stack-top 0x100000 -D FND_MM_NOMMU_ONLY -O1 -o target/foundation-wal/test.wax src32_testbench /absolute/path/target/foundation-wal/test.wax 42 --cpu pipeline --frames 120 ``` @@ -297,23 +297,23 @@ src32_testbench /absolute/path/target/foundation-wal/test.wax 42 --cpu pipeline ### 仮想記憶のテスト -カーネルは `CPUID` で Extension V を見つけると、スロットの代わりにページ表を使います([仕様 §5.5](../doc/spec_Foundation.md)、設計は [foundation_vm_plan.md](../doc/foundation_vm_plan.md))。V のない CPU を想定してスロット方式で動かすには、カーネルを `-D FND_MM_SLOT_ONLY` でビルドします(`boot/build.py` は環境変数 `FND_MM=slot`)。同じソースの 2 つのバックエンドを、次のように確かめます。 +カーネルは `CPUID` で Extension V を見つけると、nommu のアリーナの代わりにページ表を使います([仕様 §5.5](../doc/spec_Foundation.md)、設計は [foundation_vm_plan.md](../doc/foundation_vm_plan.md))。V のない CPU を想定して nommu(アリーナ方式)で動かすには、カーネルを `-D FND_MM_NOMMU_ONLY` でビルドします(`boot/build.py` は環境変数 `FND_MM=nommu`)。同じソースの 2 つのバックエンドを、次のように確かめます。 -- `tests/phase1`、`tests/wax`、`tests/wal` は物理スロットの中身を直接見るので、`FND_MM_SLOT_ONLY` でビルドします。 -- `tests/shell`、`tests/disk`、`tests/lzma` など、起動イメージを丸ごと動かすテストは、どちらのモードでも通ります。`tests/shell` は実際に使う testbench と CPU エンジンで `cpu_features.a` の CPUID を実行し、V がなくてもスロット方式の期待値を使います。`FND_MM=slot` は CPU の V の有無にかかわらずスロット方式です。仮想記憶では空き容量が正であることを検証し、スロット方式では正確な値(12 MiB 構成では 0 KiB)を比較します。 +- `tests/phase1`、`tests/wax`、`tests/wal` は物理メモリの中身を直接見るので、`FND_MM_NOMMU_ONLY` でビルドします。 +- `tests/shell`、`tests/disk`、`tests/lzma` など、起動イメージを丸ごと動かすテストは、どちらのモードでも通ります。`tests/shell` は実際に使う testbench と CPU エンジンで `cpu_features.a` の CPUID を実行し、free の空き容量は、どちらのモードでも値を N に正規化して比べます。`FND_MM=nommu` は CPU の V の有無にかかわらず nommu です。 `tests/shell` は `SRC32_TESTBENCH` があればその実行ファイル、なければ `target/release`、`target/debug` の順で testbench を探します。古い release 版が残っていると新しい debug 版より優先されます。仮想記憶を検証するときは `cargo build --release --bin src32_testbench` で更新するか、`SRC32_TESTBENCH` に V 対応のビルドの絶対パスを指定してください。 ```sh python -m unittest discover -s foundation/tests/shell # 仮想記憶(V のある testbench) -FND_MM=slot python -m unittest discover -s foundation/tests/shell # スロット +FND_MM=nommu python -m unittest discover -s foundation/tests/shell # nommu(アリーナ) ``` -`tests/vm/main.c` は testbench 上の実行テストで、`tests/wax/build.py` の画像を使います。同じアドレスにリンクした `app` を 2 つ同時に起動してどちらも 42 で終了すること(スロットでは `FND_E_ADDRESS_CONFLICT`)、RX への書き込みと RW の実行がページフォルト(`0x8006000A`、`0x8006000C`)で終わること、拒否された起動と終了したプロセスがフレームを一つも残さないこと(未使用フレームの合計が起動前と一致)を確認します。`tools/llvm-src32/test_toolchain.py` の `test_foundation_vm_kernel` が `-O0`/`-O1`/`-O2` × 4 種の CPU で実行します。 +`tests/vm/main.c` は testbench 上の実行テストで、`tests/wax/build.py` の画像を使います。同じアドレスにリンクした `app` を 2 つ同時に起動してどちらも 42 で終了すること(nommu のアリーナでは、固定アドレスの画像どうしは `FND_E_ADDRESS_CONFLICT`。再配置可能な画像は別の場所に置かれます)、RX への書き込みと RW の実行がページフォルト(`0x8006000A`、`0x8006000C`)で終わること、拒否された起動と終了したプロセスがフレームを一つも残さないこと(未使用フレームの合計が起動前と一致)を確認します。`tools/llvm-src32/test_toolchain.py` の `test_foundation_vm_kernel` が `-O0`/`-O1`/`-O2` × 4 種の CPU で実行します。 ## fs サービス -`fs/`(スロット 3)は FS2([仕様](../doc/spec_FS2.md))を `/` として見せる fs サービスです。disk サービス(`services/disk/`、スロット 2)の IPC を通して、カーネルの `blk_io`(IDC + DMAC)でディスクを読み書きします。 +`fs/`は FS2([仕様](../doc/spec_FS2.md))を `/` として見せる fs サービスです。disk サービス(`services/disk/`)の IPC を通して、カーネルの `blk_io`(IDC + DMAC)でディスクを読み書きします。 - **VFS**:`/` が FS2、`/tmp` と `/dev`(`/dev/con`、`/dev/null`、`/dev/fb0`、`/dev/sgu`、`/dev/sgc`、`/dev/trng`、`/dev/prng`、`/dev/uart0`)がメモリ上のノードです。`/dd/N` は dsfs が受け持ちます。`/dev` のノードでは TRUNC と APPEND を無視するので、`> /dev/null` が使えます。 - **VDP bitmap**:`/dev/fb0` はカーネルドライバです。通常のopen/read/writeと `` で、320×240のINDEX8/RGB555/RGB888と全256色のCLUTを操作できます。openは排他で、最後のclose・プロセス終了時に表示設定・CLUT・重なったPCG文字領域を復元します。[実行例](../examples/foundation-fb/README.md)、[BMP 表示 `bmpview`](../examples/foundation-bmp/README.md)、[API仕様 §9.1](../doc/spec_Foundation_API.md#91-カーネル-vdp-ビットマップドライバ-devfb0)。 @@ -324,9 +324,9 @@ FND_MM=slot python -m unittest discover -s foundation/tests/shell # スロッ ## 注意と未決事項 - `src32_testbench` には UART がありません。そのため、そこで `debug_write` に 1 byte 以上を渡したり `kernel_panic` が UART へ出力したりすると、カーネル内でバスエラーになります(`kernel_panic` は PCG へ先に描くので画面には残り、再入は黙って停止します)。エミュレーター本体では UART に出力されます。 -- RAM 容量の取得方法(§4.1)はまだ決めていません。スロットは既定の 16 MiB を前提にしています。 +- RAM 容量の取得方法(§4.1)はまだ決めていません。nommu のページプールは [0x400000, RAM 末尾)、最大 256 MiB です。 - 仮想記憶では、権限はページ単位です。Foundation のイメージは `--page-align` でコード(RX)とデータ(RW)を別ページに置くので W^X です(`.rodata` は RW データの中で、R 専用ではありません)。MMIO の付与はページ単位で、窓の検査は byte 単位に加えてページ範囲が届く先も検査しますが、同じページにある別のレジスタには触れます([仕様 §5.5](../doc/spec_Foundation.md))。 -- 仮想記憶でもプロセスは 7 個、スレッドは 8 個までです。スロットの数に縛られなくなったので、`FND_MAX_PROCS` を増やせます。 +- プロセスは(仮想記憶でも nommu でも)7 個、スレッドは 8 個までです。スロットにもアリーナにも縛られないので、`FND_MAX_PROCS` を増やせます。 - スレッドは全体で `FND_MAX_THREADS`(8)個です。`irq_bind` はまだありません。 - `--protected` の C プログラムでは、アセンブラが `.ORG` で分けたブロックのうち、同じ権限で並ぶものの隙間(整列の埋め草)をゼロで埋めて 1 つにします(PROTECTED WAX は 7 ブロックまで)。ゼロ初期化のグローバルもイメージに含まれるので、シェルのイメージは約 230 KiB です。 - WAX の読み込み中は割り込み禁止です。`tests/wax` の起動 11 回(うち 5 回は失敗)は、-O0 の Scalar で約 16M サイクル(1 回あたり約 30 tick 分)かかります。-O2 では約 10M サイクルです。 @@ -337,4 +337,4 @@ FND_MM=slot python -m unittest discover -s foundation/tests/shell # スロッ `/dev/trng` は READ 専用で、ハードウェアの READY が落ちている間は呼び出したスレッドをブロックします。`/dev/prng` は READ 専用のノンブロッキング GFSR 乱数です(暗号用途向けではありません)。`/dev/uart0` はホスト標準入出力へつながるバイトストリームで、読み出しは受信待ち、書き込みはデータをそのまま送信します。通常の `fnd_file_open`/`fnd_read`/`fnd_write` で利用できます。複数 open は同じハードウェアを共有します。乱数列の seed 設定 API は公開しません。[API §9.4](../doc/spec_Foundation_API.md#94-pec-乱数uart-devtrngdevprngdevuart0)。 -検証: `python -m unittest discover -s foundation/tests/pec -v`。ゲスト検証は `FND_MM=slot` でも実行してください。testbench には UART がないため、UART の正長入出力と TRNG の未準備からの復帰は、実ドライバと IPC ディスパッチを制御可能な MMIO に接続するホストテストで確認します。ゲスト側では RNG の 4096 bytes をまたぐ read、短い末尾、VFS とハンドル転送、権限、stat、複数 open を確認します。 +検証: `python -m unittest discover -s foundation/tests/pec -v`。ゲスト検証は `FND_MM=nommu` でも実行してください。testbench には UART がないため、UART の正長入出力と TRNG の未準備からの復帰は、実ドライバと IPC ディスパッチを制御可能な MMIO に接続するホストテストで確認します。ゲスト側では RNG の 4096 bytes をまたぐ read、短い末尾、VFS とハンドル転送、権限、stat、複数 open を確認します。 diff --git a/foundation/boot/build.py b/foundation/boot/build.py index 6c8fde0..9d61cf0 100644 --- a/foundation/boot/build.py +++ b/foundation/boot/build.py @@ -3,26 +3,28 @@ python foundation/boot/build.py OUT [--clang PATH] [-O0|-O1|-O2] [--stop-ticks N] [--root DIR] [--disk-blocks N] -The programs, by slot (doc/spec_Foundation.md §5.2): +The programs (doc/spec_Foundation.md §5.2). Every one is a relocatable WAX v3 +(driver --pie, doc/spec_WAX_v3.md §3.1), which the kernel loads into any free +RAM, so none of them has an address of its own: - slot 0 init starts /bin/shell.wax (services/init) - slot 1 console the console service (services/console) - slot 2 disk the disk service, blk_io for fs (services/disk) - slot 3 fs the fs service (fs/) - slot 4 shell /bin/shell.wax (doc/spec_Foundation_shell.md) - slot 5 /bin/* the standard commands (bin/, §10.3 of the shell spec) + init starts /bin/shell.wax (services/init) + console the console service (services/console) + disk the disk service, blk_io for fs (services/disk) + fs the fs service (fs/) + shell /bin/shell.wax (doc/spec_Foundation_shell.md) + /bin/* the standard commands (bin/, §10.3 of the shell spec) libfnd is the WAL "fnd" 1.1 (/lib/fnd.1.wal, exports in lib/fnd/fnd.exports), and the shell and the commands are WAX v3 that import it; fnd_proc_exec reads it from /lib when it starts them (§9.5). init and the services are -FNDBOOT modules (§11), PROTECTED WAX v2 with libfnd linked in, since the -kernel starts them before there is an fs. +FNDBOOT modules (§11) with libfnd linked in, since the kernel starts them +before there is an fs. -The four modules (slots 0 to 3) go into FNDBOOT (§11); /bin and /lib are +The four modules (init, console, disk, fs) go into FNDBOOT (§11); /bin and /lib are files on the boot disk, which fs serves as "/". -Environment: FND_MM=slot builds the kernel for fixed slots only, which is -what a CPU without Extension V runs; otherwise it uses virtual memory when +Environment: FND_MM=nommu builds the kernel for physical memory only, which +is what a CPU without Extension V runs; otherwise it uses virtual memory when CPUID reports V (doc/foundation_vm_plan.md). Outputs: @@ -59,13 +61,6 @@ import fs2 # noqa: E402 import wax # noqa: E402 -# (origin, stack top) of the user slots used here. -SLOT0 = (0x00400000, 0x00480000) -SLOT1 = (0x00480000, 0x00500000) -SLOT2 = (0x00500000, 0x00580000) -SLOT3 = (0x00580000, 0x00600000) -SLOT4 = (0x00600000, 0x00800000) -SLOT5 = (0x00800000, 0x00C00000) STACK = 0x4000 FS_STACK = 0x8000 FND_PRIO_SERVICE = 1 @@ -116,20 +111,20 @@ def driver(sources, output, args, *extra): return output -def layout(slot, stack=STACK): - origin, stack_top = slot - return ["--origin", hex(origin), "--stack-top", hex(stack_top), "--user-stack-size", hex(stack)] +def layout(stack=STACK, heap=0): + return ["--pie", "--user-stack-size", hex(stack), *(["--heap-hint", hex(heap)] if heap else [])] -def static(sources, output, args, slot, *extra, stack=STACK): - """A PROTECTED WAX v2 with libfnd linked in.""" - return driver([*sources, *LIBFND], output, args, "--protected", "--page-align", *layout(slot, stack), *extra) +def static(sources, output, args, *extra, stack=STACK, heap=0): + """A WAX v3 with libfnd linked in (no imports).""" + return driver([*sources, *LIBFND], output, args, "--wax3", "--page-align", *layout(stack, heap), *extra) -def program(sources, output, args, slot, image_id, fnd, *libs): +def program(sources, output, args, image_id, fnd, *libs, heap=0): """A WAX v3 that imports the WAL fnd, and any other WALs in libs.""" imports = [arg for lib in (fnd, *libs) for arg in ("--import-wal", lib)] - return driver(sources, output, args, "--wax3", "--page-align", "--image-id", image_id, *imports, *layout(slot)) + return driver(sources, output, args, "--wax3", "--page-align", "--image-id", image_id, *imports, + *layout(heap=heap)) def embed(name, data): @@ -213,10 +208,10 @@ def main(argv=None): kernel_args = ["--origin", hex(KERNEL_LOAD), "--stack-top", hex(KERNEL_STACK_TOP)] if args.stop_ticks is not None: kernel_args += ["-D", f"FOUNDATION_BOOT_STOP_TICKS={args.stop_ticks}u"] - if os.environ.get("FND_MM") == "slot": - # Ignore Extension V and run on fixed slots even on a CPU that has it + if os.environ.get("FND_MM") in ("nommu", "slot"): + # Ignore Extension V and run on physical memory even on a CPU that has it # (doc/foundation_vm_plan.md); the tests use this to check both backends. - kernel_args += ["-D", "FND_MM_SLOT_ONLY"] + kernel_args += ["-D", "FND_MM_NOMMU_ONLY"] image_job = pool.submit(driver, [HERE / "main.c", *KERNEL], out / "foundation.wax", args, "-D", "FOUNDATION_BOOT_EMBEDDED", *kernel_args) kernel_job = pool.submit(driver, [HERE / "main.c", *KERNEL], out / "kernel.bin", args, *kernel_args) @@ -226,17 +221,17 @@ def main(argv=None): dict(name="console", priority=FND_PRIO_SERVICE, caps=FND_CAP_SVC, grants=console_grants(), image=pool.submit(static, [services / "console/main.c", services / "console/con_pcg.c", services / "console/con_wide.c"], - out / "console.wax", args, SLOT1, "-I", str(services / "console"), + out / "console.wax", args, "-I", str(services / "console"), "-I", str(wide_font.write_header(out / "console/con_wide_font.h").parent))), dict(name="disk", priority=FND_PRIO_SERVICE, caps=FND_CAP_DISK | FND_CAP_SVC, image=pool.submit(static, [services / "disk/main.c", services / "disk/cache.c"], - out / "disk.wax", args, SLOT2)), + out / "disk.wax", args)), dict(name="fs", priority=FND_PRIO_SERVICE, caps=FND_CAP_SVC, - image=pool.submit(static, [*FS, FOUNDATION / "lib/fnd/name.c"], out / "fs.wax", args, SLOT3, + image=pool.submit(static, [*FS, FOUNDATION / "lib/fnd/name.c"], out / "fs.wax", args, f"-DFS2_FULL_MOUNT_CHECK={FS2_FULL_MOUNT_CHECK}", "-I", str(FOUNDATION / "fs"), stack=FS_STACK)), dict(name="init", priority=FND_PRIO_SERVICE, caps=0, - image=pool.submit(static, [services / "init/main.c", LIBFND_PROC], out / "init.wax", args, SLOT0)), + image=pool.submit(static, [services / "init/main.c", LIBFND_PROC], out / "init.wax", args)), ] fnd = fnd_job.result() @@ -245,7 +240,7 @@ def main(argv=None): sgc_build = importlib.util.module_from_spec(sgc_spec) sgc_spec.loader.exec_module(sgc_build) sgc_job = pool.submit(sgc_build.build, out / "sgc", fnd, args.clang, args.optimization) - program_jobs = [pool.submit(program, [FOUNDATION / "shell/shell.c"], root / "bin/shell.wax", args, SLOT4, + program_jobs = [pool.submit(program, [FOUNDATION / "shell/shell.c"], root / "bin/shell.wax", args, "foundation.shell", fnd)] # The commands that import libsgc go last, so the others start while it builds. for name in sorted(COMMANDS, key=lambda name: name in SGC_COMMANDS): @@ -254,7 +249,7 @@ def main(argv=None): if name == "sgubplay": sources.append(ROOT / "sdk/cpt32/src/sgub.c") program_jobs.append(pool.submit(program, sources, - root / "bin" / f"{name}.wax", args, SLOT5, f"foundation.{name}", fnd, *libs)) + root / "bin" / f"{name}.wax", args, f"foundation.{name}", fnd, *libs)) shutil.copyfile(lzma_job.result(), root / "lib/lzma.1.wal") shutil.copyfile(sgc_job.result(), root / "lib/sgc.1.wal") shutil.copyfile(rle_job.result(), root / "lib/rle.1.wal") diff --git a/foundation/boot/main.c b/foundation/boot/main.c index dd09b6b..08c0f2f 100644 --- a/foundation/boot/main.c +++ b/foundation/boot/main.c @@ -1,10 +1,10 @@ /* Foundation kernel entry: spawns the FNDBOOT modules (doc/spec_Foundation.md * §11) in table order, init last. build.py packs * - * console slot 1, SERVICE, SVC, MMIO grants for the PCG and PeC input - * disk slot 2, SERVICE, DISK | SVC (blk_io for fs) - * fs slot 3, SERVICE, SVC (FS2 on the boot drive, /dd, /dev, /tmp) - * init slot 0, SERVICE, starts /bin/shell.wax and restarts it + * console SERVICE, SVC, MMIO grants for the PCG and PeC input + * disk SERVICE, DISK | SVC (blk_io for fs) + * fs SERVICE, SVC (FS2 on the boot drive, /dd, /dev, /tmp) + * init SERVICE, starts /bin/shell.wax and restarts it * * Disk boot: stage1 enters with R1 = boot drive and R2 = the FNDBOOT image * staged at 0x00200000. FOUNDATION_BOOT_EMBEDDED builds a WAX that the host diff --git a/foundation/include/foundation/kernel.h b/foundation/include/foundation/kernel.h index e113dc3..f5daad2 100644 --- a/foundation/include/foundation/kernel.h +++ b/foundation/include/foundation/kernel.h @@ -9,19 +9,21 @@ /* Supervisor-side interface of the kernel, for the image that links * it (boot code, demo, tests). User programs use . */ -#define FND_MAX_PROCS 7u /* one per user slot */ +#define FND_MAX_PROCS 7u /* process records; no longer tied to memory slots */ #define FND_MAX_THREADS 8u /* user threads; the idle thread is extra */ -#define FND_REGIONS 8u /* Extension P regions: the slot backend's limit */ +#define FND_REGIONS 8u /* Extension P regions: the nommu backend's limit */ /* The regions a process record holds: an image's (at most 8) and the MMIO - * grants (at most FND_MAX_GRANTS). The slot backend keeps all in the 8 CPU + * grants (at most FND_MAX_GRANTS). The nommu backend keeps all in the 8 CPU * regions; the vm backend only records them. */ #define FND_PROC_REGIONS 16u #define FND_TICK_HZ 100u #define FND_TICK_CYCLES (48000000u / FND_TICK_HZ) /* PeC timer ch0 RELOAD at 48 MHz */ #define FND_SLICE_TICKS 1u /* 10 ms, one scheduler tick */ -/* User slots (§5.2): the slot backend's fixed physical areas. With - * Extension V the same numbers are only link-time addresses. */ +/* The fixed ranges of the old slot layout. Only processes that run code + * linked into the kernel image (fnd_proc_create_builtin) and tests that link + * executables to fixed addresses still use them; the nommu backend has no + * slots (§5.2) and takes any free range. */ #define FND_SLOTS 7u uint32_t fnd_slot_base(uint32_t slot); uint32_t fnd_slot_end(uint32_t slot); @@ -45,7 +47,7 @@ struct fnd_proc { uint32_t state; uint32_t pid; uint32_t parent; /* pid of the spawner, 0 for the kernel */ - uint32_t slot; + uint32_t arena_lo, arena_hi; /* nommu backend: the pages it owns (§5.2), else 0 */ uint32_t priority; uint32_t caps; /* FND_CAP_* (§9.1) */ uint32_t threads; /* live threads */ @@ -64,8 +66,8 @@ struct fnd_proc { }; /* A process that runs user code linked into the kernel image. [code_start, code_end) is granted R/X, and the top ram_size - * bytes of the slot ([fnd_slot_end(slot) - ram_size, fnd_slot_end(slot)), - * zeroed) R/W for stack and data. The thread starts at entry in U mode with + * bytes of the old slot range ([fnd_slot_end(slot) - ram_size, fnd_slot_end(slot)), + * zeroed, claimed as the arena) R/W for stack and data. The thread starts at entry in U mode with * USP = slot end - 8, where the argument word arg is stored (C stack ABI: * first argument at SP), UFP = 0, UGP = slot base. Returns the pid (>= 1) or * a negative FND_E_* code. */ @@ -86,16 +88,16 @@ struct fnd_grant { }; #define FND_MAX_GRANTS 8u -/* Loads a PROTECTED WAX into the user slot that contains its base_addr and - * starts it as §9.2 describes: v2, or v3 with the WAL graph it needs, chosen +/* Loads a PROTECTED WAX into free RAM (the pages it reserves, or anywhere + * for a relocatable one) and starts it as §9.2 describes: v2, or v3 with the WAL graph it needs, chosen * from libs (doc/spec_WAX_v3.md §4.1, §7). The image and the libraries must * stay unchanged during the call; the kernel does not copy them first. * grants become the last regions (§5.3 step 4); a grant outside the §10.3 * windows is FND_E_ACCESS, and VRAM or PCMRAM already granted to a live * process is FND_E_BUSY. Atomic: on failure nothing is allocated or written. * Returns the pid (>= 1) or FND_E_FORMAT, FND_E_UNSUPPORTED, - * FND_E_DEPENDENCY, FND_E_REGION_LIMIT, FND_E_ADDRESS_CONFLICT (slot in - * use), FND_E_LIMIT, FND_E_ACCESS, FND_E_BUSY or FND_E_BAD_ARG. */ + * FND_E_DEPENDENCY, FND_E_REGION_LIMIT, FND_E_ADDRESS_CONFLICT (its fixed + * range is taken), FND_E_NO_MEMORY, FND_E_LIMIT, FND_E_ACCESS, FND_E_BUSY or FND_E_BAD_ARG. */ int32_t fnd_proc_spawn_wax(const char *name, uint32_t priority, const void *image, uint32_t size, const struct fnd_image *libs, uint32_t lib_count, const struct fnd_grant *grants, uint32_t grant_count); diff --git a/foundation/kernel/mm.c b/foundation/kernel/mm.c index 21939f9..62ec9ec 100644 --- a/foundation/kernel/mm.c +++ b/foundation/kernel/mm.c @@ -1,7 +1,10 @@ /* Memory management backends (doc/foundation_vm_plan.md); the interface is - * mm.h. The slot backend programs the Extension P protection regions - * (spec_Foundation.md §5.3). The vm backend keeps a page table per process for - * Extension V (doc/spec_CPU_vm.md) and a frame pool above the kernel area. */ + * mm.h. The nommu backend (no Extension V) works like Linux without an MMU: + * a process gets one contiguous arena of RAM from a page allocator, and the + * Extension P protection regions (spec_Foundation.md §5.3) keep it inside + * it. The vm backend keeps a page table per process for Extension V + * (doc/spec_CPU_vm.md) and a frame pool above the kernel area. Both draw on + * the one page bitmap below. */ #include "internal.h" #include @@ -46,7 +49,104 @@ uint32_t mm_ram_bytes(void) { return bytes ? bytes : RAM_BYTES_FALLBACK; } -/* ---- slot backend ---- */ +/* ---- the page pool of both backends ---- */ + +/* Pages come from [POOL_BASE, installed RAM): the area below holds the + * kernel, its heap and the staged boot image (§5.1), which the processes + * never see. 64K pages are 256 MiB. */ +#define POOL_BASE MM_USER_BASE +#define MAX_FRAMES 65536u + +static uint32_t frame_used[MAX_FRAMES / 32u]; /* bit set: handed out */ +static uint32_t pool_frames, free_frames, next_hint; + +static void pool_init(uint32_t reserved) { + uint32_t ram = mm_ram_bytes(); + pool_frames = ram > POOL_BASE ? (ram - POOL_BASE) / MM_PAGE : 0; + if (pool_frames > MAX_FRAMES) + pool_frames = MAX_FRAMES; + free_frames = pool_frames; + for (uint32_t i = 0; i < MAX_FRAMES / 32u; ++i) + frame_used[i] = 0; + for (uint32_t f = 0; f < reserved && f < pool_frames; ++f) { + frame_used[f >> 5] |= 1u << (f & 31u); + --free_frames; + } + next_hint = reserved; +} + +static int frame_taken(uint32_t f) { return (frame_used[f >> 5] >> (f & 31u)) & 1u; } + +/* ---- nommu backend: the arena allocator ---- */ + +static uint32_t first_page(uint32_t addr) { return (addr - POOL_BASE) / MM_PAGE; } + +/* Whether the pages of [lo, hi) all exist and are free. */ +static int range_free(uint32_t lo, uint32_t hi) { + if (lo < POOL_BASE || hi <= lo || (hi - POOL_BASE) / MM_PAGE > pool_frames || ((lo | hi) & MM_PAGE_MASK)) + return 0; + for (uint32_t f = first_page(lo); f < first_page(hi); ++f) { + if (frame_taken(f)) + return 0; + } + return 1; +} + +static void range_take(uint32_t lo, uint32_t hi) { + for (uint32_t f = first_page(lo); f < first_page(hi); ++f) + frame_used[f >> 5] |= 1u << (f & 31u); + free_frames -= (hi - lo) / MM_PAGE; +} + +int32_t mm_arena_claim(uint32_t lo, uint32_t hi) { + lo &= ~MM_PAGE_MASK; + hi = (hi + MM_PAGE_MASK) & ~MM_PAGE_MASK; + if (lo < POOL_BASE || hi <= lo || (hi - POOL_BASE) / MM_PAGE > pool_frames) + return FND_E_NO_MEMORY; /* outside the installed RAM or the user area */ + if (!range_free(lo, hi)) + return FND_E_ADDRESS_CONFLICT; + range_take(lo, hi); + return FND_OK; +} + +uint32_t mm_arena_alloc(uint32_t bytes) { + uint32_t pages = (bytes + MM_PAGE_MASK) / MM_PAGE; + if (pages == 0 || pages > free_frames) + return 0; + /* First fit from the bottom. */ + uint32_t run = 0; + for (uint32_t f = 0; f < pool_frames; ++f) { + run = frame_taken(f) ? 0 : run + 1u; + if (run == pages) { + uint32_t lo = POOL_BASE + (f + 1u - pages) * MM_PAGE; + range_take(lo, lo + pages * MM_PAGE); + return lo; + } + } + return 0; +} + +void mm_arena_free(uint32_t lo, uint32_t hi) { + if (lo < POOL_BASE || hi <= lo) + return; + for (uint32_t f = first_page(lo); f < first_page((hi + MM_PAGE_MASK) & ~MM_PAGE_MASK); ++f) { + if (frame_taken(f)) { + frame_used[f >> 5] &= ~(1u << (f & 31u)); + ++free_frames; + } + } +} + +int mm_arena_extend(uint32_t hi, uint32_t new_hi) { + if (new_hi <= hi || ((hi | new_hi) & MM_PAGE_MASK) || !range_free(hi, new_hi)) + return 0; + range_take(hi, new_hi); + return 1; +} + +uint32_t mm_arena_free_bytes(void) { return free_frames * MM_PAGE; } + +/* ---- nommu backend: the protection regions ---- */ static struct fnd_proc *active; /* process whose memory the CPU enforces */ static uint32_t active_regions; /* regions with nonzero FLAGS on the CPU */ @@ -123,12 +223,6 @@ static int slot_range_ok(const struct fnd_proc *proc, uint32_t addr, uint32_t le /* ---- vm backend ---- */ -/* Frames come from [POOL_BASE, installed RAM): the area below holds the - * kernel, its heap and the staged boot image (§5.1), which the processes - * never see. 64K frames are 256 MiB. */ -#define POOL_BASE 0x00400000u -#define MAX_FRAMES 65536u - #define PTE_V 0x01u #define PTE_R 0x02u #define PTE_W 0x04u @@ -139,25 +233,9 @@ static int slot_range_ok(const struct fnd_proc *proc, uint32_t addr, uint32_t le #define PTE_FRAME 0xFFFFF000u #define PTE_RWX (PTE_R | PTE_W | PTE_X) -static uint32_t frame_used[MAX_FRAMES / 32u]; /* bit set: handed out */ -static uint32_t pool_frames, free_frames, next_hint; - static void vm_init(void) { - uint32_t ram = mm_ram_bytes(); - pool_frames = ram > POOL_BASE ? (ram - POOL_BASE) / MM_PAGE : 0; - if (pool_frames > MAX_FRAMES) - pool_frames = MAX_FRAMES; - free_frames = pool_frames; - next_hint = 0; - for (uint32_t i = 0; i < MAX_FRAMES / 32u; ++i) - frame_used[i] = 0; /* The first frames are the DMA bounce buffer for good. */ - uint32_t reserved = MM_BOUNCE_BYTES / MM_PAGE; - for (uint32_t f = 0; f < reserved && f < pool_frames; ++f) { - frame_used[f >> 5] |= 1u << (f & 31u); - --free_frames; - } - next_hint = reserved; + pool_init(MM_BOUNCE_BYTES / MM_PAGE); } uint32_t mm_bounce(void) { return POOL_BASE; } @@ -363,14 +441,16 @@ void mm_init(void) { if (mm_decided) return; mm_decided = 1; -#ifndef FND_MM_SLOT_ONLY +#ifndef FND_MM_NOMMU_ONLY if (cpt32_cpu_features() & CPT32_FEATURE_V) { vm_init(); mm_vm = 1; /* U mode is translated from now on; S mode never is. */ cpt32_mtsr(CPT32_SR_VCTL, CPT32_VCTL_EN); + return; } #endif + pool_init(0); } void proc_activate(struct fnd_proc *proc) { diff --git a/foundation/kernel/mm.h b/foundation/kernel/mm.h index 341d587..84aa8c2 100644 --- a/foundation/kernel/mm.h +++ b/foundation/kernel/mm.h @@ -7,15 +7,17 @@ * Two backends sit behind this interface and the kernel picks one at its first * process start from CPUID (CPU_FEATURES bit 9, Extension V): * - * slot No V. Every process owns a fixed physical slot (§5.2) and the CPU - * checks it with the Extension P protection regions (§5.3). + * nommu No V, like Linux without an MMU. Addresses are physical. Every + * process owns one contiguous arena that a page allocator hands out + * (§5.2) and the CPU checks it with the Extension P protection + * regions (§5.3). Relocatable executables load anywhere in it. * vm V present. Every process has its own page table; user addresses are * virtual and the kernel (S mode, untranslated) reaches them through * the page table in software. No slots, no region limit. * * Kernel code never dereferences a user address. It checks it with * proc_user_range_ok and moves data with the mm_copy_* helpers, which are the - * plain loads and stores of the slot backend. */ + * plain loads and stores of the nommu backend. */ struct fnd_proc; @@ -92,11 +94,30 @@ static inline uint32_t mm_read8(const struct fnd_proc *proc, uint32_t addr) { return value; } -/* ---- vm backend: building an address space (proc.c, only when mm_vm) ---- */ +/* ---- nommu backend: the arena allocator (mm.c) ---- */ +/* Where user RAM starts: below it are the stage1, the kernel, its stack and + * heap (§5.1). The allocator hands out whole pages above it, up to the RAM + * the BMC reports. Free pages are zero. */ +#define MM_USER_BASE 0x00400000u #define MM_PAGE 4096u #define MM_PAGE_MASK (MM_PAGE - 1u) +/* Takes exactly the pages of [lo, hi) (rounded out to pages): FND_E_NO_MEMORY + * if they are not RAM of the user area, FND_E_ADDRESS_CONFLICT if any is + * taken. */ +int32_t mm_arena_claim(uint32_t lo, uint32_t hi); +/* The lowest free run of bytes (rounded up to pages), or 0. */ +uint32_t mm_arena_alloc(uint32_t bytes); +/* Returns [lo, hi) to the pool. The caller has zeroed what it wrote. */ +void mm_arena_free(uint32_t lo, uint32_t hi); +/* Takes the free pages [hi, new_hi) right above an arena (both page + * aligned); 0 if any is taken or missing. */ +int mm_arena_extend(uint32_t hi, uint32_t new_hi); +uint32_t mm_arena_free_bytes(void); + +/* ---- vm backend: building an address space (proc.c, only when mm_vm) ---- */ + /* The page table of a new process: FND_E_NO_MEMORY if no frame is free. */ int32_t mm_vm_create(struct fnd_proc *proc); /* Backs [start, end) with zeroed frames of the process, perm being diff --git a/foundation/kernel/proc.c b/foundation/kernel/proc.c index a4060c4..d533a01 100644 --- a/foundation/kernel/proc.c +++ b/foundation/kernel/proc.c @@ -1,4 +1,4 @@ -/* Processes, user slots and protection regions (doc/spec_Foundation.md §5, +/* Processes, their memory and protection regions (doc/spec_Foundation.md §5, * §9.2, §9.4). Processes come from code linked into the kernel image * (phase 1) or from a PROTECTED WAX loaded by wax.c. */ #include "internal.h" @@ -18,6 +18,7 @@ static uint32_t boot_drive; void fnd_set_boot_drive(uint32_t drive) { boot_drive = drive; } +/* The old slot layout: the fixed ranges builtin processes ask for by index. */ static const uint32_t slot_bounds[FND_SLOTS + 1u] = { 0x00400000u, 0x00480000u, 0x00500000u, 0x00580000u, 0x00600000u, 0x00800000u, 0x00C00000u, 0x01000000u}; @@ -25,9 +26,12 @@ static const uint32_t slot_bounds[FND_SLOTS + 1u] = { uint32_t fnd_slot_base(uint32_t slot) { return slot < FND_SLOTS ? slot_bounds[slot] : 0; } uint32_t fnd_slot_end(uint32_t slot) { return slot < FND_SLOTS ? slot_bounds[slot + 1u] : 0; } -/* §5.2: a slot that does not lie wholly in the installed RAM is unusable. - * With V the slots are only address ranges and need no RAM of their own. */ -static int slot_present(uint32_t slot) { return mm_vm || slot_bounds[slot + 1u] <= mm_ram_bytes(); } +/* The default heap a relocatable executable gets with its arena when it does + * not ask for one (heap_hint): more grows in place while the pages above are + * free, as the brk of Linux without an MMU does. */ +#define DEFAULT_HEAP 0x20000u +#define RELOC_WINDOW 0x08000000u /* how far above the sizing load address pass 1 may place libraries */ +#define RELOC_PROBE_BASE 0x01000000u /* the load address pass 1 sizes a relocatable executable at */ /* The vm backend's user address space (doc/foundation_vm_plan.md §3): images * and libraries are placed below VM_WINDOW_END, which keeps clear of the @@ -36,14 +40,13 @@ static int slot_present(uint32_t slot) { return mm_vm || slot_bounds[slot + 1u] #define VM_WINDOW_START 0x00001000u #define VM_WINDOW_END 0x10000000u #define VM_HEAP_TOP 0x80000000u -#define VM_NO_SLOT FND_SLOTS /* proc->slot of a process that has none */ #define zero_range kmem_zero static void maybe_free(struct fnd_proc *proc); /* §9.4: threads DEAD, handles closed (PEER_GONE, CLIENT_GONE, services), - * owned ranges zeroed, slot released, waiters woken. The record stays + * owned ranges zeroed, arena released, waiters woken. The record stays * readable (state EXITED) until its PROCESS handles are closed and the * record is reused. */ void proc_exit(struct fnd_proc *proc, int32_t code, uint32_t fault_epc) { @@ -61,6 +64,9 @@ void proc_exit(struct fnd_proc *proc, int32_t code, uint32_t fault_epc) { if (proc->regions[i].owned) zero_range(proc->regions[i].start, proc->regions[i].end); } + if (!mm_vm) + mm_arena_free(proc->arena_lo, proc->arena_hi); /* free pages are zero */ + proc->arena_lo = proc->arena_hi = 0; ipc_proc_exited(proc); maybe_free(proc); } @@ -87,14 +93,10 @@ struct fnd_proc *fnd_proc_get(uint32_t pid) { return 0; } -/* A free process record for slot, or NULL with *error set. */ -static struct fnd_proc *claim_record(uint32_t slot, int32_t *error) { +/* A free process record, or NULL with *error set. */ +static struct fnd_proc *claim_record(int32_t *error) { struct fnd_proc *proc = 0; for (uint32_t i = 0; i < FND_MAX_PROCS; ++i) { - if (!mm_vm && procs[i].state == FND_PROC_LIVE && procs[i].slot == slot) { - *error = FND_E_ADDRESS_CONFLICT; - return 0; - } if (!proc && procs[i].state == FND_PROC_FREE) proc = &procs[i]; } @@ -106,6 +108,7 @@ static struct fnd_proc *claim_record(uint32_t slot, int32_t *error) { if (proc) { proc->pt_root = 0; proc->vm_pages = 0; + proc->arena_lo = proc->arena_hi = 0; } return proc; } @@ -113,9 +116,8 @@ static struct fnd_proc *claim_record(uint32_t slot, int32_t *error) { /* Fills in a claimed record whose regions are set and readies its thread: * U mode at entry with IE = 1, R1..R27 and R31 zero (§9.2). */ static int32_t start_proc(struct fnd_proc *proc, struct fnd_thread *thread, const char *name, - uint32_t slot, uint32_t priority, uint32_t entry, uint32_t usp, uint32_t ugp) { + uint32_t priority, uint32_t entry, uint32_t usp, uint32_t ugp) { proc->pid = next_pid++; - proc->slot = slot; proc->priority = priority; proc->threads = 1; proc->refs = 0; @@ -152,19 +154,25 @@ int32_t fnd_proc_create_builtin(const char *name, uint32_t slot, uint32_t priori return FND_E_BAD_ARG; if (code_start >= code_end || entry < code_start || entry >= code_end || (entry & 3u)) return FND_E_BAD_ARG; - if (!slot_present(slot)) - return FND_E_NO_MEMORY; uint32_t end = slot_bounds[slot + 1u], ram = end - ram_size; if (code_start < end && slot_bounds[slot] < code_end) return FND_E_ADDRESS_CONFLICT; /* code must not lie in the writable slot */ int32_t error; - struct fnd_proc *proc = claim_record(slot, &error); + struct fnd_proc *proc = claim_record(&error); if (!proc) return error; struct fnd_thread *thread = sched_alloc_thread(); if (!thread) return FND_E_LIMIT; + if (!mm_vm) { + /* The data and stack at the top of the slot's range. */ + int32_t status = mm_arena_claim(ram, end); + if (status != FND_OK) + return status; + proc->arena_lo = ram & ~MM_PAGE_MASK; + proc->arena_hi = (end + MM_PAGE_MASK) & ~MM_PAGE_MASK; + } if (mm_vm) { /* The code stays where the kernel image put it (VA = PA); the data * is private frames at the slot's addresses. */ @@ -191,7 +199,7 @@ int32_t fnd_proc_create_builtin(const char *name, uint32_t slot, uint32_t priori proc->region_count = 2; uint32_t sp = end - 8u; mm_write32(proc, sp, arg); - return start_proc(proc, thread, name, slot, priority, entry, sp, slot_bounds[slot]); + return start_proc(proc, thread, name, priority, entry, sp, slot_bounds[slot]); } /* Loader categories to kernel codes (doc/spec_Foundation.md §9.2). */ @@ -296,10 +304,9 @@ static struct wax_launch launch; /* Plans the heap of a loaded image (§5.3 step 3): it starts at the end of * the highest WAX block and may grow up to the next reservation above it - * (stack or a library) or the end of the slot. */ -static void plan_heap(struct fnd_proc *proc, uint32_t slot, const struct fnd_grant *grants, - uint32_t grant_count) { - uint32_t top = launch.top, limit = mm_vm ? VM_HEAP_TOP : slot_bounds[slot + 1u]; + * (stack, library or MMIO grant) or the end of the process's arena. */ +static void plan_heap(struct fnd_proc *proc, const struct fnd_grant *grants, uint32_t grant_count) { + uint32_t top = launch.top, limit = mm_vm ? VM_HEAP_TOP : proc->arena_hi; proc->heap_region = FND_PROC_REGIONS; /* none yet */ for (uint32_t i = 0; i < launch.region_count; ++i) { const struct wax_region *r = &launch.region[i]; @@ -329,6 +336,60 @@ static uint8_t *vm_load_map(void *context, uint32_t address, uint32_t *run) { return mm_vm_ptr(context, address, run); } +static uint32_t page_up(uint32_t n) { return (n + MM_PAGE_MASK) & ~MM_PAGE_MASK; } + +/* nommu (§5.2): plans the launch and takes the pages it needs. A relocatable + * executable is first planned at a trial address to learn how much room the + * executable, its libraries and its stack need; the arena (with the heap on + * top) comes from the allocator and the launch is planned again inside it. + * An executable linked to fixed addresses takes exactly the pages it + * reserves. On success proc has its arena and `launch` is final. */ +static int32_t plan_arena(struct fnd_proc *proc, const void *image, uint32_t size, + const struct wax_library *table, uint32_t lib_count) { + struct wax_info info; + enum wax_error error = wax_inspect(image, size, &info); + if (error != WAX_OK) + return wax_status(error); + struct wax_target target = {"foundation", FND_ABI_MAJOR, FND_ABI_MINOR, 0, 0, 0}; + if (!info.relocatable) { + /* The window of the old slot: what the image reserves, stack included. */ + target.window_start = info.low & ~MM_PAGE_MASK; + target.window_end = page_up(info.high); + if ((error = wax_plan(&launch, image, size, table, lib_count, &target)) != WAX_OK) + return wax_status(error); + int32_t status = mm_arena_claim(target.window_start, target.window_end); + if (status != FND_OK) + return status; + proc->arena_lo = target.window_start; + proc->arena_hi = target.window_end; + return FND_OK; + } + /* Pass 1: the stack is just below the image; the window starts at the + * stack's page so that libraries land where they will in pass 2. */ + uint32_t below = page_up(info.stack_size); + target.load_base = RELOC_PROBE_BASE; + target.window_start = RELOC_PROBE_BASE - below; + target.window_end = target.window_start + RELOC_WINDOW; + if ((error = wax_plan(&launch, image, size, table, lib_count, &target)) != WAX_OK) + return wax_status(error); + uint32_t reserve = page_up(info.heap_hint ? info.heap_hint : DEFAULT_HEAP); + uint32_t used = launch.top - target.window_start; + if (used > RELOC_WINDOW - reserve) + return FND_E_NO_MEMORY; + uint32_t length = page_up(used + reserve); + uint32_t arena = mm_arena_alloc(length); + if (!arena) + return FND_E_NO_MEMORY; + /* The same plan, moved: the stack's page is the arena's first. */ + if ((error = wax_rebase(&launch, arena - target.window_start)) != WAX_OK) { + mm_arena_free(arena, arena + length); /* a library alignment above a page */ + return wax_status(error); + } + proc->arena_lo = arena; + proc->arena_hi = arena + length; + return FND_OK; +} + /* The common part of every WAX launch. startup is kernel memory, copied into * the new process's startup block. */ static int32_t spawn_image(const char *name, uint32_t priority, uint32_t caps, uint32_t parent, @@ -336,7 +397,6 @@ static int32_t spawn_image(const char *name, uint32_t priority, uint32_t caps, u uint32_t lib_count, const struct fnd_grant *grants, uint32_t grant_count, const void *startup, uint32_t startup_len) { struct wax_library table[FND_MAX_LIBRARIES]; - uint32_t base, slot = 0; mm_init(); if (priority > FND_PRIO_APP || lib_count > FND_MAX_LIBRARIES || (lib_count && !libs) || startup_len > FND_STARTUP_MAX) @@ -344,36 +404,33 @@ static int32_t spawn_image(const char *name, uint32_t priority, uint32_t caps, u int32_t status = check_grants(grants, grant_count); if (status != FND_OK) return status; - enum wax_error error = wax_base_addr(image, size, &base); - if (error != WAX_OK) - return wax_status(error); - if (mm_vm) { - slot = VM_NO_SLOT; /* any address in the window will do */ - } else { - while (slot < FND_SLOTS && !(slot_bounds[slot] <= base && base < slot_bounds[slot + 1u])) - ++slot; - if (slot == FND_SLOTS) - return FND_E_FORMAT; /* §5.2: the image must lie in one user slot */ - if (!slot_present(slot)) - return FND_E_NO_MEMORY; - } - struct fnd_proc *proc = claim_record(slot, &status); + struct fnd_proc *proc = claim_record(&status); if (!proc) return status; for (uint32_t i = 0; i < lib_count; ++i) { table[i].data = libs[i].data; table[i].size = libs[i].size; } - struct wax_target target = {"foundation", FND_ABI_MAJOR, FND_ABI_MINOR, - mm_vm ? VM_WINDOW_START : slot_bounds[slot], - mm_vm ? VM_WINDOW_END : slot_bounds[slot + 1u]}; - if ((error = wax_plan(&launch, image, size, table, lib_count, &target)) != WAX_OK) - return wax_status(error); - if (!mm_vm && launch.region_count + grant_count > FND_REGIONS) + if (mm_vm) { + struct wax_target target = {"foundation", FND_ABI_MAJOR, FND_ABI_MINOR, VM_WINDOW_START, VM_WINDOW_END, 0}; + enum wax_error error = wax_plan(&launch, image, size, table, lib_count, &target); + if (error != WAX_OK) + return wax_status(error); + } else if ((status = plan_arena(proc, image, size, table, lib_count)) != FND_OK) { + return status; + } + if (!mm_vm && launch.region_count + grant_count > FND_REGIONS) { + mm_arena_free(proc->arena_lo, proc->arena_hi); + proc->arena_lo = proc->arena_hi = 0; return FND_E_REGION_LIMIT; /* §5.3: the grants come after the image */ + } struct fnd_thread *thread = sched_alloc_thread(); - if (!thread) + if (!thread) { + if (!mm_vm) + mm_arena_free(proc->arena_lo, proc->arena_hi); + proc->arena_lo = proc->arena_hi = 0; return FND_E_LIMIT; + } if (mm_vm) { /* The whole address space is built before anything is loaded, so @@ -406,11 +463,12 @@ static int32_t spawn_image(const char *name, uint32_t priority, uint32_t caps, u proc->region_count = launch.region_count + grant_count; proc->parent = parent; proc->caps = caps; - plan_heap(proc, slot, grants, grant_count); + plan_heap(proc, grants, grant_count); uint32_t index = (uint32_t)(proc - procs); kmem_copy((uint32_t)(uintptr_t)startup_blocks[index], (uint32_t)(uintptr_t)startup, startup_len); proc->startup_len = startup_len; - return start_proc(proc, thread, name, slot, priority, launch.entry, launch.stack_top, base); + return start_proc(proc, thread, name, priority, launch.entry, launch.stack_top, + launch.image[0].origin); } /* The startup block of a bundled module (§11): argv = [name], cwd = "/", @@ -559,8 +617,16 @@ int32_t proc_mem_grow(struct fnd_proc *proc, uint32_t delta) { uint32_t old = proc->heap_end; if (delta == 0) return (int32_t)old; - if (!proc->heap_limit || old > proc->heap_limit || delta > proc->heap_limit - old) + if (!proc->heap_limit || old > proc->heap_limit) return FND_E_NO_MEMORY; + if (delta > proc->heap_limit - old) { + /* nommu: a heap that ends the arena grows into the free pages above + * it, like brk without an MMU; there is no moving it. */ + uint32_t want = page_up(old + delta); + if (mm_vm || proc->heap_limit != proc->arena_hi || want < old || !mm_arena_extend(proc->arena_hi, want)) + return FND_E_NO_MEMORY; + proc->arena_hi = proc->heap_limit = want; + } if (mm_vm) { /* Frames are zeroed when handed out, and need not be adjacent. */ int32_t status = mm_vm_map(proc, old, old + delta, CPT32_PERM_R | CPT32_PERM_W); @@ -614,11 +680,10 @@ static void put_word(const struct fnd_proc *proc, uint32_t buf, uint32_t cap, ui } int32_t proc_sys_info(const struct fnd_proc *caller, uint32_t sel, uint32_t buf, uint32_t cap) { - uint32_t live = 0, used = 0, records = 0; + uint32_t live = 0, records = 0; for (uint32_t i = 0; i < FND_MAX_PROCS; ++i) { if (procs[i].state == FND_PROC_LIVE) { ++live; - used += mm_vm ? 0 : slot_bounds[procs[i].slot + 1u] - slot_bounds[procs[i].slot]; } if (procs[i].state != FND_PROC_FREE) ++records; @@ -662,13 +727,8 @@ int32_t proc_sys_info(const struct fnd_proc *caller, uint32_t sel, uint32_t buf, return (int32_t)size; } if (sel == 0) { - uint32_t user = 0; - for (uint32_t slot = 0; !mm_vm && slot < FND_SLOTS; ++slot) { - if (slot_present(slot)) - user += slot_bounds[slot + 1u] - slot_bounds[slot]; - } uint32_t info[8] = {FND_ABI_MAJOR, FND_ABI_MINOR, FND_TICK_HZ, mm_ram_bytes(), - mm_vm ? mm_vm_free_bytes() : user - used, live, + mm_vm ? mm_vm_free_bytes() : mm_arena_free_bytes(), live, boot_drive, 0}; for (uint32_t w = 0; w < 8u; ++w) put_word(caller, buf, cap, 4u * w, info[w]); diff --git a/foundation/kernel/wax.c b/foundation/kernel/wax.c index 7b967d8..341c771 100644 --- a/foundation/kernel/wax.c +++ b/foundation/kernel/wax.c @@ -9,6 +9,10 @@ #define TYPE_WAL 2u #define FLAG_DEBUG 1u #define FLAG_PROTECTED 2u +#define FLAG_RELOCATABLE 4u +#define RELOC_PAIR 1u +#define OP_LDIH 0x3Du +#define OP_LDIL 0x3Cu #define PERM_R 1u #define PERM_RW 3u #define PERM_RX 5u @@ -317,14 +321,22 @@ static enum wax_error parse_v3(struct wax_image *im) { if (size < HEADER_V3) return WAX_E_FORMAT; uint32_t type = rd16(d + 6), flags = rd16(d + 10); - if (rd16(d + 8) != 1u || type < TYPE_WAX || type > TYPE_WAL || (flags & ~3u) || word(im, 20) != 1u) + if (rd16(d + 8) != 1u || type < TYPE_WAX || type > TYPE_WAL || (flags & ~7u) || word(im, 20) != 1u) return WAX_E_UNSUPPORTED; /* arch, type, flags, call_abi */ if (!(flags & FLAG_PROTECTED) || word(im, 12) != HEADER_V3 || word(im, 16) != size) return WAX_E_FORMAT; - for (uint32_t offset = 128; offset < HEADER_V3; offset += 4u) { + for (uint32_t offset = 140; offset < HEADER_V3; offset += 4u) { if (word(im, offset)) return WAX_E_FORMAT; } + /* §3.1: the relocation table and heap hint belong to relocatable executables. */ + im->relocatable = (flags & FLAG_RELOCATABLE) != 0; + im->reloc_table = word(im, 128); + im->reloc_count = word(im, 132); + im->heap_hint = word(im, 136); + if (im->relocatable ? type != TYPE_WAX || !im->reloc_count || (im->heap_hint & 7u) + : im->reloc_table || im->reloc_count || im->heap_hint) + return WAX_E_FORMAT; im->type = type; im->base = word(im, 24); im->entry_offset = word(im, 28); @@ -366,6 +378,7 @@ static enum wax_error parse_v3(struct wax_image *im) { !add_table(&spans, im->dep_table, im->dep_count, 16u, size) || !add_table(&spans, im->export_table, im->export_count, 24u, size) || !add_table(&spans, im->import_table, im->import_count, 24u, size) || + !add_table(&spans, im->reloc_table, im->reloc_count, 4u, size) || !add_table(&spans, im->strings, 1u, im->string_size, size) || !add_span(&spans, debug_offset, debug_size, size)) return WAX_E_FORMAT; @@ -416,6 +429,8 @@ static enum wax_error parse_v3(struct wax_image *im) { return WAX_E_FORMAT; if (top - stack < end && im->base < top) return WAX_E_FORMAT; /* stack overlaps the image reservation */ + if (im->relocatable && top != im->base) + return WAX_E_FORMAT; /* §3.1: the stack lies just below the image */ } /* Dependencies (§4.1). */ @@ -474,6 +489,33 @@ static enum wax_error parse_v3(struct wax_image *im) { } if (used != (1u << im->dep_count) - 1u) return WAX_E_FORMAT; + + /* Relocations (§3.1): ascending, each a whole data word or an LDIH/LDIL + * pair in initialized bytes of a block that may hold it. */ + uint32_t previous_at = 0; + for (uint32_t i = 0; i < im->reloc_count; ++i) { + uint32_t entry = word(im, im->reloc_table + 4u * i), at = entry & ~3u, kind = entry & 3u; + if (kind > RELOC_PAIR || (i && at <= previous_at) || at < im->base || at - im->base > 0xFFFFFFF0u) + return WAX_E_FORMAT; + previous_at = at; + uint32_t len = kind == RELOC_PAIR ? 8u : 4u, found = 0; + for (uint32_t k = 0; k < im->block_count && !found; ++k) { + struct block b; + get_block(im, k, &b); + if (b.address <= at && at - b.address <= b.file_size && len <= b.file_size - (at - b.address) && + (kind == RELOC_PAIR ? b.perm == PERM_RX : b.perm != PERM_RX)) { + found = 1; + if (kind == RELOC_PAIR) { + const uint8_t *code = d + b.file_offset + (at - b.address); + if (rd32(code) >> 26 != OP_LDIH || rd32(code + 4) >> 26 != OP_LDIL || + ((rd32(code) ^ rd32(code + 4)) & 0x03E00000u)) + return WAX_E_FORMAT; + } + } + } + if (!found) + return WAX_E_FORMAT; + } return WAX_OK; } @@ -493,6 +535,51 @@ static enum wax_error parse(struct wax_image *im, const void *data, uint32_t siz return WAX_E_UNSUPPORTED; } +/* A light read of the header and block table: the plan validates the image + * properly, and this runs first on every spawn, so it only guards its own + * reads. */ +enum wax_error wax_inspect(const void *data, uint32_t size, struct wax_info *info) { + const uint8_t *d = data; + if (!d || size < HEADER_V2 || d[0] != 'W' || d[1] != 'A' || d[2] != 'X' || d[3] != 0) + return WAX_E_FORMAT; + uint32_t version = rd16(d + 4), base, stack_top, stack_size, blocks, count, entry_size, flags = rd16(d + 10); + if (version == 2u) { + base = rd32(d + 16); + blocks = rd32(d + 24), count = rd32(d + 28), entry_size = rd32(d + 32); + stack_top = rd32(d + 48), stack_size = rd32(d + 52); + flags = 0; + } else if (version == 3u && size >= HEADER_V3) { + base = rd32(d + 24); + stack_top = rd32(d + 32), stack_size = rd32(d + 36); + blocks = rd32(d + 40), count = rd32(d + 44), entry_size = 24u; + info->heap_hint = rd32(d + 136); + } else { + return version == 2u || version == 3u ? WAX_E_FORMAT : WAX_E_UNSUPPORTED; + } + uint32_t block_size = version == 2u ? 16u : 24u; + if (entry_size != block_size || !count || count > WAX_MAX_BLOCKS || blocks > size || + count * block_size > size - blocks || stack_size > stack_top) + return WAX_E_FORMAT; + uint32_t end = base; + for (uint32_t i = 0; i < count; ++i) { + const uint8_t *e = d + blocks + i * block_size; + uint32_t address = rd32(e), len = version == 2u ? rd32(e + 8) : rd32(e + 12); + if (address < base || len > CPU_RESERVED - address) + return WAX_E_FORMAT; + if (address + len > end) + end = address + len; + } + uint32_t stack_low = stack_top - stack_size; + info->base = base; + info->low = stack_low < base ? stack_low : base; + info->high = stack_top > end ? stack_top : end; + info->relocatable = (flags & FLAG_RELOCATABLE) != 0; + info->stack_size = stack_size; + if (version != 3u) + info->heap_hint = 0; + return WAX_OK; +} + enum wax_error wax_base_addr(const void *data, uint32_t size, uint32_t *base) { const uint8_t *d = data; if (!d || size < HEADER_V2 || d[0] != 'W' || d[1] != 'A' || d[2] != 'X' || d[3] != 0) @@ -660,10 +747,10 @@ static enum wax_error place(struct wax_launch *launch, const struct resolver *r) struct span taken[WAX_MAX_IMAGES + 1u]; uint32_t n = 0; const struct wax_image *exe = &launch->image[0]; - taken[n].start = exe->base; - taken[n++].end = exe->base + exe->span; - taken[n].start = exe->stack_top - exe->stack_size; - taken[n++].end = exe->stack_top; + taken[n].start = exe->origin; + taken[n++].end = exe->origin + exe->span; + taken[n].start = exe->stack_top - exe->stack_size + r->launch->delta; + taken[n++].end = exe->stack_top + r->launch->delta; for (uint32_t i = 0; i < n; ++i) { if (taken[i].start < t->window_start || taken[i].end > t->window_end) return fail(launch, WAX_E_FORMAT, 0, 0, 0); @@ -701,8 +788,8 @@ static enum wax_error place(struct wax_launch *launch, const struct resolver *r) static enum wax_error plan_regions(struct wax_launch *launch) { const struct wax_image *exe = &launch->image[0]; uint32_t next[WAX_MAX_IMAGES] = {0}, n = 1; - launch->region[0].start = exe->stack_top - exe->stack_size; - launch->region[0].end = exe->stack_top; + launch->region[0].start = exe->stack_top - exe->stack_size + launch->delta; + launch->region[0].end = exe->stack_top + launch->delta; launch->region[0].flags = PERM_RW; launch->top = 0; for (;;) { @@ -758,6 +845,16 @@ enum wax_error wax_plan(struct wax_launch *launch, const void *data, uint32_t si if (exe->version == 3u && !abi_ok(exe, target)) return WAX_E_ABI_MISMATCH; exe->origin = exe->base; + launch->delta = 0; + launch->heap_hint = exe->heap_hint; + if (exe->relocatable && target->load_base) { + /* §3.1: the stack moves with the image. */ + if (target->load_base < exe->stack_size || (target->load_base & 3u) || + exe->span > CPU_RESERVED - target->load_base) + return WAX_E_FORMAT; + exe->origin = target->load_base; + launch->delta = target->load_base - exe->base; + } launch->count = 1; r.launch = launch; r.libs = libs; @@ -767,12 +864,30 @@ enum wax_error wax_plan(struct wax_launch *launch, const void *data, uint32_t si if ((error = resolve(&r, 0)) != WAX_OK || (error = check_imports(launch)) != WAX_OK || (error = place(launch, &r)) != WAX_OK || (error = plan_regions(launch)) != WAX_OK) return error; - launch->entry = exe->base + exe->entry_offset; - launch->stack_top = exe->stack_top; + launch->entry = exe->origin + exe->entry_offset; + launch->stack_top = exe->stack_top + launch->delta; launch->stack_size = exe->stack_size; return WAX_OK; } +enum wax_error wax_rebase(struct wax_launch *launch, uint32_t shift) { + for (uint32_t i = 0; i < launch->count; ++i) { + if (launch->image[i].align > 1u && (shift & (launch->image[i].align - 1u))) + return WAX_E_FORMAT; + } + for (uint32_t i = 0; i < launch->count; ++i) + launch->image[i].origin += shift; + for (uint32_t i = 0; i < launch->region_count; ++i) { + launch->region[i].start += shift; + launch->region[i].end += shift; + } + launch->entry += shift; + launch->stack_top += shift; + launch->top += shift; + launch->delta += shift; + return WAX_OK; +} + /* Byte copies and fills, by words (eight per step) where the addresses are * 4-byte aligned: byte by byte, loading the boot modules took most of a * second. */ @@ -843,6 +958,55 @@ static void put32(wax_map_fn map, void *context, uint32_t address, uint32_t valu put_bytes(map, context, address, bytes, 4u); } +static uint32_t get32(wax_map_fn map, void *context, uint32_t address) { + uint32_t run; + return rd32(map(context, address, &run)); /* aligned: a run is at least 4 bytes */ +} + +/* The instruction word insn with its 16-bit immediate replaced. */ +static __attribute__((noinline)) uint32_t imm_of(uint32_t insn, uint32_t imm) { + return insn - (insn & 0xFFFFu) + (imm & 0xFFFFu); +} + +/* §3.1: adds the load offset to every address the executable holds. */ +static void relocate(const struct wax_launch *launch, wax_map_fn map, void *context) { + const struct wax_image *exe = &launch->image[0]; + if (!exe->relocatable || launch->delta == 0) + return; +#if defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ + /* The kernel's own memory, and words in the byte order of the image: a + * relocation is a few operations (this runs for thousands of them per + * spawn, and the generated code does not keep values in registers). */ + if (map == bias_map && !((uintptr_t)exe->data & 3u)) { + const uint32_t *table = (const uint32_t *)(exe->data + exe->reloc_table); + uint32_t delta = launch->delta; + for (uint32_t i = 0; i < exe->reloc_count; ++i) { + uint32_t entry = table[i]; + volatile uint32_t *at = (volatile uint32_t *)((uintptr_t)context + (entry & ~3u) + delta); + if (entry & RELOC_PAIR) { + uint32_t value = (at[0] << 16) + (at[1] & 0xFFFFu) + delta; + at[0] = (at[0] & 0xFFFF0000u) | value >> 16; + at[1] = (at[1] & 0xFFFF0000u) | (value & 0xFFFFu); + } else { + *at += delta; + } + } + return; + } +#endif + for (uint32_t i = 0; i < exe->reloc_count; ++i) { + uint32_t entry = word(exe, exe->reloc_table + 4u * i), at = (entry & ~3u) + launch->delta; + if (entry & RELOC_PAIR) { + uint32_t high = get32(map, context, at), low = get32(map, context, at + 4u); + uint32_t value = (high & 0xFFFFu) * 0x10000u + (low & 0xFFFFu) + launch->delta; + put32(map, context, at, imm_of(high, value / 0x10000u)); + put32(map, context, at + 4u, imm_of(low, value)); + } else { + put32(map, context, at, get32(map, context, at) + launch->delta); + } + } +} + void wax_load_mapped(const struct wax_launch *launch, wax_map_fn map, void *context) { for (uint32_t i = 0; i < launch->count; ++i) { const struct wax_image *im = &launch->image[i]; @@ -855,6 +1019,7 @@ void wax_load_mapped(const struct wax_launch *launch, wax_map_fn map, void *cont } } put_zeros(map, context, launch->stack_top - launch->stack_size, launch->stack_size); + relocate(launch, map, context); /* Import descriptors (§4.3): resolved address, provider origin. */ for (uint32_t i = 0; i < launch->count; ++i) { const struct wax_image *im = &launch->image[i]; diff --git a/foundation/kernel/wax.h b/foundation/kernel/wax.h index be60247..4b3a164 100644 --- a/foundation/kernel/wax.h +++ b/foundation/kernel/wax.h @@ -41,6 +41,9 @@ struct wax_image { uint32_t span; /* reservation: highest block end - lowest address */ uint32_t align; /* largest block alignment */ uint32_t entry_offset, stack_top, stack_size; + uint32_t relocatable; /* WAX flag RELOCATABLE (§3.1) */ + uint32_t reloc_table, reloc_count; /* 4-byte entries, link-time address | kind */ + uint32_t heap_hint; /* bytes of heap the program asks for, 0 = OS default */ uint32_t block_count, block_table, block_entry_size; uint32_t dep_count, dep_table; uint32_t export_count, export_table; @@ -65,6 +68,10 @@ struct wax_target { const char *os_id; uint32_t os_major, os_minor; uint32_t window_start, window_end; /* all blocks, libraries and the stack */ + /* Where a relocatable executable (v3 §3.1) is to run: its lowest block + * goes to load_base and its stack, which lies just below, moves with it. + * 0 keeps the link address. Other executables always keep theirs. */ + uint32_t load_base; }; struct wax_launch { @@ -72,6 +79,8 @@ struct wax_launch { struct wax_image image[WAX_MAX_IMAGES]; /* libraries in placement order */ uint32_t entry, stack_top, stack_size; uint32_t top; /* end of the highest block: where the heap starts (§5.3) */ + uint32_t delta; /* load address minus link address of the executable (relocation) */ + uint32_t heap_hint; /* of the executable, 0 if it has none */ uint32_t region_count; struct wax_region region[WAX_REGIONS]; /* [0] is the stack */ /* Diagnostics for a failed plan: the requesting image (launch index or, @@ -86,6 +95,16 @@ struct wax_launch { * version, so the caller can pick the window before wax_plan. */ enum wax_error wax_base_addr(const void *data, uint32_t size, uint32_t *base); +/* What the caller needs to choose where an executable goes before wax_plan: + * the link-time range its blocks and stack reserve ([low, high), holes + * included), whether it is relocatable (v3 §3.1), the stack it needs and the + * heap it asks for. Validates the executable alone. */ +struct wax_info { + uint32_t base, low, high; + uint32_t relocatable, stack_size, heap_hint; +}; +enum wax_error wax_inspect(const void *data, uint32_t size, struct wax_info *info); + /* Validates the executable and every library it needs, selects providers, * places the libraries inside the window, plans the regions and checks every * import. Writes nothing outside *launch. */ @@ -93,8 +112,14 @@ enum wax_error wax_plan(struct wax_launch *launch, const void *data, uint32_t si const struct wax_library *libs, uint32_t lib_count, const struct wax_target *target); +/* Moves a successful plan of a relocatable executable by shift bytes (a + * multiple of every image's alignment): the plan is made once at a trial + * address and then rebased onto the memory it is given. */ +enum wax_error wax_rebase(struct wax_launch *launch, uint32_t shift); + /* Copies the blocks of a successful plan, zeroes their tails and the stack, - * and writes the import descriptors. Address a is written at bias + a. */ + * applies the relocations of a relocatable executable and writes the import + * descriptors. Address a is written at bias + a. */ void wax_load(const struct wax_launch *launch, uintptr_t bias); /* The same through a mapping, for an address space the loader cannot write diff --git a/foundation/tests/bmpview/test_bmpview.py b/foundation/tests/bmpview/test_bmpview.py index 0491fb5..64882ed 100644 --- a/foundation/tests/bmpview/test_bmpview.py +++ b/foundation/tests/bmpview/test_bmpview.py @@ -1,6 +1,6 @@ """Shell -> /bin/bmpview -> /dev/fb0: pixels of 8-bit and 24-bit BMPs reach the screen. -Build cargo build --release --bin src32_testbench first; run again with FND_MM=slot. +Build cargo build --release --bin src32_testbench first; run again with FND_MM=nommu. """ from pathlib import Path import subprocess diff --git a/foundation/tests/chip8/test_guest.py b/foundation/tests/chip8/test_guest.py index ffbb8e4..e81bf7b 100644 --- a/foundation/tests/chip8/test_guest.py +++ b/foundation/tests/chip8/test_guest.py @@ -1,7 +1,7 @@ """Shell -> CHIP-8 WAX -> fb/SGU, plus real held/released PeC input. Build cargo build --release --bin src32_testbench before running this suite. -Run again with FND_MM=slot. Temporary files must be writable on Windows. +Run again with FND_MM=nommu. Temporary files must be writable on Windows. """ import os from pathlib import Path diff --git a/foundation/tests/console/test_console_guest.py b/foundation/tests/console/test_console_guest.py index abd54c9..8a7dddb 100644 --- a/foundation/tests/console/test_console_guest.py +++ b/foundation/tests/console/test_console_guest.py @@ -1,6 +1,6 @@ """ST_WRITE over real libfnd/IPC with PCG text and attribute checks. -Run also with FND_MM=slot. Uses the shell boot builder and testbench. +Run also with FND_MM=nommu. Uses the shell boot builder and testbench. """ import os from pathlib import Path diff --git a/foundation/tests/fb/test_fb.py b/foundation/tests/fb/test_fb.py index 1b6bc2a..438aede 100644 --- a/foundation/tests/fb/test_fb.py +++ b/foundation/tests/fb/test_fb.py @@ -1,6 +1,6 @@ """Real shell -> fs -> transferred kernel CHANNEL -> VDP, in VM and slots. -Run python -m unittest discover -s foundation/tests/fb -v, also FND_MM=slot. +Run python -m unittest discover -s foundation/tests/fb -v, also FND_MM=nommu. Uses the shell harness's boot builder and workspace-local TEMP on Windows. """ import os diff --git a/foundation/tests/libsgc/test_libsgc.py b/foundation/tests/libsgc/test_libsgc.py index 5a69c9a..6d828af 100644 --- a/foundation/tests/libsgc/test_libsgc.py +++ b/foundation/tests/libsgc/test_libsgc.py @@ -1,6 +1,6 @@ """libsgc: the command builder on the host, and the WAL "sgc" in Foundation. -Run python -m unittest discover -s foundation/tests/libsgc -v, also FND_MM=slot. +Run python -m unittest discover -s foundation/tests/libsgc -v, also FND_MM=nommu. The guest tests need src32_testbench and Clang (see tests/shell). """ import os diff --git a/foundation/tests/panic/test_panic.py b/foundation/tests/panic/test_panic.py index bbc94db..a711f53 100644 --- a/foundation/tests/panic/test_panic.py +++ b/foundation/tests/panic/test_panic.py @@ -39,7 +39,7 @@ def test_bus_error_in_kernel_shows_diagnosis(self): *(str(ROOT / 'foundation/kernel' / name) for name in KERNEL), '-I', str(ROOT / 'sdk/cpt32/include'), '-I', str(ROOT / 'foundation/include'), '--origin', '0x10000', '--stack-top', '0x100000', - '-D', 'FND_MM_SLOT_ONLY', '-O1', '-o', str(image)] + '-D', 'FND_MM_NOMMU_ONLY', '-O1', '-o', str(image)] if os.environ.get('CLANG'): command += ['--clang', os.environ['CLANG']] subprocess.run(command, check=True, capture_output=True, text=True, timeout=300) diff --git a/foundation/tests/pec/test_pec_guest.py b/foundation/tests/pec/test_pec_guest.py index 4e0bed6..1f8d977 100644 --- a/foundation/tests/pec/test_pec_guest.py +++ b/foundation/tests/pec/test_pec_guest.py @@ -1,6 +1,6 @@ """Real shell -> fs -> transferred kernel CHANNEL -> PeC streams, in VM and slots. -Run python -m unittest discover -s foundation/tests/pec -v, also FND_MM=slot. +Run python -m unittest discover -s foundation/tests/pec -v, also FND_MM=nommu. Uses the shell harness's boot builder and workspace-local TEMP on Windows. """ import os diff --git a/foundation/tests/perf/profile_spawn.py b/foundation/tests/perf/profile_spawn.py index 05af9bc..587d93d 100644 --- a/foundation/tests/perf/profile_spawn.py +++ b/foundation/tests/perf/profile_spawn.py @@ -1,7 +1,7 @@ """Build/run an isolated, instrumented Foundation echo spawn (Issue #106). python foundation/tests/perf/profile_spawn.py target/spawn-profile --cpu cached -Set FND_MM=slot to force slots. Production sources are never rewritten. +Set FND_MM=nommu to force slots. Production sources are never rewritten. """ import argparse import importlib.util diff --git a/foundation/tests/sgc/guest.c b/foundation/tests/sgc/guest.c index 32b7234..10e24fd 100644 --- a/foundation/tests/sgc/guest.c +++ b/foundation/tests/sgc/guest.c @@ -28,9 +28,17 @@ int main(void) { /* Fill the FIFO exactly: NOP words, then a full FIFO must not block. */ uint32_t nops[FND_SGC_FIFO_WORDS + 8]; for (uint32_t i = 0; i < FND_SGC_FIFO_WORDS + 8; ++i) nops[i] = FND_SGC_OP_NOP; - CHECK(fnd_sgc_submit(h, nops, FND_SGC_FIFO_WORDS + 8, &done) == FND_OK && done == FND_SGC_FIFO_WORDS); - CHECK(fnd_sgc_submit(h, nops, 1, &done) == FND_E_WOULD_BLOCK && done == 0); - CHECK(fnd_sgc_get_status(h, 0, &st) == FND_OK && st.fifo_free == 0 && !(st.status & FND_SGC_STATUS_FIFO_FULL)); + /* A VDP frame drains the FIFO at any moment, so a frame that lands between the calls below makes this attempt + * meaningless (which frame the guest meets depends on how long the spawn took). Wait a frame and try again. */ + int full = 0; + for (int attempt = 0; attempt < 4 && !full; ++attempt) { + if (attempt) fnd_sys_sleep(3); + full = fnd_sgc_submit(h, nops, FND_SGC_FIFO_WORDS + 8, &done) == FND_OK && done == FND_SGC_FIFO_WORDS && + fnd_sgc_submit(h, nops, 1, &done) == FND_E_WOULD_BLOCK && done == 0 && + fnd_sgc_get_status(h, 0, &st) == FND_OK && st.fifo_free == 0 && + !(st.status & FND_SGC_STATUS_FIFO_FULL); + } + CHECK(full); CHECK(fnd_sgc_fifo_clear(h) == FND_OK); CHECK(fnd_sgc_get_status(h, FND_SGC_STATUS_FIFO_ERROR, &st) == FND_OK && (st.status & FND_SGC_STATUS_FIFO_ERROR)); CHECK(fnd_sgc_get_status(h, 0, &st) == FND_OK && st.fifo_used == 0 && !(st.status & FND_SGC_STATUS_FIFO_ERROR)); diff --git a/foundation/tests/sgc/test_sgc.py b/foundation/tests/sgc/test_sgc.py index 537e818..bc4a1fd 100644 --- a/foundation/tests/sgc/test_sgc.py +++ b/foundation/tests/sgc/test_sgc.py @@ -1,6 +1,6 @@ """Real shell -> fs -> transferred kernel CHANNEL -> SGC, in VM and slots. -Run python -m unittest discover -s foundation/tests/sgc -v, also FND_MM=slot. +Run python -m unittest discover -s foundation/tests/sgc -v, also FND_MM=nommu. Uses the shell harness's boot builder and workspace-local TEMP on Windows. """ import os diff --git a/foundation/tests/sgub/test_sgub.py b/foundation/tests/sgub/test_sgub.py index 053de41..5d17efc 100644 --- a/foundation/tests/sgub/test_sgub.py +++ b/foundation/tests/sgub/test_sgub.py @@ -1,4 +1,4 @@ -"""SGUB output backend and Foundation command. Run with FND_MM=slot too.""" +"""SGUB output backend and Foundation command. Run with FND_MM=nommu too.""" import ctypes as C import os from pathlib import Path diff --git a/foundation/tests/shell/cpu_features.a b/foundation/tests/shell/cpu_features.a deleted file mode 100644 index 3e64fb7..0000000 --- a/foundation/tests/shell/cpu_features.a +++ /dev/null @@ -1,16 +0,0 @@ -; Report CPUID Extension V as a single PCG character, with R1=42 on success. -; Run on the same CPU engine as the shell; no Foundation kernel is involved. -.ORG 0 - CPUID - NOP - NOP - NOP - LDIL R3, 0x0200 - AND R2, R2, R3 - SLTU R2, R0, R2 - ADDI R2, R2, 48 - LDIH R3, 0x1000 - LDIL R3, 0 - STB [R3+0], R2 - ADDI R1, R0, 42 - HALT diff --git a/foundation/tests/shell/test_shell.py b/foundation/tests/shell/test_shell.py index 0bb822d..7304952 100644 --- a/foundation/tests/shell/test_shell.py +++ b/foundation/tests/shell/test_shell.py @@ -26,8 +26,7 @@ sys.path.insert(0, str(ROOT / 'tools/fs2')) import fs2 # noqa: E402 STOP_TICKS = 800 # 8 seconds at 100 Hz, including long command batches -# FND_MM=slot forces slots; otherwise CPUID decides the actual backend. -SLOT_MM = os.environ.get('FND_MM') == 'slot' +# FND_MM=nommu forces physical memory; otherwise CPUID decides the actual backend. FRAMES = 660 # Keys typed before the shell is up (about frame 120: fs checks the boot # volume, then init reads /bin/shell.wax) wait in the console's type-ahead. @@ -53,14 +52,6 @@ def setUpClass(cls): cls.harness = find_testbench() cls.temp = tempfile.TemporaryDirectory(prefix='foundation-shell-') cls.work = Path(cls.temp.name) - cls.cpu_features = cls.work / 'cpu_features.wax' - result = subprocess.run( - [sys.executable, str(ROOT / 'tools/asm/asm.py'), - str(HERE / 'cpu_features.a'), '-o', str(cls.cpu_features)], - capture_output=True, text=True, encoding='utf-8', timeout=60) - if result.returncode != 0: - raise RuntimeError(result.stdout + result.stderr) - cls.cpu_vm = {} command = [sys.executable, str(ROOT / 'foundation/boot/build.py'), str(cls.work), '--stop-ticks', str(STOP_TICKS)] clang = os.environ.get('CLANG') or shutil.which('clang') @@ -78,26 +69,26 @@ def tearDownClass(cls): cls.temp.cleanup() def free_kib(self, rows): - """VM free memory depends on image sizes: require a positive value - and normalize it to N. Slots retain their exact expectations.""" + """Free memory depends on the image sizes and the backend: require a + positive value and normalize it to N.""" out = [] for row in rows: match = re.match(r'^( +)(\d+)( KiB free)$', row) - if match and not self.slot_mm: + if match: self.assertGreater(int(match.group(2)), 0, row) row = ' ' * (len(match.group(1)) + len(match.group(2)) - 1) + 'N' + match.group(3) match = re.match(r'^(Mem: \d+K total, )(\d+)(K free)$', row) - if match and not self.slot_mm: + if match: self.assertGreater(int(match.group(2)), 0, row) row = match.group(1) + 'N' + match.group(3) out.append(row) return out def free_line(self, kib): - return '%8s KiB free' % (kib if self.slot_mm else 'N') + return '%8s KiB free' % 'N' def mem_line(self, kib): - return 'Mem: 16384K total, %sK free' % (kib if self.slot_mm else 'N') + return 'Mem: 16384K total, NK free' def screen(self, *batches, step=STEP_FRAMES, at_frames=None, config=None, cpu='cached', final=(), keys=(), audio=False): """Types each batch STEP frames after the previous one; returns the @@ -110,17 +101,6 @@ def screen(self, *batches, step=STEP_FRAMES, at_frames=None, config=None, cpu='c 'disk:\n - slot: 0\n type: "hdd"\n image: "disk.img"\n read_only: false\n', encoding='utf-8') cpu = os.environ.get('FOUNDATION_TEST_CPU', cpu) - if cpu not in self.cpu_vm: - probe = subprocess.run( - [str(self.harness), str(self.cpu_features), '42', '--cpu', cpu, - '--frames', '1', '--dump-pcg'], cwd=run, capture_output=True, - text=True, encoding='utf-8', errors='replace', timeout=60) - self.assertEqual(probe.returncode, 0, probe.stdout + probe.stderr) - lines = probe.stdout.splitlines() - rows = lines[lines.index('PCG SCREEN BEGIN') + 1:lines.index('PCG SCREEN END')] - self.assertIn(rows[0], ('0', '1'), probe.stdout) - self.cpu_vm[cpu] = rows[0] == '1' - self.slot_mm = SLOT_MM or not self.cpu_vm[cpu] command = [str(self.harness), str(self.image), '42', '--cpu', cpu, '--frames', str(FRAMES), '--dump-pcg'] if audio: @@ -173,10 +153,8 @@ def test_exec_after_image_replacement(self): ]) def test_free_reports_installed_memory(self): - # The kernel takes the sizes from the BMC (spec_BMC_DMAC.md §1.5). - # 12 MiB holds slots 0-5, and the disk service now uses slot 2, so - # nothing is left with the slot backend (forced or CPUID V absent). - # A V-capable CPU instead reports its remaining physical frames. + # The kernel takes the sizes from the BMC (spec_BMC_DMAC.md §1.5); + # the free memory is whatever pages the processes have left. self.assertEqual(self.screen('free\\n', config='memory:\n ram_mib: 12\n vram_mib: 8\n'), BANNER + [ '/> free', diff --git a/foundation/tests/tinybasic/test_tinybasic.py b/foundation/tests/tinybasic/test_tinybasic.py index ae4d5b2..77e08c8 100644 --- a/foundation/tests/tinybasic/test_tinybasic.py +++ b/foundation/tests/tinybasic/test_tinybasic.py @@ -1,7 +1,7 @@ """Foundation C TinyBASIC: actual shell -> WAX/WAL -> console/fs IPC. Uses the shell harness to build a fresh disk and inspect the real PCG output. -Run with FND_MM=slot as well to check private storage in the fixed-slot backend. +Run with FND_MM=nommu as well to check private storage in the fixed-slot backend. """ from pathlib import Path import sys diff --git a/foundation/tests/wav/test_wav.py b/foundation/tests/wav/test_wav.py index 387867a..0f521b5 100644 --- a/foundation/tests/wav/test_wav.py +++ b/foundation/tests/wav/test_wav.py @@ -1,6 +1,6 @@ """WAV parsing/conversion and real Foundation -> FIFO -> stereo capture. -Run with FND_MM=slot too. --dump-audio advances the SGU at 48 kHz; without +Run with FND_MM=nommu too. --dump-audio advances the SGU at 48 kHz; without it the historical testbench never drains a DAC FIFO. """ import ctypes as C diff --git a/foundation/tests/wax/build.py b/foundation/tests/wax/build.py index 63914d7..e4a9dff 100644 --- a/foundation/tests/wax/build.py +++ b/foundation/tests/wax/build.py @@ -91,6 +91,19 @@ def v2_program(out, clang): return image.read_bytes() +def pie_program(out, clang): + """A relocatable executable in C (driver --pie).""" + image = out / "pie.wax" + command = [sys.executable, str(ROOT / "tools/llvm-src32/driver.py"), str(HERE / "pie.c"), + "-I", str(ROOT / "sdk/cpt32/include"), "-I", str(ROOT / "foundation/include"), + "--wax3", "--pie", "--page-align", "--image-id", "test.pie", "--user-stack-size", "0x1000", + "-O1", "-o", str(image)] + if clang: + command[3:3] = ["--clang", clang] + subprocess.run(command, check=True) + return image.read_bytes() + + def main(argv=None): parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) parser.add_argument("out", type=Path) @@ -125,6 +138,7 @@ def main(argv=None): "poke_write_vm": poke(0x00600000, "write_code", 0x1000), "poke_exec_vm": poke(0x00500000, "exec_data", 0x1000), "v2app": v2_program(args.out, args.clang), + "pie": pie_program(args.out, args.clang), } # Big-endian words: the same bytes in SRC32 memory, 4-byte aligned. lines = ["/* Generated by foundation/tests/wax/build.py. */", "#include ", ""] diff --git a/foundation/tests/wax/host.c b/foundation/tests/wax/host.c index 8b1aead..d619d25 100644 --- a/foundation/tests/wax/host.c +++ b/foundation/tests/wax/host.c @@ -3,6 +3,9 @@ #include "../../kernel/wax.h" static struct wax_launch launch; +static uint32_t load_base; /* host_set_load_base: where a relocatable executable goes */ + +void host_set_load_base(uint32_t base) { load_base = base; } /* out: error, count, entry, stack_top, stack_size, top, region_count, * regions (start, end, flags) x 8, origins x 8, diag_image, diag_index. */ @@ -10,7 +13,7 @@ int host_plan(const uint8_t *exe, uint32_t size, const uint8_t *const *libs, con uint32_t lib_count, uint32_t window_start, uint32_t window_end, uint8_t *ram, uint32_t ram_base, uint32_t *out) { struct wax_library table[16]; - struct wax_target target = {"foundation", 1, 0, window_start, window_end}; + struct wax_target target = {"foundation", 1, 0, window_start, window_end, load_base}; for (uint32_t i = 0; i < lib_count && i < 16u; ++i) { table[i].data = libs[i]; table[i].size = lib_sizes[i]; @@ -43,3 +46,17 @@ const char *host_diag_name(void) { return launch.diag_name; } int host_base_addr(const uint8_t *data, uint32_t size, uint32_t *base) { return (int)wax_base_addr(data, size, base); } + +/* out: error, base, low, high, relocatable, stack_size, heap_hint. */ +int host_inspect(const uint8_t *data, uint32_t size, uint32_t *out) { + struct wax_info info = {0}; + enum wax_error error = wax_inspect(data, size, &info); + out[0] = error; + out[1] = info.base; + out[2] = info.low; + out[3] = info.high; + out[4] = info.relocatable; + out[5] = info.stack_size; + out[6] = info.heap_hint; + return (int)error; +} diff --git a/foundation/tests/wax/main.c b/foundation/tests/wax/main.c index 47e1a26..e95345a 100644 --- a/foundation/tests/wax/main.c +++ b/foundation/tests/wax/main.c @@ -4,6 +4,8 @@ * test.base, each process with its own instances at different bases; * - poke_write / poke_exec: a store to RX and a jump into RW, both faults; * - v2app: a PROTECTED WAX v2 program in C; + * - pie: a relocatable executable (driver --pie) spawned three times at once, + * each copy in an arena of its own; * - spin: keeps the CPU busy, as src32_testbench treats the idle HALT as * the end of the program. * Rejected spawns change nothing. After the first round app is spawned again @@ -11,6 +13,8 @@ * failed checks. */ #include +#include "../../kernel/mm.h" + #include "images.h" #define IMAGE(name) img_##name, img_##name##_size @@ -26,7 +30,17 @@ static const struct fnd_image libs[] = {{IMAGE(math_old)}, {IMAGE(math)}, {IMAGE static const struct fnd_image base_only[] = {{IMAGE(base)}}; static const struct fnd_image old_math[] = {{IMAGE(math_old)}, {IMAGE(base)}}; -static int32_t pid_app, pid_app2, pid_v2, pid_write, pid_exec; +#define PIES 3u + +static int32_t pid_app, pid_app2, pid_v2, pid_write, pid_exec, pid_pie[PIES]; +static int32_t pid_spin; +static uint32_t pie_lo[PIES], pie_hi[PIES], pool_bytes; + +/* The bytes of arena the live process pid holds. */ +static uint32_t held(int32_t pid) { + struct fnd_proc *p = fnd_proc_get((uint32_t)pid); + return p && p->state == FND_PROC_LIVE ? p->arena_hi - p->arena_lo : 0; +} static uint32_t failures, rounds; static void check(int ok) { @@ -68,11 +82,37 @@ static uint32_t on_tick(uint32_t ticks) { check(exited(pid_exec, (int32_t)0x80050004u) && x->fault_epc == POKE_EXEC_DATA); check(slot_clear(SLOT_APP2)); } + if (rounds == 1) { + /* The relocatable copies: the last one grew in place (42), the others + * were boxed in by the next arena (43); every arena was zeroed and + * went back to the pool. */ + for (uint32_t i = 0; i < PIES; ++i) + check(exited(pid_pie[i], i + 1u == PIES ? 42 : 43) && zero(pie_lo[i], pie_hi[i])); + check(mm_arena_free_bytes() + held(pid_spin) == pool_bytes); + } if (++rounds == ROUNDS) return failures == 0u ? 42u : 0x100u + failures; /* Spawn and exit again: the slot, records and threads are reused. */ pid_app = fnd_proc_spawn_wax("app", FND_PRIO_APP, IMAGE(app), libs, 3, 0, 0); check(pid_app > 0); + if (rounds == 1) { + /* Three copies of one relocatable executable at once (the records of + * the programs that have exited are reused). */ + for (uint32_t i = 0; i < PIES; ++i) { + pid_pie[i] = fnd_proc_spawn_wax("pie", FND_PRIO_APP, IMAGE(pie), 0, 0, 0, 0); + check(pid_pie[i] > 0); + struct fnd_proc *p = fnd_proc_get((uint32_t)pid_pie[i]); + if (!p) + continue; + pie_lo[i] = p->arena_lo; + pie_hi[i] = p->arena_hi; + /* One arena each, above the one before: nothing overlaps. */ + check(p->arena_lo && p->arena_hi > p->arena_lo && (i == 0 || p->arena_lo >= pie_hi[i - 1u])); + } + /* Everything alive now, plus what is free, is the whole pool. */ + pool_bytes = mm_arena_free_bytes() + held(pid_app) + held(pid_spin) + held(pid_pie[0]) + + held(pid_pie[1]) + held(pid_pie[2]); + } return 0; } @@ -91,7 +131,8 @@ int main(void) { pid_v2 = fnd_proc_spawn_wax("v2app", FND_PRIO_APP, IMAGE(v2app), 0, 0, 0, 0); pid_write = fnd_proc_spawn_wax("poke-write", FND_PRIO_APP, IMAGE(poke_write), 0, 0, 0, 0); pid_exec = fnd_proc_spawn_wax("poke-exec", FND_PRIO_APP, IMAGE(poke_exec), 0, 0, 0, 0); - if (fnd_proc_spawn_wax("spin", FND_PRIO_APP, IMAGE(spin), 0, 0, 0, 0) < 0) + pid_spin = fnd_proc_spawn_wax("spin", FND_PRIO_APP, IMAGE(spin), 0, 0, 0, 0); + if (pid_spin < 0) return 2; if (pid_app < 0 || pid_app2 < 0 || pid_v2 < 0 || pid_write < 0 || pid_exec < 0) return 2; diff --git a/foundation/tests/wax/pie.c b/foundation/tests/wax/pie.c new file mode 100644 index 0000000..b523925 --- /dev/null +++ b/foundation/tests/wax/pie.c @@ -0,0 +1,47 @@ +/* Relocatable executable (driver --pie, doc/spec_WAX_v3.md §3.1) for the + * loader test. Static data that holds addresses (a pointer to a global, a + * table of function pointers) only works if the loader relocated it, and the + * code builds the addresses of those objects the same way. It runs several + * times at once, each copy in an arena of its own. + * + * Exit code: 42 when everything checks out and the heap grew past the + * kernel's default reserve in place; 43 when only the growth was refused + * because another arena sits right above (every copy but the last, which + * the test relies on being so: it asks before any copy can finish). + * Anything else is a failure. */ +#include + +static uint32_t counter = 7; +static uint32_t *const where = &counter; + +static uint32_t twice(uint32_t x) { return 2u * x; } +static uint32_t thrice(uint32_t x) { return 3u * x; } +static uint32_t (*const ops[2])(uint32_t) = {twice, thrice}; +static const char *const names[2] = {"twice", "thrice"}; + +int main(void) { + int32_t status = 1; + if (where != &counter || *where != 7u || ops[0] != twice || ops[1](ops[0](5u)) != 30u || + names[1][0] != 't' || names[1][1] != 'h') + fnd_sys_proc_exit(status); + /* The heap, well beyond the reserve, asked for first thing: while the + * copies are still starting, the next arena is there for all but the + * last. */ + int32_t first = fnd_sys_mem_grow(0x30000u); + if (first == FND_E_NO_MEMORY) + fnd_sys_proc_exit(43); + if (first <= 0) + fnd_sys_proc_exit(2); + uint32_t *heap = (uint32_t *)(uintptr_t)first; + for (uint32_t i = 0; i < 0x2000u; ++i) + heap[i] = i ^ (uint32_t)first; + for (uint32_t i = 0; i < 0x2000u; ++i) { + if (heap[i] != (i ^ (uint32_t)first)) + fnd_sys_proc_exit(3); + } + uint32_t *top = (uint32_t *)(uintptr_t)(first + 0x30000 - 4); + *top = 0xC0FFEEu; + fnd_sys_proc_exit(*top == 0xC0FFEEu ? 42 : 4); + for (;;) { + } +} diff --git a/foundation/tests/wax/test_wax_loader.py b/foundation/tests/wax/test_wax_loader.py index f7ebe16..b7fdbf5 100644 --- a/foundation/tests/wax/test_wax_loader.py +++ b/foundation/tests/wax/test_wax_loader.py @@ -21,8 +21,8 @@ ROOT = HERE.parents[2] sys.path.insert(0, str(ROOT / "tools/wax")) import wax # noqa: E402 (v2 packer) -from wax3 import (FUNCTION, OBJECT, PERM_R, PERM_RW, PERM_RX, Block, Dependency, # noqa: E402 - Export, Import, build) +from wax3 import (FUNCTION, OBJECT, PERM_R, PERM_RW, PERM_RX, RELOC_DATA, RELOC_PAIR, Block, # noqa: E402 + Dependency, Export, Import, build) (OK, FORMAT, UNSUPPORTED, DEPENDENCY_MISSING, ABI_MISMATCH, DEPENDENCY_CONFLICT, DEPENDENCY_CYCLE, SYMBOL_MISSING, SYMBOL_MISMATCH, REGION_LIMIT, RESOURCE_LIMIT) = range(11) @@ -69,6 +69,30 @@ def imp(dep=0, name="write", slot=0x100, kind=FUNCTION, signature=SIG, size=0): return Import(dep, name, kind, slot, size, signature if kind == FUNCTION else None) +LDIH, LDIL = 0x3D << 26, 0x3C << 26 +PIE_BASE = 0x110000 + + +def word(value): + return value.to_bytes(4, "big") + + +def pie(*, base=PIE_BASE, code=None, data=None, relocs=None, stack_size=0x1000, **kw): + """A relocatable executable: the stack lies just below base (spec_WAX_v3.md + §3.1). At base + 4 an LDIH/LDIL pair loads base + 0x104 into R2, and at + base + 0x100 a data word holds base + 0x100.""" + if code is None: + code = word(0) + word(LDIH | 2 << 21 | (base + 0x104) >> 16) \ + + word(LDIL | 2 << 21 | (base + 0x104) & 0xFFFF) + bytes(0xF4) + if data is None: + data = word(base + 0x100) + word(0x1234) + bytes(0x78) + if relocs is None: + relocs = [(base + 4) | RELOC_PAIR, (base + 0x100) | RELOC_DATA] + return build("wax", image_id="app", blocks=[Block(base, code, perm=PERM_RX), + Block(base + 0x100, data, perm=PERM_RW)], + entry_offset=0, stack_top=base, stack_size=stack_size, relocs=relocs, **kw) + + class LoaderTests(unittest.TestCase): @classmethod def setUpClass(cls): @@ -83,6 +107,7 @@ def setUpClass(cls): str(HERE / "host.c"), "-o", str(library)], check=True) cls.lib = ctypes.CDLL(str(library)) cls.lib.host_diag_name.restype = ctypes.c_char_p + cls.lib.host_set_load_base.argtypes = [ctypes.c_uint32] @classmethod def tearDownClass(cls): @@ -101,6 +126,9 @@ def plan(self, exe, libs=(), window=WINDOW, fill=0xAA): name = self.lib.host_diag_name() return Result(list(out), ram.raw, name.decode() if name else None) + def tearDown(self): + self.lib.host_set_load_base(0) + def ok(self, exe, libs=(), **kw): result = self.plan(exe, libs, **kw) self.assertEqual(result.error, OK) @@ -251,7 +279,7 @@ def test_protected_v2(self): def test_header_identity(self): for offset, value, error in ((4, 4, UNSUPPORTED), (6, 3, UNSUPPORTED), (8, 2, UNSUPPORTED), - (10, 6, UNSUPPORTED), (10, 0, FORMAT)): + (10, 10, UNSUPPORTED), (10, 6, FORMAT), (10, 0, FORMAT)): image = app() image.data[offset:offset + 2] = value.to_bytes(2, "big") self.rejects(error, image) @@ -449,6 +477,91 @@ def test_symbols(self): imports=[Import(0, "deep", FUNCTION, 0x100, 0, "fn()->void")]), [outer, inner]) + # --- relocatable executables (spec_WAX_v3.md §3.1) --- + + def test_reloc_linked_address_without_load_base(self): + r = self.ok(pie()) + self.assertEqual((r.entry, r.stack_top), (PIE_BASE, PIE_BASE)) + self.assertEqual(r.regions[0], (PIE_BASE - 0x1000, PIE_BASE, PERM_RW)) + self.assertEqual(r.word(PIE_BASE + 4), LDIH | 2 << 21 | 0x11) # unchanged + self.assertEqual(r.word(PIE_BASE + 0x100), PIE_BASE + 0x100) + + def test_reloc_load_base_moves_everything(self): + self.lib.host_set_load_base(0x150000) + r = self.ok(pie()) + self.assertEqual((r.entry, r.stack_top, r.origins[0]), (0x150000, 0x150000, 0x150000)) + self.assertEqual(r.regions, [(0x14F000, 0x150000, PERM_RW), (0x150000, 0x150100, PERM_RX), + (0x150100, 0x150180, PERM_RW)]) + self.assertEqual(r.top, 0x150180) + # The pair now names 0x150104 and the data word 0x150100; the rest is as linked. + self.assertEqual(r.word(0x150004), LDIH | 2 << 21 | 0x15) + self.assertEqual(r.word(0x150008), LDIL | 2 << 21 | 0x0104) + self.assertEqual(r.word(0x150100), 0x150100) + self.assertEqual(r.word(0x150104), 0x1234) + self.assertEqual(r.ram[0x4F000:0x50000], bytes(0x1000)) # the moved stack is zeroed + + def test_reloc_pair_carries_into_the_high_half(self): + # Linked at 0x10FFFC + 0x104 = 0x110100 -> low half 0x0100; moving by 0xFF00 wraps it. + self.lib.host_set_load_base(PIE_BASE + 0xFF00) + r = self.ok(pie()) + value = PIE_BASE + 0x104 + 0xFF00 + self.assertEqual(r.word(PIE_BASE + 0xFF00 + 4) & 0xFFFF, value >> 16) + self.assertEqual(r.word(PIE_BASE + 0xFF00 + 8) & 0xFFFF, value & 0xFFFF) + + def test_reloc_load_base_is_ignored_for_fixed_executables(self): + self.lib.host_set_load_base(0x150000) + self.assertEqual(self.ok(app()).entry, 0x100000) + + def test_reloc_libraries_follow_the_moved_image(self): + self.lib.host_set_load_base(0x150000) + exe = pie(deps=[Dependency("a")], imports=[imp(slot=0x120)], + data=word(PIE_BASE + 0x100) + bytes(0x1C) + bytes(8) + bytes(0x58)) + r = self.ok(exe, [lib("a")], window=(0x14F000, 0x1F0000)) + self.assertEqual(r.count, 2) + self.assertGreaterEqual(r.origins[1], 0x150180) # after the executable's blocks + self.assertEqual(r.word(0x150120), r.origins[1] + 0x20) # the import slot + self.assertEqual(r.word(0x150124), r.origins[1]) + + def test_reloc_inspect(self): + out = (ctypes.c_uint32 * 7)() + image = bytes(pie(heap_hint=0x4000).data) + self.assertEqual(self.lib.host_inspect(image, len(image), out), OK) + self.assertEqual(list(out), [OK, PIE_BASE, PIE_BASE - 0x1000, PIE_BASE + 0x180, 1, 0x1000, 0x4000]) + fixed = bytes(app().data) + self.assertEqual(self.lib.host_inspect(fixed, len(fixed), out), OK) + self.assertEqual(out[4], 0) + + def test_reloc_rejections(self): + base = PIE_BASE + bad = { + "reloc outside every block": dict(relocs=[(base + 0x400) | RELOC_DATA]), + "pair in data": dict(relocs=[(base + 0x100) | RELOC_PAIR]), + "data word in code": dict(relocs=[(base + 4) | RELOC_DATA]), + "pair that is not LDIH/LDIL": dict(relocs=[base | RELOC_PAIR]), + "pair at the end of the code": dict(relocs=[(base + 0xFC) | RELOC_PAIR]), + "duplicate": dict(relocs=[(base + 4) | RELOC_PAIR, (base + 4) | RELOC_PAIR]), + "unknown kind": dict(relocs=[(base + 0x100) | 2]), + "unaligned heap hint": dict(heap_hint=12), + } + for name, kw in bad.items(): + with self.subTest(name): + self.rejects(FORMAT, pie(**kw)) + + def test_reloc_must_be_sorted(self): + image = pie(relocs=[(PIE_BASE + 4) | RELOC_PAIR, (PIE_BASE + 0x100) | RELOC_DATA]) + first, second = image.layout["relocs"], image.layout["relocs"] + 4 + a, b = image.get32(first), image.get32(second) + image.put32(first, b) + image.put32(second, a) + self.rejects(FORMAT, image) + + def test_reloc_flag_needs_a_table_and_a_stack_below_the_image(self): + self.rejects(FORMAT, pie(relocs=[]) if False else build( + "wax", image_id="app", blocks=[Block(PIE_BASE, bytes(0x100), perm=PERM_RX)], + stack_top=PIE_BASE, stack_size=0x1000, relocs=[])) + self.rejects(FORMAT, build( + "wax", image_id="app", blocks=[Block(PIE_BASE, bytes(0x100), perm=PERM_RX), + Block(PIE_BASE + 0x100, bytes(4), perm=PERM_RW)], + stack_top=0x1FF000, stack_size=0x1000, relocs=[(PIE_BASE + 0x100) | RELOC_DATA])) + -if __name__ == "__main__": - unittest.main() diff --git a/memory/2026-10-25-issue-175-nommu-arena.md b/memory/2026-10-25-issue-175-nommu-arena.md new file mode 100644 index 0000000..6f18453 --- /dev/null +++ b/memory/2026-10-25-issue-175-nommu-arena.md @@ -0,0 +1,25 @@ +# nommu のスロット方式をアリーナ方式と再配置可能 WAX に置き換えた(#175) + +V のない CPU(`FND_MM=nommu` / `-D FND_MM_NOMMU_ONLY`)の `mm` バックエンドは、固定スロットをやめて nommu Linux 風になった。仕様は `doc/spec_Foundation.md` §5.2、画像形式は `doc/spec_WAX_v3.md` §3.1。V のある CPU のページ表バックエンドは変えていない。 + +## 契約 + +- ページプールは `[0x400000, RAM 末尾)`、4 KiB、最大 256 MiB のビットマップ。空きページは常にゼロ(プロセス終了時にゼロ化して返す)。 +- プロセスは連続したアリーナ 1 つ。長さは「使用分 + 予備(`heap_hint`、無ければ 128 KiB)」。 +- 固定アドレスの画像は `[min(base, stack 下端), max(終端, stack 上端))` を丸めてちょうどそこを要求する。取れなければ `FND_E_ADDRESS_CONFLICT`。 +- 再配置可能な画像は、試しの基底 `0x01000000` で一度だけ計画してから確保し、`wax_rebase` で本番の基底に動かす(計画を 2 回すると spawn が遅く、nommu のシェルテストが時間切れになった)。 +- `mem_grow` はアリーナを動かせない。ヒープがアリーナの末尾にあり、上のページが空いているときだけ伸びる。大きなヒープが要るプログラムは `--heap-hint` で最初に求める。 +- 再配置可能な WAX のスタックは最下位ブロックの直下(`stack_top == base_addr`)。初期 R30 は配置後の origin。 +- 全 Foundation プログラム(init・各サービス・シェル・`/bin`)は `--wax3 --pie`。`FND_MAX_PROCS`(7)とスレッド 8 はスロットに縛られなくなったが、まだ増やしていない。 + +## ツールチェーン(`--pie`) + +- `driver.py` は PIE_BASE(`0x100000`)と、`0x01000000` ずらした 2 回目のリンクを作り、ブロックの差分から relocation を作る(`v3.relocations`)。データ語は差がちょうど probe、コードは `LDIH` の直後に同じ rd の `LDIL`。説明のつかない差はエラー(アドレスを足す・切り捨てるコードは `--pie` にできない)。 +- relocation entry は上位 30 bit がリンク時アドレス、下位 2 bit が種類(0 データ語、1 LDIH/LDIL 組)。 + +## 落とし穴 + +- llvm-src32 の生成コードは遅い(約 25 命令/操作)。relocation と spawn のループは軽く保つ。`@llvm.fshl` は未対応なので、ビット操作に回転を使わない。 +- ホストテスト(`foundation/tests/wax/host.c`)の PIE_BASE が、ホストの RAM バッファより下にあるとメモリが壊れる(`0x110000` にした)。 +- `tests/wax`・`tests/wal` は物理メモリを直接見るので `FND_MM_NOMMU_ONLY` でビルドする。`tests/shell` などは両モードで通る。free の値は両モードとも N に正規化して比べる。 +- 変更後に確認したもの: `python3 -m unittest discover -s foundation/tests/wax`、`foundation/tests/shell`(V と `FND_MM=nommu` の両方)、disk・fb・sgc・libsgc・tinybasic・wav・sgub・bmpview・chip8・console・pec・panic・fs・lzma(nommu)、`tools/llvm-src32/test_toolchain.py` の phase1/vm/wal。 diff --git a/tools/llvm-src32/README.md b/tools/llvm-src32/README.md index cd7d4e0..0234de1 100644 --- a/tools/llvm-src32/README.md +++ b/tools/llvm-src32/README.md @@ -136,6 +136,7 @@ python tools/llvm-src32/driver.py lib.c --wal --image-id my.lib --abi-version 1. python tools/llvm-src32/driver.py app.c --wax3 --import-wal lib.wal --origin 0x800000 --stack-top 0xC00000 -o app.wax ``` +- `--pie` は WAX v3 を[再配置可能な実行形式](../../doc/spec_WAX_v3.md)(§3.1、nommu Linux の FLAT 形式に相当)にします。`--origin`/`--stack-top` は指定しません(内部の固定アドレス `0x100000` にリンクし、スタックはその直下)。ドライバーは `0x01000000` ずらした 2 回目のリンクと突き合わせて、アドレスを持つデータ語と `LDIH`/`LDIL` の組を見つけ、relocation table に書きます。突き合わせで説明のつかない差やアドレスの半端な使い方(例:アドレスの加算や切り捨て)はエラーです。Foundation のカーネルはこの画像を空いている場所に読み込み、再配置して起動します。`--heap-hint BYTES` は、プログラムが求めるヒープの大きさです(既定は OS の 128 KiB)。 - `--exports FILE` は WAL が公開する記号の一覧です。1 行に 1 つ、関数は名前と [§6](../../doc/spec_WAX_v3.md) の signature(例:`fnd_write fn(u32,ptr,u32,ptr)->i32`)、オブジェクトは `名前 object` と書きます。`#` 以降と空行は無視します。一覧にない関数は外から見えません。 - signature の型は C の型と対応していなければなりません。`u32`/`i32` は 32-bit 整数、`u64`/`i64` は 64-bit 整数、`ptr` はポインタ、戻り値の `void` です(符号は LLVM IR に残らないので、`u32` と `i32` の区別は一覧の記述に従います)。`char`、`short`、`bool`、`float`、`double` の引数や戻り値、構造体の値渡し、可変長引数は境界を越えられないのでエラーです(浮動小数点はビット列を `u32`/`u64` で渡します)。`fnptr`(関数記述子)はまだ対応していません。 - `--import-wal WAL`(複数可)を指定すると、定義のない関数とグローバル変数のうち、その WAL が export しているものを import にします。使わなかった WAL は依存表に入りません。依存の `min_minor` は指定した WAL の `abi_minor` です。C 側は普通に `extern` で宣言して呼びます。 diff --git a/tools/llvm-src32/driver.py b/tools/llvm-src32/driver.py index 79be7b6..7fcee30 100644 --- a/tools/llvm-src32/driver.py +++ b/tools/llvm-src32/driver.py @@ -15,6 +15,8 @@ ROOT = Path(__file__).resolve().parent RUNTIME_NAMES = ("memcpy", "memmove", "memset", "memcmp", "strlen") CXX_SUFFIXES = (".cpp", ".cc", ".cxx") +PIE_BASE = 0x00100000 # link address of a relocatable executable; its stack lies just below +PIE_PROBE = 0x01000000 # how far the second link moves it to find the addresses # OS ABIs a WAX v3 can declare (doc/spec_WAX_v3.md §1): syscall ABI version # and the SYSCALL number the startup code exits with (spec_Foundation.md §12). V3_OSES = {"foundation": {"version": (1, 0), "exit": 0x11}} @@ -74,6 +76,11 @@ def parser() -> argparse.ArgumentParser: help="emit a protected WAX v3 executable at --origin (.wax)") kind.add_argument("--wal", action="store_true", help="emit a position-independent WAX v3 library (.wal) exporting --exports") + v3.add_argument("--pie", action="store_true", + help="with --wax3, emit a relocatable executable: linked at %#x with the stack just below, " + "and a relocation table so the OS can load it anywhere (no-MMU loading)" % PIE_BASE) + v3.add_argument("--heap-hint", type=address, default=0, + help="with --pie, the heap in bytes the program asks the OS to reserve (default: the OS's)") v3.add_argument("--image-id", help="image or library ID (default: the output file name)") v3.add_argument("--abi-version", default="1.0", metavar="MAJOR.MINOR", help="library ABI version, or the executable's contract version (default 1.0)") @@ -209,6 +216,16 @@ def run(args) -> Path: raise DriverError(f"unsupported input extension: {source} (expected .c, .cpp, .cc, .cxx, .ll, or .bc)") if not source.is_file(): raise DriverError(f"input file not found: {source}") + if args.pie and not args.wax3: + raise DriverError("--pie applies to --wax3 only") + if args.pie and args.origin: + raise DriverError("--pie links at a fixed internal address; drop --origin") + if args.heap_hint and not args.pie: + raise DriverError("--heap-hint needs --pie") + if args.heap_hint % 8: + raise DriverError("--heap-hint must be a multiple of 8") + if args.pie: + args.origin = args.stack_top = PIE_BASE if args.origin % 4: raise DriverError("--origin must be 4-byte aligned") if not args.stack_top or args.stack_top % 8: @@ -411,11 +428,14 @@ def build_newlib(item): raise DriverError(f"--import-wal {path}: different call ABI or OS ABI") libraries.append(library) imports = v3_module.resolve_imports(linked, layout, libraries, wax3) - assembly = compile_module(linked, origin=0 if args.wal else args.origin, stack_top=args.stack_top, + def generate(origin, stack_top, info): + return compile_module(linked, origin=origin, stack_top=stack_top, entry=None if args.wal else args.entry, protected=True, - info=layout_info, pic=args.wal, imports=imports.kinds, + info=info, pic=args.wal, imports=imports.kinds, exit_syscall=V3_OSES[args.os]["exit"], page_align=args.page_align, prunable=prunable) + + assembly = generate(0 if args.wal else args.origin, args.stack_top, layout_info) else: assembly = compile_module(linked, origin=args.origin, stack_top=args.stack_top, entry=args.entry, protected=args.protected, info=layout_info, data_origin=args.data_origin, @@ -434,6 +454,17 @@ def build_newlib(item): options = dict(kind="wal", exports=v3_module.exports_table(specs, linked, layout, layout_info, wax3)) \ if args.wal else dict(kind="wax", origin=args.origin, stack_top=args.stack_top, stack_size=args.user_stack_size) + if args.pie: + # Link again PIE_PROBE higher: what moved is an address (spec_WAX_v3.md §3.1). + moved_assembly = generate(args.origin + PIE_PROBE, args.stack_top + PIE_PROBE, {}) + moved_assembler = assembler_module.Assembler(strip=True, sparse=True) + moved_assembler.assemble(moved_assembly, source="", base_dir=str(ROOT)) + moved_blocks = protected_blocks(moved_assembler.blocks, layout_info["data_start"] + PIE_PROBE) + try: + options["relocs"] = v3_module.relocations(wax3, blocks, moved_blocks, PIE_PROBE) + except v3_module.RelocationError as exc: + raise DriverError(f"--pie: {exc}") from exc + options["heap_hint"] = args.heap_hint binary = v3_module.pack(wax3, image_id=image_id, abi=args.abi, blocks=blocks, info=layout_info, imports=imports, os_id=args.os, os_version=V3_OSES[args.os]["version"], **options) diff --git a/tools/llvm-src32/test_toolchain.py b/tools/llvm-src32/test_toolchain.py index df73fce..ad4cc72 100644 --- a/tools/llvm-src32/test_toolchain.py +++ b/tools/llvm-src32/test_toolchain.py @@ -813,7 +813,7 @@ def test_foundation_phase1_kernel(self): image = self.work / ('foundation' + opt[1:] + '.wax') self.driver(*sources, '-I', ROOT / 'sdk/cpt32/include', '-I', foundation / 'include', '--origin', '0x10000', - '--stack-top', '0x100000', '-D', 'FND_MM_SLOT_ONLY', opt, '-o', image) + '--stack-top', '0x100000', '-D', 'FND_MM_NOMMU_ONLY', opt, '-o', image) for cpu in ('scalar', 'cached', 'cached-relaxed', 'pipeline'): with self.subTest(cpu=cpu): result = subprocess.run( @@ -843,7 +843,7 @@ def test_foundation_wax_loader(self): image = self.work / ('wax' + opt[1:] + '.wax') self.driver(*sources, '-I', ROOT / 'sdk/cpt32/include', '-I', foundation / 'include', '-I', images, '--origin', '0x10000', '--stack-top', '0x100000', - '-D', 'FND_MM_SLOT_ONLY', opt, '-o', image) + '-D', 'FND_MM_NOMMU_ONLY', opt, '-o', image) for cpu in ('scalar', 'cached', 'cached-relaxed', 'pipeline'): with self.subTest(cpu=cpu): result = subprocess.run( @@ -899,7 +899,7 @@ def test_foundation_wal_from_c(self): image = images / 'kernel.wax' self.driver(*kernel, '-I', ROOT / 'sdk/cpt32/include', '-I', foundation / 'include', '-I', images, '--origin', '0x10000', '--stack-top', '0x100000', '-O1', - '-D', 'FND_MM_SLOT_ONLY', '-o', image) + '-D', 'FND_MM_NOMMU_ONLY', '-o', image) for cpu in ('scalar', 'pipeline'): with self.subTest(cpu=cpu): result = subprocess.run( diff --git a/tools/llvm-src32/v3.py b/tools/llvm-src32/v3.py index 666a262..6215682 100644 --- a/tools/llvm-src32/v3.py +++ b/tools/llvm-src32/v3.py @@ -165,7 +165,7 @@ def exports_table(specs, module, layout, info, wax3): def pack(wax3, *, kind, image_id, abi, blocks, info, imports, exports=(), origin=0, - stack_top=0, stack_size=0, os_id="foundation", os_version=(1, 0)): + stack_top=0, stack_size=0, os_id="foundation", os_version=(1, 0), relocs=None, heap_hint=0): """blocks: (address, data, perm) from the driver, absolute for a WAX and image-relative (origin 0) for a WAL.""" used = sorted({entry[0] for entry in imports.entries}) @@ -180,4 +180,53 @@ def pack(wax3, *, kind, image_id, abi, blocks, info, imports, exports=(), origin return wax3.build(kind, image_id=image_id, abi_major=abi[0], abi_minor=abi[1], blocks=v3_blocks, entry_offset=0, stack_top=stack_top, stack_size=stack_size, deps=deps, exports=list(exports), imports=table, os_id=os_id, os_major=os_version[0], - os_min_minor=os_version[1]).data + os_min_minor=os_version[1], relocs=relocs, heap_hint=heap_hint).data + + +class RelocationError(Exception): + pass + + +def relocations(wax3, blocks, moved, probe): + """The relocation entries of a relocatable WAX (spec_WAX_v3.md §3.1). + + `blocks` is the image linked at some origin and `moved` the same program + linked `probe` bytes higher (a multiple of 64 KiB): wherever the two + differ, the word holds an address. A data word must differ by exactly + `probe`. In code, the difference must be in an LDIH whose LDIL follows + unchanged, which is how the backend builds every address. Anything else + cannot be moved by the loader and is an error.""" + LDIH, LDIL = 0x3D, 0x3C + if probe & 0xFFFF: + raise ValueError("the probe must be a multiple of 64 KiB") + if len(blocks) != len(moved): + raise RelocationError("the image changes shape when moved") + entries = [] + for (address, data, perm), (other_address, other, other_perm) in zip(blocks, moved): + if other_address != address + probe or len(other) != len(data) or other_perm != perm: + raise RelocationError("the image changes shape when moved") + executable = perm == wax3.PERM_RX + for at in range(0, len(data) - len(data) % 4, 4): + a, b = data[at:at + 4], other[at:at + 4] + if a == b: + continue + old, new = int.from_bytes(a, "big"), int.from_bytes(b, "big") + where = address + at + if executable: + nxt = int.from_bytes(data[at + 4:at + 8], "big") if at + 8 <= len(data) else -1 + if (old >> 26 != LDIH or new >> 26 != LDIH or (old ^ new) >> 16 + or ((new & 0xFFFF) - (old & 0xFFFF)) & 0xFFFF != probe >> 16 + or nxt < 0 or nxt >> 26 != LDIL + or nxt >> 21 & 31 != old >> 21 & 31 or data[at + 4:at + 8] != other[at + 4:at + 8]): + raise RelocationError(f"code at 0x{where:08X} depends on the load address in a way " + "the loader cannot relocate") + entries.append(where | wax3.RELOC_PAIR) + else: + if (new - old) & 0xFFFFFFFF != probe: + raise RelocationError(f"data at 0x{where:08X} depends on the load address in a way " + "the loader cannot relocate") + entries.append(where | wax3.RELOC_DATA) + tail = len(data) % 4 + if tail and data[-tail:] != other[-tail:]: + raise RelocationError("an address is not word aligned in the data") + return entries diff --git a/tools/wax/wax3.py b/tools/wax/wax3.py index c433fba..aae1d46 100644 --- a/tools/wax/wax3.py +++ b/tools/wax/wax3.py @@ -13,7 +13,8 @@ VERSION = 3 TYPE_WAX, TYPE_WAL = 1, 2 ARCH_SRC32 = 1 -FLAG_DEBUG, FLAG_PROTECTED = 1, 2 +FLAG_DEBUG, FLAG_PROTECTED, FLAG_RELOCATABLE = 1, 2, 4 +RELOC_DATA, RELOC_PAIR = 0, 1 # kinds in the low two bits of a relocation entry HEADER_SIZE = 160 CALL_ABI = 1 PERM_R, PERM_RW, PERM_RX = 1, 3, 5 @@ -87,8 +88,13 @@ def add(self, text): def build(kind="wax", *, image_id, blocks, abi_major=1, abi_minor=0, entry_offset=0, stack_top=0, stack_size=0, deps=(), exports=(), imports=(), os_id="foundation", - os_major=1, os_min_minor=0, debug=b"", call_abi=CALL_ABI, protected=True): - """Returns an Image. Tables are written in the order given.""" + os_major=1, os_min_minor=0, debug=b"", call_abi=CALL_ABI, protected=True, + relocs=None, heap_hint=0): + """Returns an Image. Tables are written in the order given. + + relocs makes a WAX relocatable (spec §3.1): a list of link-time addresses, + each with its kind (RELOC_DATA or RELOC_PAIR) in the low two bits; + heap_hint is the heap the program asks for in bytes.""" file_type = {"wax": TYPE_WAX, "wal": TYPE_WAL}[kind] strings = _Strings() image_id_offset = strings.add(image_id) @@ -105,6 +111,9 @@ def build(kind="wax", *, image_id, blocks, abi_major=1, abi_minor=0, entry_offse ("exports", len(export_rows), 24), ("imports", len(import_rows), 24)): layout[name] = at if count else 0 at += count * size + reloc_rows = sorted(relocs) if relocs is not None else [] + layout["relocs"] = at if reloc_rows else 0 + at += 4 * len(reloc_rows) layout["strings"] = at at = _align4(at + len(strings.table)) layout["payloads"] = [] @@ -116,7 +125,8 @@ def build(kind="wax", *, image_id, blocks, abi_major=1, abi_minor=0, entry_offse out = bytearray(at) base = blocks[0].address if file_type == TYPE_WAX and blocks else 0 - flags = (FLAG_PROTECTED if protected else 0) | (FLAG_DEBUG if debug else 0) + flags = (FLAG_PROTECTED if protected else 0) | (FLAG_DEBUG if debug else 0) | \ + (FLAG_RELOCATABLE if relocs is not None else 0) struct.pack_into(">4sHHHHIIIIIII", out, 0, MAGIC, VERSION, file_type, ARCH_SRC32, flags, HEADER_SIZE, at, call_abi, base, entry_offset, stack_top, stack_size) struct.pack_into(">" + "I" * 22, out, 40, @@ -127,6 +137,9 @@ def build(kind="wax", *, image_id, blocks, abi_major=1, abi_minor=0, entry_offse layout["strings"], len(strings.table), image_id_offset, abi_major, abi_minor, os_id_offset, os_major, os_min_minor, layout["debug"], len(debug)) + struct.pack_into(">3I", out, 128, layout["relocs"], len(reloc_rows), heap_hint) + if reloc_rows: + struct.pack_into(">%dI" % len(reloc_rows), out, layout["relocs"], *reloc_rows) for i, block in enumerate(blocks): memory_size = len(block.data) if block.memory_size is None else block.memory_size struct.pack_into(">6I", out, layout["blocks"] + 24 * i, block.address, From 8f3b322befc422bfccb476def0c2f96d1d48c0d4 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 25 Oct 2026 12:11:48 +0000 Subject: [PATCH 2/2] =?UTF-8?q?Foundation:=20relocation=20table=20?= =?UTF-8?q?=E3=81=AE=E6=A4=9C=E8=A8=BC=E3=82=92=201=20=E5=9B=9E=E3=81=AE?= =?UTF-8?q?=E8=B5=B0=E6=9F=BB=E3=81=AB=E3=81=97=E3=80=81=E3=83=87=E3=82=A3?= =?UTF-8?q?=E3=82=B9=E3=82=AF=E8=B5=B7=E5=8B=95=E3=83=86=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=81=AE=E4=BD=99=E8=A3=95=E3=82=92=E5=BA=83=E3=81=92=E3=82=8B?= =?UTF-8?q?=20(#175)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit エントリごとにブロックを探すと、数千エントリの画像で起動が遅くなり、 disk 起動テストの STOP_TICKS に間に合わなくなっていた。 Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_011HTdnvAqSYxxtyGHv9j9d3 --- foundation/kernel/wax.c | 36 ++++++++++++++----------- foundation/tests/disk/test_disk_boot.py | 2 +- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/foundation/kernel/wax.c b/foundation/kernel/wax.c index 341c771..c5b5ac8 100644 --- a/foundation/kernel/wax.c +++ b/foundation/kernel/wax.c @@ -492,29 +492,35 @@ static enum wax_error parse_v3(struct wax_image *im) { /* Relocations (§3.1): ascending, each a whole data word or an LDIH/LDIL * pair in initialized bytes of a block that may hold it. */ - uint32_t previous_at = 0; + uint32_t previous_at = 0, k = 0, loaded = ~0u; + struct block b = {0}; for (uint32_t i = 0; i < im->reloc_count; ++i) { uint32_t entry = word(im, im->reloc_table + 4u * i), at = entry & ~3u, kind = entry & 3u; if (kind > RELOC_PAIR || (i && at <= previous_at) || at < im->base || at - im->base > 0xFFFFFFF0u) return WAX_E_FORMAT; previous_at = at; - uint32_t len = kind == RELOC_PAIR ? 8u : 4u, found = 0; - for (uint32_t k = 0; k < im->block_count && !found; ++k) { - struct block b; - get_block(im, k, &b); - if (b.address <= at && at - b.address <= b.file_size && len <= b.file_size - (at - b.address) && - (kind == RELOC_PAIR ? b.perm == PERM_RX : b.perm != PERM_RX)) { - found = 1; - if (kind == RELOC_PAIR) { - const uint8_t *code = d + b.file_offset + (at - b.address); - if (rd32(code) >> 26 != OP_LDIH || rd32(code + 4) >> 26 != OP_LDIL || - ((rd32(code) ^ rd32(code + 4)) & 0x03E00000u)) - return WAX_E_FORMAT; - } + uint32_t len = kind == RELOC_PAIR ? 8u : 4u; + /* Entries and blocks both ascend, so one cursor walks the blocks (a + * search per entry costs thousands of entries a scan each). */ + while (k < im->block_count) { + if (loaded != k) { + get_block(im, k, &b); + loaded = k; } + if (at >= b.address && at - b.address >= b.file_size) + ++k; + else + break; } - if (!found) + if (k >= im->block_count || at < b.address || len > b.file_size - (at - b.address) || + (kind == RELOC_PAIR ? b.perm != PERM_RX : b.perm == PERM_RX)) return WAX_E_FORMAT; + if (kind == RELOC_PAIR) { + const uint8_t *code = d + b.file_offset + (at - b.address); + if (rd32(code) >> 26 != OP_LDIH || rd32(code + 4) >> 26 != OP_LDIL || + ((rd32(code) ^ rd32(code + 4)) & 0x03E00000u)) + return WAX_E_FORMAT; + } } return WAX_OK; } diff --git a/foundation/tests/disk/test_disk_boot.py b/foundation/tests/disk/test_disk_boot.py index 999860b..11ee74c 100644 --- a/foundation/tests/disk/test_disk_boot.py +++ b/foundation/tests/disk/test_disk_boot.py @@ -30,7 +30,7 @@ # The kernel starts after the 120-frame POST hold and the HDD read (~frame # 125) and stops STOP_TICKS later (360 frames at 60 Hz). fs checks the volume # before init can load /bin/shell.wax, so the banner appears ~115 frames later. -FRAMES = 560 +FRAMES = 600 # the stop lands around frame 550; the margin covers codegen differences between Clang versions TYPE_FRAME = 320 # the shell has printed its banner by then FAIL_FRAMES = 200 # stage1 halts right after the POST hold BANNER = ['Foundation OS (kernel ABI 1.0)', 'Foundation shell 1.0']