Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
27 changes: 25 additions & 2 deletions abi/include/openarch/abi.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
76 changes: 75 additions & 1 deletion abi/include/openarch/pte_encode.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
2 changes: 1 addition & 1 deletion abi/mcpp.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
2 changes: 1 addition & 1 deletion backends/aarch64/mcpp.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
2 changes: 2 additions & 0 deletions backends/aarch64/src/pte_impl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
55 changes: 55 additions & 0 deletions backends/armv7a/mcpp.toml
Original file line number Diff line number Diff line change
@@ -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" }
57 changes: 57 additions & 0 deletions backends/armv7a/src/context.S
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading