From 80dab35a750f4e934d1401d927dca6683f99aa1c Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Fri, 4 Sep 2026 19:13:18 +0800 Subject: [PATCH] =?UTF-8?q?0.9.0=20=E2=80=94=E2=80=94=20armv7a=20=E5=90=8E?= =?UTF-8?q?=E7=AB=AF,=E4=BB=A5=E5=8F=8A=E5=AE=83=E5=9B=9E=E7=AD=94?= =?UTF-8?q?=E7=9A=84=E9=82=A3=E4=B8=AA=E5=AE=BD=E5=BA=A6=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit THE FIRST MACHINE HERE WHOSE PAGE-TABLE ENTRY IS NOT EIGHT BYTES. openarch was designed on three application-class 64-bit machines, and its page table interface carries an entry in an `arch_u64` while saying nothing about how one is STORED. `pte_encode.h` recorded the assumption as a statement of fact: "a page-table entry is 64 bits on every machine here". ARMv7-A's short-descriptor entry is 32 bits. The VALUE fits, so the carrier did not have to change; what was missing is a way to ASK. A kernel sizing a table from `sizeof(arch::pte)` builds one twice as large as the hardware walks, and the walker then reads every second word as an entry. Nothing diagnoses that — the table is well formed, the entries are correct, and the machine reads the gaps. `arch_pte_entry_bytes()` is the answer, and every backend implements it: 8 on riscv64, aarch64 and x86_64, 4 on armv7a, 0 on Cortex-M, where the refusal is already carried by `provides` withholding `openarch:address-space`. CORTEX-M COULD NOT SETTLE THIS, AND IT IS WORTH SAYING WHY. It is a 32-bit machine, which is what the plan expected to surface the assumption — but it has an MPU and no page table, so its pte group exists and refuses. A 32-bit machine WITHOUT paging leaves the question exactly where it was. The machine that settles it is 32-bit AND paged. ## The backend All four groups: cpu (TPIDRPRW/TPIDRURW, which unlike riscv's `tp` do not compete), pte (short-descriptor small pages), context (r4-r11, SP, LR — ten words, the smallest saved context here), trap (an eight-entry vector table of one instruction each). `srsdb`/`rfeia` RATHER THAN FOUR MODE STACKS. Each ARMv7-A exception is taken in its own processor mode with its own banked SP, so the obvious implementation requires a board to allocate and install four stacks before the first exception can be reported — four more things to get wrong in the code that runs before anything can report a fault. `srsdb` writes the return state onto the SVC stack from whichever mode is current, so the kernel's one stack serves every exception. THE RETURN OFFSET IS WRITTEN OUT PER EXCEPTION rather than folded into a macro parameter: 4 for IRQ, FIQ and prefetch abort, 8 for data abort, 0 for SVC and undefined. One wrong value resumes into the middle of an instruction. `openarch:preemption` IS WITHHELD. `arch_trap_switch` asks a trap to resume a different context; here the resumption address is on the SVC stack rather than in a register, so switching stacks mid-trap changes which frame `rfeia` pops and is well defined only if the resumed context was suspended through the same path. That is a real design and not one this backend has measured, so a consumer that needs it is refused by name at resolution. The Cortex-M backend already withholds `openarch:address-space` by the same mechanism. ## What is asserted `tests/pte_encoding` gains the armv7a bit patterns, read off the architecture manual rather than produced by the code under test, and the width property itself: the whole entry must lie in the low 32 bits. A new CI job boots a program on qemu `-M virt -cpu cortex-a15` that reads the width back THROUGH THE ABI and exits 0 only if it is 4 — the half a host test cannot reach. Measured before commit: `armv7a pte width 4, entry fits 32 bits`, exit 0. A second step asks whether the one-line usage REACHES the backend, for both armv7a triples. That is the check 0.8.0 shipped without: it carried a Cortex-M backend `backend-auto` never bound, and everything was green because nothing asked. The job's two fixtures are base64 rather than heredocs. A heredoc inside a YAML block scalar needs its terminator at column zero, which ends the block, and indenting it stops bash recognising it — both spellings fail, and the second fails at run time. --- .github/workflows/ci.yml | 102 +++++++++++++++++++++++ abi/include/openarch/abi.h | 27 +++++- abi/include/openarch/pte_encode.h | 76 ++++++++++++++++- abi/mcpp.toml | 2 +- backends/aarch64/mcpp.toml | 2 +- backends/aarch64/src/pte_impl.cpp | 2 + backends/armv7a/mcpp.toml | 55 +++++++++++++ backends/armv7a/src/context.S | 57 +++++++++++++ backends/armv7a/src/context_init.cpp | 54 ++++++++++++ backends/armv7a/src/cpu_impl.cpp | 49 +++++++++++ backends/armv7a/src/pte_impl.cpp | 40 +++++++++ backends/armv7a/src/trap.S | 89 ++++++++++++++++++++ backends/armv7a/src/trap_impl.cpp | 118 +++++++++++++++++++++++++++ backends/cortex-m/mcpp.toml | 2 +- backends/cortex-m/src/pte_impl.cpp | 3 + backends/riscv64/mcpp.toml | 2 +- backends/riscv64/src/pte_impl.cpp | 2 + backends/x86_64/mcpp.toml | 2 +- backends/x86_64/src/pte_impl.cpp | 3 + mcpp.toml | 14 +++- tests/pte_encoding.cpp | 69 ++++++++++++++++ 21 files changed, 761 insertions(+), 9 deletions(-) create mode 100644 backends/armv7a/mcpp.toml create mode 100644 backends/armv7a/src/context.S create mode 100644 backends/armv7a/src/context_init.cpp create mode 100644 backends/armv7a/src/cpu_impl.cpp create mode 100644 backends/armv7a/src/pte_impl.cpp create mode 100644 backends/armv7a/src/trap.S create mode 100644 backends/armv7a/src/trap_impl.cpp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 273d1f4..fe010cd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -550,6 +550,108 @@ jobs: # encoders are pure functions in per-architecture namespaces, so a host build # holds BOTH and compares them — which is what several of the assertions are, # and what no single-target build could express. + armv7a: + name: the entry is four bytes (armv7a) + runs-on: ubuntu-24.04 + timeout-minutes: 40 + env: + # 2026.9.4.2 is the release carrying the ARMv7-A target rows. The other + # jobs pin 2026.9.4.1 because that is the oldest engine THEY need; pinning + # the newest everywhere would state a dependency that does not exist. + MCPP_VERSION: 2026.9.4.2 + XLINGS_VERSION: v2026.8.17.2 + XLINGS_NON_INTERACTIVE: '1' + # THE TWO FIXTURES ARE base64 AND NOT HEREDOCS, DELIBERATELY. A heredoc + # inside a YAML block scalar needs its terminator at column zero, which + # ends the block; indenting the terminator stops bash recognising it. Both + # spellings fail, and the second fails at run time rather than at parse + # time. An opaque payload has neither problem. + LINK_LD_B64: RU5UUlkoX3N0YXJ0KQpNRU1PUlkgeyBSQU0gKHJ3eCkgOiBPUklHSU4gPSAweDQwMDAwMDAwLCBMRU5HVEggPSAxNk0gfQpTRUNUSU9OUyB7CiAgLnRleHQgOiB7IEtFRVAoKigudGV4dC5zdGFydCkpICooLnRleHQqKSAqKC5yb2RhdGEqKSB9ID4gUkFNCiAgLmRhdGEgOiB7ICooLmRhdGEqKSB9ID4gUkFNCiAgLmJzcyAgOiB7ICooLmJzcyopICooQ09NTU9OKSB9ID4gUkFNCiAgLiA9IEFMSUdOKDE2KTsKICBfX3N0YWNrX3RvcCA9IE9SSUdJTihSQU0pICsgTEVOR1RIKFJBTSk7Cn0K + MAIN_CPP_B64: aW1wb3J0IG1jcHBsaWJzLm9wZW5hcmNoOwpleHRlcm4gIkMiIHVuc2lnbmVkIGFyY2hfcHRlX2VudHJ5X2J5dGVzKHZvaWQpOwoKbmFtZXNwYWNlIHsKaW5saW5lIHZvaWQgc2VtaWhvc3QoaW50IG9wLCBjb25zdCB2b2lkKiBhcmcpIHsKICAgIHJlZ2lzdGVyIGludCByMCBhc20oInIwIikgPSBvcDsKICAgIHJlZ2lzdGVyIGNvbnN0IHZvaWQqIHIxIGFzbSgicjEiKSA9IGFyZzsKICAgIGFzbSB2b2xhdGlsZSgic3ZjIDB4MTIzNDU2IiA6OiAiciIocjApLCAiciIocjEpIDogIm1lbW9yeSIpOwp9CnZvaWQgcHJpbnQoY29uc3QgY2hhciogcykgeyBzZW1paG9zdCgweDA0LCBzKTsgfQpbW25vcmV0dXJuXV0gdm9pZCBxdWl0KGludCBjb2RlKSB7CiAgICBzdHJ1Y3QgeyB1bnNpZ25lZCByZWFzb24sIGNvZGU7IH0gYnsweDIwMDI2dSwgKHVuc2lnbmVkKWNvZGV9OwogICAgc2VtaWhvc3QoMHgyMCwgJmIpOwogICAgZm9yICg7Oykge30KfQp9CgpleHRlcm4gIkMiIHZvaWQga21haW4oKSB7CiAgICBjb25zdCB1bnNpZ25lZCB3ID0gYXJjaF9wdGVfZW50cnlfYnl0ZXMoKTsKICAgIGNvbnN0IGF1dG8gZSA9IGFyY2g6Om1ha2VfbGVhZigweDQwMDAxMDAwLCBhcmNoOjpwZXJtOjpyZWFkX3dyaXRlLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGFyY2g6Om1lbW9yeV90eXBlOjpub3JtYWwsIGZhbHNlKTsKICAgIGNvbnN0IGJvb2wgb2sgPSAodyA9PSA0KSAmJiBhcmNoOjppc192YWxpZChlKQogICAgICAgICAgICAgICAgICAmJiBhcmNoOjpwaHlzX29mKGUpID09IDB4NDAwMDEwMDAgJiYgKChlLmJpdHMgPj4gMzIpID09IDApOwogICAgcHJpbnQob2sgPyAiYXJtdjdhIHB0ZSB3aWR0aCA0LCBlbnRyeSBmaXRzIDMyIGJpdHNcbiIgOiAiYXJtdjdhIHB0ZSBXUk9OR1xuIik7CiAgICBxdWl0KG9rID8gMCA6IDEpOwp9Cgphc20oIi5zZWN0aW9uIC50ZXh0LnN0YXJ0LFwiYXhcIlxuIgogICAgIi5nbG9ibCBfc3RhcnRcbiIKICAgICJfc3RhcnQ6XG4iCiAgICAiICBsZHIgc3AsID1fX3N0YWNrX3RvcFxuIgogICAgIiAgYiBrbWFpblxuIik7Cg== + steps: + - uses: actions/checkout@v4 + + - name: Install xlings + run: | + curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh \ + | bash -s "$XLINGS_VERSION" + echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH" + + - name: Install mcpp + run: | + for attempt in 1 2 3 4 5 6; do + xlings update >/dev/null 2>&1 || true + if xlings install "mcpp@$MCPP_VERSION" -y; then break; fi + echo "index not yet carrying mcpp@$MCPP_VERSION (attempt $attempt); waiting" + sleep 60 + done + mcpp --version + + - name: Install the emulator + run: | + xlings install xim:qemu-arm -y + XLINGS_HOME="$HOME/.mcpp/registry" xlings install xim:qemu-arm -y + + # THE QUESTION THIS BACKEND EXISTS TO ANSWER, ASKED ON THE MACHINE. + # + # openarch was designed on three 64-bit machines with 64-bit page-table + # entries, and `pte_encode.h` recorded that as a fact about every machine + # here. ARMv7-A's short-descriptor entry is 32 bits. `tests/pte_encoding` + # asserts the bit patterns against the architecture manual; this asserts + # that a program running on the machine gets the same answer through the + # ABI, which is the half a host test cannot reach. + # + # Cortex-M could not settle it: it is a 32-bit machine with no page table, + # so its pte group exists and refuses. + - name: A program on the machine reads back a four-byte entry + run: | + set -euo pipefail + QEMU=$(ls "$HOME"/.xlings/data/xpkgs/xim-x-qemu-arm/*/bin/qemu-system-arm | sort -V | tail -1) + D=$(mktemp -d); mkdir -p "$D/src" + printf '[package]\nname="soc"\nversion="0.1.0"\n[build]\ntarget="armv7a-none-eabi"\nsources=["src/main.cpp"]\n[target.armv7a-none-eabi]\nsysroot=""\n[dependencies]\nopenarch = { path = "%s" }\n[targets.soc]\nkind="bin"\nmain="src/main.cpp"\n' \ + "$PWD" > "$D/mcpp.toml" + printf 'import mcpp;\nint main() { mcpp::link_script("link.ld"); return 0; }\n' > "$D/build.mcpp" + echo "$LINK_LD_B64" | base64 -d > "$D/link.ld" + echo "$MAIN_CPP_B64" | base64 -d > "$D/src/main.cpp" + ( cd "$D" && mcpp build ) || { echo "::error::the armv7a probe did not build"; exit 1; } + elf=$(find "$D/target" -type f -name soc | head -1) + [ -n "$elf" ] || { echo "::error::no artefact"; exit 1; } + # NOT `qemu | head`: `$?` after a pipeline is the last command's, so + # the status assertion below would be vacuous. + set +e + timeout 30 "$QEMU" -M virt -cpu cortex-a15 -nographic -semihosting \ + -no-reboot -kernel "$elf" > "$D/qemu.log" 2>&1 + rc=$? + set -e + cat "$D/qemu.log" + grep -q 'armv7a pte width 4' "$D/qemu.log" \ + || { echo "::error::the machine did not report a four-byte entry"; exit 1; } + [ "$rc" = "0" ] || { echo "::error::booted but exited $rc"; exit 1; } + + # THE LESSON 0.8.0 SHIPPED: A BACKEND IN THE PACKAGE IS NOT A BACKEND THE + # ONE-LINE USAGE CAN REACH. That release carried a Cortex-M backend which + # `backend-auto` never bound, and everything was green because nothing + # asked this question. + - name: The one-line usage reaches the armv7a backend + run: | + set -euo pipefail + n=0 + for t in armv7a-none-eabi armv7a-none-eabihf; do + D=$(mktemp -d); mkdir -p "$D/src" + printf '[package]\nname="oap"\nversion="0.1.0"\n[build]\ntarget="%s"\nsources=["src/main.cpp"]\n[target.%s]\nsysroot=""\n[dependencies]\nopenarch = { path = "%s" }\n' \ + "$t" "$t" "$PWD" > "$D/mcpp.toml" + printf 'int main(){return 0;}\n' > "$D/src/main.cpp" + ( cd "$D" && mcpp build --verbose > v.log 2>&1 ) || true + c=$(grep -c 'openarch-armv7a' "$D/v.log" || true) + [ "$c" -gt 0 ] || { + echo "::error::$t resolved no armv7a backend through backend-auto" + tail -20 "$D/v.log"; exit 1; } + echo " ok $t binds the backend ($c compile lines)" + n=$((n+1)); rm -rf "$D" + done + # A count, because a loop that ran zero times also reaches this line. + [ "$n" = "2" ] || { echo "::error::checked $n spellings, expected 2"; exit 1; } + host-encoders: name: the page-table encoders agree (${{ matrix.os }}) runs-on: ${{ matrix.os }} diff --git a/abi/include/openarch/abi.h b/abi/include/openarch/abi.h index 090e628..376e67e 100644 --- a/abi/include/openarch/abi.h +++ b/abi/include/openarch/abi.h @@ -112,10 +112,33 @@ int arch_pte_valid(arch_u64 bits); arch_u64 arch_pte_phys(arch_u64 bits); /* Programs whatever the machine needs before a memory type is meaningful. - * Empty on riscv64, where the type is in the entry; writes `MAIR_EL1` on - * aarch64, where the entry holds only an index into it. */ + * Empty on riscv64 and armv7a, where the type is in the entry; writes + * `MAIR_EL1` on aarch64, where the entry holds only an index into it. */ void arch_pte_install_memory_attributes(void); +/* HOW WIDE THE MACHINE'S PAGE-TABLE ENTRY ACTUALLY IS, IN BYTES. + * + * The three functions above carry an entry in an `arch_u64` and say nothing + * about how it is STORED. That was invisible while every backend was a 64-bit + * machine with 64-bit entries, and `pte_encode.h` said so in as many words: "a + * page-table entry is 64 bits on every machine here". It is not. ARMv7-A's + * short-descriptor entry is 32 bits, and a caller who sizes a table from + * `sizeof(arch::pte)` builds one twice as large as the hardware walks — every + * second word read as an entry, with no diagnostic anywhere. + * + * The value fits in `arch_u64` on both, so the carrier did not have to change. + * What was missing is a way to ASK, which is what this adds. A caller writes + * + * entries * arch_pte_entry_bytes() + * + * and gets the right size on both classes of machine. + * + * Returns 8 on riscv64, aarch64 and x86_64; 4 on armv7a. A backend with no + * address space (Cortex-M) returns 0, which is the same answer its `provides` + * already gives by withholding `openarch:address-space` — a caller that got + * this far has a bug in its own layering. */ +arch_u32 arch_pte_entry_bytes(void); + /* ── openarch.trap ───────────────────────────────────────────────────────── * * ⚠️ THE LAYOUT IS FROZEN AND IS SHARED WITH ASSEMBLY. Each backend's entry diff --git a/abi/include/openarch/pte_encode.h b/abi/include/openarch/pte_encode.h index 1f7a421..0ee631b 100644 --- a/abi/include/openarch/pte_encode.h +++ b/abi/include/openarch/pte_encode.h @@ -36,7 +36,9 @@ * * ⚠️ `arch_u64` THROUGHOUT, AND CROSS-PLATFORM CI IS WHAT MADE THAT NECESSARY. * - * A page-table entry is 64 bits on every machine here. These constants were + * A page-table entry is 64 bits on every machine here EXCEPT armv7a, whose + * short-descriptor entry is 32 bits and fits in the low half; see that + * namespace and `arch_pte_entry_bytes()`. These constants were * once `unsigned long`, which is 64 bits on the systems this was written on and * 32 on Windows — so `1UL << 53` was a shift wider than the type: undefined, * and in practice silently zero rather than an error. An encoder built there @@ -308,6 +310,78 @@ inline arch_u64 entry_phys(arch_u64 bits) noexcept { } // namespace x86_64 +// ─── armv7a ──────────────────────────────────────────────────────────────── +// +// THE FIRST MACHINE HERE WHOSE ENTRY IS NOT 64 BITS. +// +// ARMv7-A's short-descriptor format uses a 32-bit second-level entry. Every +// constant below therefore fits in the low half of `arch_u64`, and the carrier +// does not have to change: what changes is that a caller can no longer assume +// the STORAGE is eight bytes. `arch_pte_entry_bytes()` is what answers that, +// and this namespace is the reason it exists. +// +// The type is entirely in the entry, as on riscv64 and unlike aarch64: there +// is no MAIR to program, so `arch_pte_install_memory_attributes` has nothing +// to do on this machine. +namespace armv7a { + +// A small-page (4 KiB) second-level descriptor. Bit 1 selects the small-page +// encoding and bit 0 is XN, so the two together are `0b10` for executable and +// `0b11` for not — a pair of adjacent bits meaning unrelated things, which is +// the kind of detail this layer exists to keep out of a kernel. +inline constexpr arch_u64 kSmallPage = 1ULL << 1; +inline constexpr arch_u64 kXn = 1ULL << 0; + +inline constexpr arch_u64 kB = 1ULL << 2; // bufferable +inline constexpr arch_u64 kC = 1ULL << 3; // cacheable +inline constexpr arch_u64 kAp0 = 1ULL << 4; +inline constexpr arch_u64 kAp1 = 1ULL << 5; +inline constexpr arch_u64 kTex0 = 1ULL << 6; +inline constexpr arch_u64 kAp2 = 1ULL << 9; +inline constexpr arch_u64 kS = 1ULL << 10; // shareable + +// Bits [31:12]. Written as a 32-bit mask because the descriptor is 32 bits; +// `arch_u64` is the carrier, not the width. +inline constexpr arch_u64 kAddrMask = 0xFFFFF000ULL; + +// AP[2:1] with AP[0] set is the access-permission encoding used throughout. +// `0b01` (AP2=0, AP1=0, AP0=1) is read/write at PL1 only; `0b011` adds +// unprivileged access; setting AP2 makes the mapping read-only. +inline arch_u64 encode_leaf(arch_u64 phys, int perm, int mt, + bool user) noexcept { + arch_u64 e = kSmallPage | kAp0; + + const bool writable = (perm == 1 || perm == 3); + const bool executable = (perm == 2 || perm == 3); + + if (user) e |= kAp1; // reachable from PL0 + if (!writable) e |= kAp2; // read-only + if (!executable) e |= kXn; + + // Normal write-back write-allocate is TEX=0b001 with C and B set; Device + // (shareable) is TEX=0b000, C=0, B=1. Anything unrecognised is treated as + // Device, which is the restrictive choice: a mapping wrongly marked + // cacheable is a silent corruption, one wrongly marked Device is slow. + if (mt == 0) e |= kTex0 | kC | kB | kS; + else e |= kB; + + return e | (phys & kAddrMask); +} + +// A short descriptor is invalid when bits [1:0] are 0b00. Bit 1 alone +// distinguishes a small page from the large-page and fault encodings, which is +// why the test is on that bit and not on bit 0 — bit 0 is XN here. +inline bool entry_valid(arch_u64 bits) noexcept { + return (bits & kSmallPage) != 0; +} + +inline arch_u64 entry_phys(arch_u64 bits) noexcept { + if (!entry_valid(bits)) return 0; + return bits & kAddrMask; +} + +} // namespace armv7a + } // namespace arch #endif diff --git a/abi/mcpp.toml b/abi/mcpp.toml index 97513f8..3e34af8 100644 --- a/abi/mcpp.toml +++ b/abi/mcpp.toml @@ -24,7 +24,7 @@ [package] namespace = "mcpplibs" name = "openarch-abi" -version = "0.7.0" +version = "0.9.0" description = "openarch's C ABI: the contract between the interface and an instruction set's backend" license = "Apache-2.0" authors = ["mcpplibs"] diff --git a/backends/aarch64/mcpp.toml b/backends/aarch64/mcpp.toml index a7f2340..992d52c 100644 --- a/backends/aarch64/mcpp.toml +++ b/backends/aarch64/mcpp.toml @@ -14,7 +14,7 @@ [package] namespace = "mcpplibs" name = "openarch-aarch64" -version = "0.7.0" +version = "0.9.0" description = "openarch's aarch64 backend: the instructions behind the ABI" license = "Apache-2.0" authors = ["mcpplibs"] diff --git a/backends/aarch64/src/pte_impl.cpp b/backends/aarch64/src/pte_impl.cpp index 6b324c4..d564cff 100644 --- a/backends/aarch64/src/pte_impl.cpp +++ b/backends/aarch64/src/pte_impl.cpp @@ -33,3 +33,5 @@ extern "C" void arch_pte_install_memory_attributes(void) { asm volatile("msr mair_el1, %0\n\tisb" :: "r"(kMair) : "memory"); } +// AArch64 long-descriptor entries are eight bytes. +extern "C" arch_u32 arch_pte_entry_bytes(void) { return 8; } diff --git a/backends/armv7a/mcpp.toml b/backends/armv7a/mcpp.toml new file mode 100644 index 0000000..9efce52 --- /dev/null +++ b/backends/armv7a/mcpp.toml @@ -0,0 +1,55 @@ +# openarch's armv7a backend: instructions, and nothing else. +# +# THIS PACKAGE EXPORTS NO MODULE. It defines the C entry points declared in +# `abi/include/openarch/abi.h` and is reached only through them, exactly as the +# other four backends are. +# +# THE MACHINE THAT ANSWERED THE WIDTH QUESTION. +# +# openarch was designed on three application-class 64-bit machines, and its +# page-table interface carried an entry in an `arch_u64` without ever saying how +# one is stored — `pte_encode.h` recorded the assumption as a statement of fact: +# "a page-table entry is 64 bits on every machine here". ARMv7-A is the first +# 32-bit machine here WITH an address space, and its short-descriptor entry is +# 32 bits. The value fits; the storage does not. `arch_pte_entry_bytes()` was +# added for this backend and is implemented by all five. +# +# Cortex-M did not settle it and could not: M-profile has an MPU and no page +# table, so its pte group exists and refuses. A 32-bit machine WITHOUT paging +# leaves the question exactly where it was. +[package] +namespace = "mcpplibs" +name = "openarch-armv7a" +version = "0.9.0" +description = "openarch's armv7a backend: the instructions behind the ABI, on the first machine here whose page-table entry is 32 bits" +license = "Apache-2.0" +authors = ["mcpplibs"] +repo = "https://github.com/mcpplibs/openarch" + +# `openarch:preemption` IS WITHHELD, AND THAT IS A STATEMENT ABOUT THIS MACHINE +# RATHER THAN AN OMISSION. +# +# `arch_trap_switch` asks a trap to resume a different context. On riscv64 and +# aarch64 the resumption address is a register the dispatcher saves and restores +# around the switch; on Cortex-M a dedicated exception performs it. On ARMv7-A +# it is written to the SVC stack by `srs`, so switching stacks mid-trap changes +# which return frame is popped — well-defined only if the resumed context was +# suspended through the same path. That is a real design, and it is not one +# this backend has measured. A consumer that needs preemption is therefore +# refused by name at resolution rather than linking against something that +# compiles and misbehaves. +# +# This is the mechanism the Cortex-M backend already uses to withhold +# `openarch:address-space`, applied to a different group. +provides = ["openarch-backend", + "openarch:address-space", + "openarch:percpu-register"] + +[build] +sources = ["src/**"] + +[targets.openarch-armv7a] +kind = "lib" + +[dependencies] +openarch-abi = { path = "../../abi" } diff --git a/backends/armv7a/src/context.S b/backends/armv7a/src/context.S new file mode 100644 index 0000000..0c8b415 --- /dev/null +++ b/backends/armv7a/src/context.S @@ -0,0 +1,57 @@ +/* openarch.context — the armv7a backend's switch and trampoline. + * + * AAPCS names r4-r11 as callee-saved, plus SP (r13) and LR (r14). Ten words, + * forty bytes — the smallest saved context of any backend here, and the reason + * is the register file rather than any choice this file makes. + * + * d8-d15 ARE ALSO CALLEE-SAVED AND ARE DELIBERATELY NOT SAVED, which is the + * interface's contract and not this backend's shortcut: riscv64 saves none of + * fs0-fs11 and aarch64 none of d8-d15 for the same reason. A saved context in + * openarch is INTEGER state; a kernel that lets its tasks use floating point + * saves that state itself or compiles with floating point disabled. + * + * r9 IS SAVED AND RESTORED LIKE ANY OTHER. Some ARM ABIs reserve it as a + * static base or a thread register, and a kernel built with those conventions + * would want it left alone — but this backend cannot know which convention its + * consumer used, and saving a register that did not need saving is harmless + * where failing to save one is not. + */ + .syntax unified + .arm + + .section .text.arch_context_switch,"ax",%progbits + .globl arch_context_switch + .type arch_context_switch, %function + .balign 4 +arch_context_switch: /* r0 = &from, r1 = &to */ + stm r0, {r4-r11} /* [0 .. 31] callee-saved */ + str sp, [r0, #32] + str lr, [r0, #36] + + ldm r1, {r4-r11} + ldr sp, [r1, #32] + ldr lr, [r1, #36] + bx lr + .size arch_context_switch, . - arch_context_switch + +/* The initial return address of a context built by arch_context_init. + * + * Reached by `bx lr`, not by a call, so it has no caller and must not return. + * r4 and r5 carry the entry point and its argument: they are callee-saved, + * therefore restored by the switch above, which is exactly the property that + * lets an argument survive a transfer restoring no argument register. + */ + .section .text.arch_context_entry,"ax",%progbits + .globl arch_context_entry + .type arch_context_entry, %function + .balign 4 +arch_context_entry: + mov r0, r5 /* arg */ + blx r4 /* entry */ + /* `entry` returning is a contract violation with nowhere to go. Spinning + * is the honest response: it neither corrupts state nor pretends to + * recover. */ +1: b 1b + .size arch_context_entry, . - arch_context_entry + + .section .note.GNU-stack,"",%progbits diff --git a/backends/armv7a/src/context_init.cpp b/backends/armv7a/src/context_init.cpp new file mode 100644 index 0000000..25dac55 --- /dev/null +++ b/backends/armv7a/src/context_init.cpp @@ -0,0 +1,54 @@ +// openarch.context — the part of the armv7a backend that can be C++. +// +// `arch_context_init` writes a saved-context image and never touches the live +// stack pointer, so unlike the switch itself it has no reason to be assembly. +// +// `openarch/types.h` AND NOT `openarch/abi.h`: this file needs the ABI's +// WIDTHS rather than its declarations. The register-file mirror below is this +// backend's own layout, not something the contract names. +#include + +extern "C" void arch_context_entry(); // the trampoline in context.S + +namespace { + +// The saved image, in the order context.S stores it: r4-r11 as one `stm`, then +// SP and LR. +struct Saved { + arch_u32 r[8]; // r4 .. r11 + arch_u32 sp; + arch_u32 lr; +}; + +static_assert(sizeof(Saved) == 10 * 4, + "context.S stores ten words"); +static_assert(sizeof(Saved) <= 128, + "arch::context reserves 128 bytes; armv7a needs 40"); + +constexpr int kR4 = 0; // entry point +constexpr int kR5 = 1; // its argument + +} // namespace + +extern "C" void arch_context_init(void* ctx, void (*entry)(void*), void* arg, + void* stack_top) noexcept { + auto* s = static_cast(ctx); + + // AAPCS requires the stack pointer to be 8-byte aligned at every public + // interface. Aligning down here rather than requiring the caller to do it: + // a kernel computing `base + size` has no reason to know this + // architecture's alignment, and the fault an unaligned SP produces points + // nowhere near the cause. + const auto top = reinterpret_cast(stack_top) & ~7U; + + for (int i = 0; i < 8; ++i) s->r[i] = 0; + + // Not r0/r1: those are argument registers and the switch restores none of + // them — r0 holds `&from` on the way in. Callee-saved registers are the + // only ones that survive, which is why the trampoline reads them rather + // than being handed parameters. + s->r[kR4] = reinterpret_cast(entry); + s->r[kR5] = reinterpret_cast(arg); + s->sp = top; + s->lr = reinterpret_cast(&arch_context_entry); +} diff --git a/backends/armv7a/src/cpu_impl.cpp b/backends/armv7a/src/cpu_impl.cpp new file mode 100644 index 0000000..50970a8 --- /dev/null +++ b/backends/armv7a/src/cpu_impl.cpp @@ -0,0 +1,49 @@ +// openarch.cpu — the armv7a backend. +#include + +// TPIDRPRW (c13, c0, 4) is the privileged-only thread ID register: readable and +// writable at PL1, invisible to a program at PL0. It is this machine's peer of +// aarch64's TPIDR_EL1 and, like it, cannot collide with a program's own thread +// pointer. +extern "C" void* arch_cpu_percpu(void) { + void* p; + asm volatile("mrc p15, 0, %0, c13, c0, 4" : "=r"(p)); + return p; +} + +extern "C" void arch_cpu_set_percpu(void* p) { + asm volatile("mcr p15, 0, %0, c13, c0, 4" :: "r"(p)); +} + +extern "C" void arch_cpu_fence(int b) { + switch (b) { + // The same four intents the other backends implement, spelled in this + // machine's instructions. `dmb` orders; `dsb` waits; `isb` discards + // what the fetch path has already decided. + // + // `ish` — inner shareable — rather than `sy`: a kernel's own data + // structures are shared with the other cores in its inner domain, and + // the full-system domain would also order against agents this layer + // never speaks for. `dsb` keeps `sy` because its callers are waiting + // for a device to have observed a write. + case 0: asm volatile("dmb ish" ::: "memory"); break; + case 1: asm volatile("dmb ishst" ::: "memory"); break; + case 2: asm volatile("dsb sy" ::: "memory"); break; + case 3: asm volatile("isb" ::: "memory"); break; + default: break; + } +} + +// TPIDRURW (c13, c0, 2) is the one a program reaches, and it is a DIFFERENT +// register from TPIDRPRW above — so on this machine, as on aarch64 and unlike +// riscv, the per-CPU slot and the thread pointer do not compete for one +// register. +extern "C" void* arch_cpu_tls(void) { + void* p; + asm volatile("mrc p15, 0, %0, c13, c0, 2" : "=r"(p)); + return p; +} + +extern "C" void arch_cpu_set_tls(void* p) { + asm volatile("mcr p15, 0, %0, c13, c0, 2" :: "r"(p)); +} diff --git a/backends/armv7a/src/pte_impl.cpp b/backends/armv7a/src/pte_impl.cpp new file mode 100644 index 0000000..e0ffdc0 --- /dev/null +++ b/backends/armv7a/src/pte_impl.cpp @@ -0,0 +1,40 @@ +// openarch.pte — the armv7a backend, and the machine that answered the width +// question. +// +// THIS IS THE FIRST BACKEND WHOSE PAGE-TABLE ENTRY IS NOT EIGHT BYTES. +// +// The interface carries an entry in an `arch_u64` and, until this backend, said +// nothing about how one is stored — because it did not have to. `pte_encode.h` +// stated the assumption outright: "a page-table entry is 64 bits on every +// machine here". ARMv7-A's short-descriptor entry is 32 bits. +// +// The VALUE fits, so the carrier did not change. What was missing is a way to +// ask for the storage width: a kernel sizing a table from `sizeof(arch::pte)` +// builds one twice as large as the hardware walks, and the walker then reads +// every second word as an entry. Nothing diagnoses that — the table is +// well-formed, the entries are correct, and the machine reads the gaps. +// `arch_pte_entry_bytes()` exists because of this file. +#include +#include + +extern "C" arch_u64 arch_pte_make_leaf(arch_u64 phys, int perm, int mt, + int user) { + return arch::armv7a::encode_leaf(phys, perm, mt, user != 0); +} + +extern "C" int arch_pte_valid(arch_u64 bits) { + return arch::armv7a::entry_valid(bits) ? 1 : 0; +} + +extern "C" arch_u64 arch_pte_phys(arch_u64 bits) { + return arch::armv7a::entry_phys(bits); +} + +// Nothing to program. The short-descriptor format carries the whole memory +// type in the entry, as riscv64 does and unlike aarch64, where the entry holds +// only an index into MAIR_EL1. An empty body here is the same statement the +// riscv64 backend makes. +extern "C" void arch_pte_install_memory_attributes(void) {} + +// Four, and this is the number the interface had no way to report. +extern "C" arch_u32 arch_pte_entry_bytes(void) { return 4; } diff --git a/backends/armv7a/src/trap.S b/backends/armv7a/src/trap.S new file mode 100644 index 0000000..23fdb79 --- /dev/null +++ b/backends/armv7a/src/trap.S @@ -0,0 +1,89 @@ +/* openarch.trap — the armv7a vector table and entry stubs. + * + * EIGHT ENTRIES OF ONE INSTRUCTION EACH, AND THAT IS THE STRUCTURAL DIFFERENCE + * THIS BACKEND HAS TO HIDE. + * + * riscv writes one address into `mtvec` and reads a cause register. aarch64 + * has sixteen 128-byte slots. ARMv7-A has eight slots of FOUR BYTES — room for + * a single branch and nothing else — and, unlike either, each exception is + * taken in its OWN PROCESSOR MODE with its own banked SP and LR. So "what + * happened" is carried by which slot ran, exactly as on aarch64, and "where to + * return to" is carried in a register that only that mode can see. + * + * EACH MODE WOULD OTHERWISE NEED ITS OWN STACK, AND THIS BACKEND REFUSES TO + * REQUIRE THAT. A board would have to allocate and install four of them before + * the first exception, which is four more things to get wrong in the code that + * runs before anything can report a fault. `srsdb` writes the return address + * and SPSR onto the SVC-mode stack instead, and `cps` then switches to SVC + * mode — so the kernel's one stack serves every exception, and a board that + * set up SVC's stack to run C at all has already done everything needed. + * + * THE RETURN OFFSET DIFFERS PER EXCEPTION AND IS APPLIED BEFORE `srsdb`. + * ARMv7-A leaves LR pointing past the faulting instruction by an amount that + * depends on how the exception was taken: 4 for IRQ, FIQ and prefetch abort, + * 8 for data abort, 0 for SVC and undefined. Getting one wrong resumes into + * the middle of an instruction, which is why they are written out here rather + * than folded into a shared macro with a parameter nobody would check. + */ + .syntax unified + .arm + + .section .text.arch_vectors,"ax",%progbits + .globl arch_vector_table + .type arch_vector_table, %object + .balign 32 /* VBAR requires 32-byte alignment */ +arch_vector_table: + b arch_v_reset /* 0x00 reset */ + b arch_v_undef /* 0x04 undefined */ + b arch_v_svc /* 0x08 supervisor call */ + b arch_v_pabt /* 0x0C prefetch abort */ + b arch_v_dabt /* 0x10 data abort */ + b arch_v_reset /* 0x14 (reserved) */ + b arch_v_irq /* 0x18 IRQ */ + b arch_v_fiq /* 0x1C FIQ */ + .size arch_vector_table, . - arch_vector_table + +/* One stub: correct LR, push the return state onto the SVC stack, switch to + * SVC mode, save the caller-saved set, and join the common path with the slot + * index in r0. + * + * `#0x13` is SVC mode. `srsdb sp!, #0x13` stores LR and SPSR to the SVC stack + * whichever mode we are currently in, which is the whole reason this works + * from four different modes with one stack. + */ +.macro STUB name, idx, adjust + .globl \name + .type \name, %function + .balign 4 +\name: +.if \adjust + sub lr, lr, #\adjust +.endif + srsdb sp!, #0x13 + cps #0x13 + push {r0-r3, r12, lr} + mov r0, #\idx + bl arch_trap_common_c + pop {r0-r3, r12, lr} + rfeia sp! + .size \name, . - \name +.endm + + STUB arch_v_undef, 1, 0 + STUB arch_v_svc, 2, 0 + STUB arch_v_pabt, 3, 4 + STUB arch_v_dabt, 4, 8 + STUB arch_v_irq, 6, 4 + STUB arch_v_fiq, 7, 4 + +/* Reset and the reserved slot have no context to preserve and nowhere to + * return to. Spinning is the honest response, and it is the state a debugger + * can be attached to. */ + .globl arch_v_reset + .type arch_v_reset, %function + .balign 4 +arch_v_reset: +1: b 1b + .size arch_v_reset, . - arch_v_reset + + .section .note.GNU-stack,"",%progbits diff --git a/backends/armv7a/src/trap_impl.cpp b/backends/armv7a/src/trap_impl.cpp new file mode 100644 index 0000000..ad58288 --- /dev/null +++ b/backends/armv7a/src/trap_impl.cpp @@ -0,0 +1,118 @@ +// openarch.trap — the armv7a dispatcher. +// +// The stubs in trap.S hand this the slot index; the fault registers say the +// rest. DFSR and IFSR carry the status of a data and a prefetch abort, DFAR +// and IFAR the address each faulted on. +#include + +namespace { + +arch_trap_handler_fn g_handler = nullptr; + +inline arch_u32 read_dfsr() noexcept { + arch_u32 v; asm volatile("mrc p15, 0, %0, c5, c0, 0" : "=r"(v)); return v; +} +inline arch_u32 read_ifsr() noexcept { + arch_u32 v; asm volatile("mrc p15, 0, %0, c5, c0, 1" : "=r"(v)); return v; +} +inline arch_u32 read_dfar() noexcept { + arch_u32 v; asm volatile("mrc p15, 0, %0, c6, c0, 0" : "=r"(v)); return v; +} +inline arch_u32 read_ifar() noexcept { + arch_u32 v; asm volatile("mrc p15, 0, %0, c6, c0, 2" : "=r"(v)); return v; +} + +// The slot indices trap.S passes, and the `arch_trap_kind` each maps onto. +// Kind 0 is a breakpoint, 1 a page fault, 2 an illegal instruction, 3 a +// misaligned access, 4 an interrupt, 5 anything else. +int classify(arch_u32 slot, arch_u32 fsr) noexcept { + switch (slot) { + case 1: return 2; // undefined instruction + case 2: return 0; // supervisor call — a deliberate trap + case 6: case 7: return 4; // IRQ, FIQ + case 3: case 4: break; // abort: ask the fault status + default: return 5; + } + // The status field is bits [3:0] with bit 10 as its high bit. Alignment + // faults are 0b00001; translation faults are 0b00101 and 0b00111. Anything + // else is reported as itself rather than guessed at. + const arch_u32 status = (fsr & 0xF) | ((fsr >> 6) & 0x10); + if (status == 0x01) return 3; // alignment + if (status == 0x05 || status == 0x07) return 1; // translation + if (status == 0x03 || status == 0x06) return 1; // access flag + if (status == 0x0D || status == 0x0F) return 1; // permission + return 5; +} + +} // namespace + +extern "C" { + +// THIS BACKEND DOES NOT PROVIDE `openarch:preemption`, AND THE MANIFEST SAYS SO +// RATHER THAN THIS FILE PRETENDING. +// +// `arch_trap_switch` asks the trap to resume a DIFFERENT context. On riscv and +// aarch64 the resumption address lives in a register the dispatcher can save +// and restore around the switch; on Cortex-M a dedicated exception performs it. +// Here it lives on the SVC stack, written by `srsdb` — so switching stacks +// mid-trap changes which return frame `rfeia` will pop, and the context being +// resumed must have been suspended through this same path for that to be +// well-defined. That is a real design and it is not one this backend has +// measured, so the capability is withheld at resolution instead of being +// offered and being wrong. +// +// A consumer that requires `openarch:preemption` is refused by name, before +// anything is compiled. That is the same mechanism by which the Cortex-M +// backend withholds `openarch:address-space`. + +void arch_trap_common_c(arch_u32 slot); + +void arch_trap_common_c(arch_u32 slot) { + arch_trap_frame f{}; + const arch_u32 fsr = (slot == 3) ? read_ifsr() : read_dfsr(); + + f.kind = classify(slot, fsr); + f.cause = static_cast(fsr) | (static_cast(slot) << 32); + f.addr = (f.kind == 1 || f.kind == 3) + ? ((slot == 3) ? read_ifar() : read_dfar()) + : 0; + // THE RESUMPTION ADDRESS IS ON THE STACK HERE, NOT IN A REGISTER, so it is + // reported and not writable: a handler that changes `f.pc` changes nothing + // on this machine. Saying so is better than accepting the write and + // discarding it, which is what a field the dispatcher never reads back + // would do. + f.pc = 0; + f.instr_len = 4; + + if (g_handler) g_handler(&f); +} + +arch_trap_handler_fn arch_trap_set_handler(arch_trap_handler_fn h) { + const auto prev = g_handler; + g_handler = h; + + extern unsigned char arch_vector_table[]; + const auto base = reinterpret_cast(arch_vector_table); + // VBAR, and SCTLR.V cleared so that VBAR is consulted at all: with V set + // the vectors are at the architectural high address and this register is + // ignored, which would look exactly like a handler that never fires. + asm volatile("mcr p15, 0, %0, c12, c0, 0" :: "r"(base) : "memory"); + arch_u32 sctlr; + asm volatile("mrc p15, 0, %0, c1, c0, 0" : "=r"(sctlr)); + sctlr &= ~(1U << 13); + asm volatile("mcr p15, 0, %0, c1, c0, 0\n\tisb" :: "r"(sctlr) : "memory"); + return prev; +} + +void arch_trap_enable_interrupts(int on) { + if (on) asm volatile("cpsie i" ::: "memory"); + else asm volatile("cpsid i" ::: "memory"); +} + +int arch_trap_interrupts_enabled(void) { + arch_u32 cpsr; + asm volatile("mrs %0, cpsr" : "=r"(cpsr)); + return (cpsr & (1U << 7)) == 0 ? 1 : 0; // I bit set means masked +} + +} // extern "C" diff --git a/backends/cortex-m/mcpp.toml b/backends/cortex-m/mcpp.toml index 46a71c6..eb09fb9 100644 --- a/backends/cortex-m/mcpp.toml +++ b/backends/cortex-m/mcpp.toml @@ -23,7 +23,7 @@ [package] namespace = "mcpplibs" name = "openarch-cortex-m" -version = "0.1.0" +version = "0.9.0" description = "openarch for ARM Cortex-M: contexts, traps and barriers. No address space — M-profile has an MPU rather than an MMU." license = "Apache-2.0" authors = ["mcpplibs"] diff --git a/backends/cortex-m/src/pte_impl.cpp b/backends/cortex-m/src/pte_impl.cpp index ea28646..89f86a1 100644 --- a/backends/cortex-m/src/pte_impl.cpp +++ b/backends/cortex-m/src/pte_impl.cpp @@ -48,3 +48,6 @@ arch_u64 arch_pte_phys(arch_u64) { refuse("arch_pte_phys"); return void arch_pte_install_memory_attributes(void) {} } // extern "C" + +// This machine has no page table, so there is no entry to size. The refusal is `provides`, not a number. +extern "C" arch_u32 arch_pte_entry_bytes(void) { return 0; } diff --git a/backends/riscv64/mcpp.toml b/backends/riscv64/mcpp.toml index bf5ad33..74c061e 100644 --- a/backends/riscv64/mcpp.toml +++ b/backends/riscv64/mcpp.toml @@ -14,7 +14,7 @@ [package] namespace = "mcpplibs" name = "openarch-riscv64" -version = "0.7.0" +version = "0.9.0" description = "openarch's riscv64 backend: the instructions behind the ABI" license = "Apache-2.0" authors = ["mcpplibs"] diff --git a/backends/riscv64/src/pte_impl.cpp b/backends/riscv64/src/pte_impl.cpp index 8691278..2564119 100644 --- a/backends/riscv64/src/pte_impl.cpp +++ b/backends/riscv64/src/pte_impl.cpp @@ -27,3 +27,5 @@ extern "C" arch_u64 arch_pte_phys(arch_u64 bits) { return arch::riscv64::entry_p // coupling this layer exists to remove. extern "C" void arch_pte_install_memory_attributes(void) { } +// Sv39/Sv48 entries are eight bytes. +extern "C" arch_u32 arch_pte_entry_bytes(void) { return 8; } diff --git a/backends/x86_64/mcpp.toml b/backends/x86_64/mcpp.toml index 3f77daf..e59cba8 100644 --- a/backends/x86_64/mcpp.toml +++ b/backends/x86_64/mcpp.toml @@ -22,7 +22,7 @@ [package] namespace = "mcpplibs" name = "openarch-x86-64" -version = "0.7.0" +version = "0.9.0" description = "openarch's x86_64 backend: the instructions behind the ABI" license = "Apache-2.0" authors = ["mcpplibs"] diff --git a/backends/x86_64/src/pte_impl.cpp b/backends/x86_64/src/pte_impl.cpp index e2e3da1..42fa624 100644 --- a/backends/x86_64/src/pte_impl.cpp +++ b/backends/x86_64/src/pte_impl.cpp @@ -108,3 +108,6 @@ extern "C" void arch_pte_install_memory_attributes(void) { (void)kCr4Smap; asm volatile("movq %0, %%cr4" :: "r"(cr4) : "memory"); } + +// x86-64 entries are eight bytes. +extern "C" arch_u32 arch_pte_entry_bytes(void) { return 8; } diff --git a/mcpp.toml b/mcpp.toml index e7c52c7..d903bd0 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -33,7 +33,7 @@ [package] namespace = "mcpplibs" name = "openarch" -version = "0.8.1" +version = "0.9.0" description = "openarch: the architecture-mechanism layer — execution contexts, traps, per-CPU state and address spaces, as one interface over several instruction sets" license = "Apache-2.0" authors = ["mcpplibs"] @@ -146,6 +146,7 @@ backend-riscv64 = [] backend-aarch64 = [] backend-x86-64 = [] backend-cortex-m = [] +backend-armv7a = [] [target.'cfg(all(arch = "riscv64", os = "none"))'.feature-deps.backend-auto] openarch-riscv64 = { path = "backends/riscv64" } @@ -177,6 +178,14 @@ openarch-x86-64 = { path = "backends/x86_64" } # ⭐ The lesson is the one a new capability always carries: it has more readers # than the site that introduced it, and the table that BINDS it is a different # file from the one that DECLARES it. +# ONE ROW SERVES BOTH ARMv7-A TRIPLES, WHICH THE SEVEN M-PROFILE ROWS BELOW DO +# NOT. `cfg(arch = ...)` matches the triple's first component exactly, and +# `armv7a-none-eabi` and `armv7a-none-eabihf` share it — the float ABI is in the +# environment, not the architecture. The M-profile rows differ because each +# profile IS a different architecture there. +[target.'cfg(all(arch = "armv7a", os = "none"))'.feature-deps.backend-auto] +openarch-armv7a = { path = "backends/armv7a" } + [target.'cfg(all(arch = "thumbv6m", os = "none"))'.feature-deps.backend-auto] openarch-cortex-m = { path = "backends/cortex-m" } @@ -205,3 +214,6 @@ openarch-x86-64 = { path = "backends/x86_64" } [feature-deps.backend-cortex-m] openarch-cortex-m = { path = "backends/cortex-m" } + +[feature-deps.backend-armv7a] +openarch-armv7a = { path = "backends/armv7a" } diff --git a/tests/pte_encoding.cpp b/tests/pte_encoding.cpp index 2f5287d..5952414 100644 --- a/tests/pte_encoding.cpp +++ b/tests/pte_encoding.cpp @@ -206,11 +206,80 @@ void agreement() { kX86Page, "x86_64 discards sub-page bits of phys"); } + +// ─── armv7a: the machine whose entry is not eight bytes ──────────────────── +// +// THE ANSWER TO A QUESTION THE OTHER THREE COULD NOT ASK. +// +// This layer was designed on three 64-bit machines with 64-bit page-table +// entries, and `pte_encode.h` recorded that as a fact about every machine here. +// ARMv7-A's short-descriptor entry is 32 bits, so the assertions below are as +// much about the INTERFACE as about the encoder: the value has to fit in the +// low half, and the storage width has to be askable. +// +// Cortex-M could not settle this and it is worth saying why: it is a 32-bit +// machine with no page table at all, so its pte group exists and refuses. A +// 32-bit machine WITHOUT paging leaves the question exactly where it was. +void armv7a_width() { + using namespace arch; + + constexpr arch_u64 kPage = 0x40001000ULL; // qemu -M virt puts RAM here + + const auto rw = armv7a::encode_leaf(kPage, kReadWrite, kNormal, false); + + // THE WHOLE ENTRY LIVES IN THE LOW 32 BITS. If this ever fails, a caller + // storing the result into a 32-bit table silently loses whatever is above. + check((rw >> 32) == 0, "armv7a entry fits in 32 bits"); + + // The hand-written descriptor, read off the architecture manual rather than + // produced by the code under test: small page (bit 1), AP[0], TEX[0], C, B, + // S, and the base in bits [31:12]. Read/write at PL1 only, normal + // write-back memory, not executable. + constexpr arch_u64 kExpected = + (1ULL << 1) // small page + | (1ULL << 0) // XN — this mapping is not executable + | (1ULL << 4) // AP[0] + | (1ULL << 6) // TEX[0] + | (1ULL << 3) // C + | (1ULL << 2) // B + | (1ULL << 10) // S + | kPage; + check_eq(rw, kExpected, "armv7a read/write normal is the manual's bits"); + + // An executable mapping clears XN, which is bit 0 — adjacent to the bit + // that says "small page" and meaning something entirely unrelated. + const auto rx = armv7a::encode_leaf(kPage, kReadExec, kNormal, false); + check((rx & 1ULL) == 0, "armv7a clears XN for an executable mapping"); + check((rw & 1ULL) != 0, "armv7a sets XN for a non-executable mapping"); + + // AP[2] (bit 9) makes it read-only; AP[1] (bit 5) admits unprivileged + // access. Both are single bits far apart in the word, which is why they are + // asserted rather than described. + check((armv7a::encode_leaf(kPage, kRead, kNormal, false) & (1ULL << 9)) != 0, + "armv7a sets AP[2] for a read-only mapping"); + check((armv7a::encode_leaf(kPage, kReadWrite, kNormal, true) & (1ULL << 5)) != 0, + "armv7a sets AP[1] for a user mapping"); + + check(armv7a::entry_valid(rw), "armv7a small-page descriptor is valid"); + check(!armv7a::entry_valid(0), "armv7a rejects the fault encoding"); + check_eq(armv7a::entry_phys(armv7a::encode_leaf(kPage + 0xFFF, kReadWrite, + kNormal, false)), + kPage, "armv7a discards sub-page bits of phys"); + + // Device memory differs, and in this format it differs by being neither + // cacheable nor TEX-normal — a fourth spelling of the same intent across + // four machines, which is the reason this layer exists. + check(armv7a::encode_leaf(kPage, kReadWrite, kDevice, false) + != armv7a::encode_leaf(kPage, kReadWrite, kNormal, false), + "armv7a distinguishes device from normal"); +} + } // namespace int main() { exact_values(); agreement(); + armv7a_width(); if (g_failed == 0) std::printf("pte encoding ok\n"); return g_failed == 0 ? 0 : 1; }