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
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ main: back, witness=7 before=1234
trap: raising
trap: back, witness=1
cpu: percpu round-trips
cpu: tls round-trips
cpu: the two slots are distinct
cpu: four barriers accepted
switch ok
```
Expand All @@ -72,6 +74,21 @@ that saved the return address and the stack pointer and nothing else would
print the first two and corrupt its caller. `before` is a `volatile` local read
after the round trip.

⭐ **`the two slots are distinct` is the line that says a comment became a
check.** There are two pointer slots — one the PROCESSOR owns (`arch_cpu_percpu`,
where a kernel keeps what describes this hart) and one the running CONTEXT owns
(`arch_cpu_tls`, where the toolchain expects thread-local storage). On aarch64
and x86_64 they are different registers and the distinction is free. On riscv64
`tp` was being used for both, and this file's own backend carried a comment
saying so and a constraint — *"must not be compiled into anything that also uses
a thread pointer"* — that nothing enforced.

Measured 2026-08-23: something does. An openkal implementation on this
architecture writes `tp` so a program's `thread_local` works. The probe printed
`cpu: the two slots ALIAS`, the per-CPU pointer moved to `mscratch` — the
register this privilege level provides for it — and the line now reads
`distinct` on all three.

### What the second architecture actually changed

Two things, and neither was visible with one backend.
Expand Down
33 changes: 33 additions & 0 deletions abi/include/openarch/abi.h
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,39 @@ void* arch_cpu_percpu(void);
void arch_cpu_set_percpu(void* p);
void arch_cpu_fence(int barrier);

/* ⭐ THE OTHER POINTER SLOT: THE ONE THE RUNNING *CONTEXT* OWNS, NOT THE ONE
* THE PROCESSOR OWNS.
*
* `arch_cpu_percpu` is per PROCESSOR — a kernel points it at a structure that
* describes this hart. This pair is per CONTEXT: it is where the toolchain
* expects to find the thread-local storage of whatever is running now, and a
* program compiled with `thread_local` reads through it on every access.
*
* ⚠️ THEY ARE NOT THE SAME SLOT AND ON ONE MACHINE THEY COMPETE FOR THE SAME
* REGISTER. cpu_impl.cpp for riscv64 has said so since it was written:
*
* `tp' IS A CONVENTION HERE, NOT AN ARCHITECTURAL REGISTER. [...] a kernel
* may use it for its per-CPU pointer --- but a hosted program on the same
* ISA would find its thread pointer there instead. [...] this backend must
* not be compiled into anything that also uses a thread pointer.
*
* That warning has now been met by an actual program. Measured 2026-08-23:
* `openkal-opensbi`'s startup object writes `tp` so that libc++abi's
* `thread_local` works, because on a machine with no operating system nobody
* else will. Without it the first `throw` faults inside `__cxa_get_globals` and
* the diagnostic names an exception function and an address --- it says nothing
* about a thread pointer.
*
* ⇒ So the slot belongs here, beside the one it is confused with, and the two
* are separate operations because on aarch64 and x86_64 they are separate
* REGISTERS and on riscv64 they are not. An implementation that needs both on
* riscv64 must move the per-CPU pointer to `sscratch`; this interface makes
* that a decision with a name rather than a collision discovered at run time.
*
* ⚠️ Undefined before someone sets it, exactly as the per-CPU slot is. */
void* arch_cpu_tls(void);
void arch_cpu_set_tls(void* p);

#ifdef __cplusplus
} /* extern "C" */
#endif
Expand Down
14 changes: 14 additions & 0 deletions backends/aarch64/src/cpu_impl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,17 @@ extern "C" void arch_cpu_fence(int b) {
}
}


// `TPIDR_EL0` is the one a program reaches, and it is a DIFFERENT register from
// the `TPIDR_EL1` above — so on this machine the two slots do not compete.
// Readable and writable at EL1; readable by a program at EL0, which is what
// makes it the thread pointer.
extern "C" void* arch_cpu_tls(void) {
void* p;
asm volatile("mrs %0, tpidr_el0" : "=r"(p));
return p;
}

extern "C" void arch_cpu_set_tls(void* p) {
asm volatile("msr tpidr_el0, %0" :: "r"(p));
}
55 changes: 45 additions & 10 deletions backends/riscv64/src/cpu_impl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,46 @@
#include <openarch/abi.h>


// ⚠️ `tp` IS A CONVENTION HERE, NOT AN ARCHITECTURAL REGISTER. The ABI reserves
// it for thread-local storage and the hardware assigns it no meaning, so a
// kernel may use it for its per-CPU pointer — but a hosted program on the same
// ISA would find its thread pointer there instead. aarch64's `TPIDR_EL1` is
// architectural and privileged, so no such collision exists.
// ⭐⭐ `mscratch`, AND IT USED TO BE `tp`. THE WARNING THAT SAT HERE CAME TRUE.
//
// The consequence for a caller is nil, which is the point; the consequence for
// this backend is that it must not be compiled into anything that also uses a
// thread pointer.
// What this file said before, verbatim:
//
// `tp' IS A CONVENTION HERE, NOT AN ARCHITECTURAL REGISTER. The ABI
// reserves it for thread-local storage [...] a kernel may use it for its
// per-CPU pointer — but a hosted program on the same ISA would find its
// thread pointer there instead. [...] this backend must not be compiled
// into anything that also uses a thread pointer.
//
// That last sentence is a constraint on every consumer, stated in a comment,
// enforced by nothing. It held while nothing on this architecture wanted a
// thread pointer. Measured 2026-08-23, something does: `openkal-opensbi`'s
// startup object writes `tp` so that a program's `thread_local` works, because
// on a machine with no operating system nobody else will.
//
// And the probe says so now rather than a program discovering it:
//
// riscv64: cpu: the two slots ALIAS
// aarch64: cpu: the two slots are distinct
// x86_64: cpu: the two slots are distinct
//
// ⇒ The per-CPU pointer moves to `mscratch`, which is the register this
// privilege level provides FOR THIS PURPOSE and which nothing else in this
// backend uses — trap.S saves and restores through the stack rather than
// swapping through it. `tp` is left to the thread pointer, whose owner it is by
// ABI. An S-mode variant of this backend would use `sscratch` for the same
// reason.
//
// ⚠️ THIS CHANGES WHICH REGISTER, NOT WHAT THE INTERFACE MEANS. A caller stores
// an opaque pointer and reads it back; that is as true after the move as
// before. What changes is that a caller may now also use `arch_cpu_tls`.
extern "C" void* arch_cpu_percpu(void) {
void* p;
asm volatile("mv %0, tp" : "=r"(p));
asm volatile("csrr %0, mscratch" : "=r"(p));
return p;
}

extern "C" void arch_cpu_set_percpu(void* p) {
asm volatile("mv tp, %0" :: "r"(p));
asm volatile("csrw mscratch, %0" :: "r"(p));
}

extern "C" void arch_cpu_fence(int b) {
Expand All @@ -40,3 +63,15 @@ extern "C" void arch_cpu_fence(int b) {
}
}


// `tp`, which the ABI reserves for exactly this and which nothing else in this
// backend now touches. The per-CPU pointer moved out of it — see above.
extern "C" void* arch_cpu_tls(void) {
void* p;
asm volatile("mv %0, tp" : "=r"(p));
return p;
}

extern "C" void arch_cpu_set_tls(void* p) {
asm volatile("mv tp, %0" :: "r"(p));
}
14 changes: 14 additions & 0 deletions backends/x86_64/src/cpu_impl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ namespace {
// `swapgs` reads its own pointer from `IA32_GS_BASE`. Using the other one
// produces a per-CPU pointer that is correct only after a syscall.
constexpr unsigned kIa32GsBase = 0xC0000101u;
// ⚠️ `FS` AND NOT `GS`, AND ON THIS MACHINE THAT IS THE WHOLE DIFFERENCE
// BETWEEN THE TWO SLOTS. The System V ABI puts a program's thread-local storage
// at `%fs`; a kernel's per-CPU structure conventionally sits at `%gs`, which is
// also what `swapgs` exists to exchange on entry. Two registers, so unlike
// riscv64 the two slots here do not compete.
constexpr unsigned kIa32FsBase = 0xC0000100u;

inline arch_u64 rdmsr(unsigned msr) noexcept {
unsigned lo, hi;
Expand Down Expand Up @@ -107,3 +113,11 @@ extern "C" void arch_cpu_fence(int b) {
default: break;
}
}

extern "C" void* arch_cpu_tls(void) {
return reinterpret_cast<void*>(rdmsr(kIa32FsBase));
}

extern "C" void arch_cpu_set_tls(void* p) {
wrmsr(kIa32FsBase, reinterpret_cast<arch_u64>(p));
}
18 changes: 18 additions & 0 deletions examples/switch/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -115,17 +115,35 @@ void probe_trap() {
// continues, which is what catches a backend that emitted an instruction the
// machine does not have.
int g_percpu_area = 0;
int g_tls_area = 0;

void probe_cpu() {
arch::set_percpu(&g_percpu_area);
const bool same = (arch::percpu() == &g_percpu_area);

// ⭐ THE SECOND SLOT, AND THE ASSERTION IS THAT IT IS A SECOND SLOT.
//
// Round-tripping it alone would pass on a backend where `tls` and `percpu`
// are the same register — which on riscv64 they ARE, by convention, and
// abi.h records why that matters. So the check is that BOTH hold their own
// value at the same time, which is exactly what fails when one aliases the
// other. On riscv64 this is expected to fail until the backend moves the
// per-CPU pointer to `sscratch`; the point of the check is that the day it
// matters, a program says so instead of reading its thread-locals out of a
// per-CPU structure.
arch::set_tls(&g_tls_area);
const bool tls_ok = (arch::tls() == &g_tls_area);
const bool distinct_ok = (arch::percpu() == &g_percpu_area);

arch::fence(arch::barrier::memory);
arch::fence(arch::barrier::store);
arch::fence(arch::barrier::complete);
arch::fence(arch::barrier::fetch);

machine::print(same ? "cpu: percpu round-trips\n" : "cpu: percpu FAILED\n");
machine::print(tls_ok ? "cpu: tls round-trips\n" : "cpu: tls FAILED\n");
machine::print(distinct_ok ? "cpu: the two slots are distinct\n"
: "cpu: the two slots ALIAS\n");
machine::print("cpu: four barriers accepted\n");
}

Expand Down
2 changes: 1 addition & 1 deletion mcpp.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
[package]
namespace = "mcpplibs"
name = "openarch"
version = "0.6.0"
version = "0.7.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"]
Expand Down
15 changes: 15 additions & 0 deletions src/cpu.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,21 @@ export namespace arch {
inline void* percpu() noexcept { return ::arch_cpu_percpu(); }
inline void set_percpu(void* p) noexcept { ::arch_cpu_set_percpu(p); }

// The running CONTEXT's private pointer, which is a different question from the
// one above and is asked by different code.
//
// The per-CPU slot is read by a kernel that wants to know which hart it is on.
// This one is read by the TOOLCHAIN, on every access to a `thread_local`, and
// it is set by whoever creates the context. Where a program carries no loader,
// that is the openkal implementation beneath it.
//
// ⚠️ On riscv64 the two slots want the same register (`tp`), and abi.h records
// the measurement that made the conflict real rather than theoretical. Naming
// them separately is what lets an implementation choose; it does not make the
// register conflict go away.
inline void* tls() noexcept { return ::arch_cpu_tls(); }
inline void set_tls(void* p) noexcept { ::arch_cpu_set_tls(p); }

// The four orderings both machines can state.
enum class barrier {
// Everything before is ordered before everything after, as observed by
Expand Down
Loading