mirror of
https://github.com/brazilofmux/tinymux
synced 2026-08-13 00:23:11 -04:00
dbt(a64): don't form a warm-loop superblock that over-commits the cache
The RV64->ARM64 translator forms a "warm-loop" superblock for self-loops, keeping the loop body's registers resident across the back-edge (warm_entry). The register cache has only RC_NUM_SLOTS - RC_NUM_PINNED (= 4) free slots since a0-a3 are pinned. A loop referencing more than 4 non-pinned registers cannot keep a consistent guest->host mapping across warm_entry: a loop-invariant, never-dirty register (e.g. the magic reciprocal divisor in an itoa /10 loop) is pre-loaded and read at the top with no reload, then evicted mid-body for a working register. rc_flush at the back-edge saves only dirty registers, so later iterations read a stale host register -- corrupting the result. This surfaced as strlen()/add() etc. producing garbage for >=4-digit values when the rv64 softlib was built with a newer GCC whose codegen used more than four live registers in the itoa /10 loop. The reference interpreter executed the identical machine code correctly, proving a translator bug rather than a miscompile. Fix: when the loop's non-pinned used-register count exceeds the free slots, fall back to ordinary per-iteration dispatch (flush and reload through ctx), which is always correct. Adds a differential interpreter-vs-translator regression test (dbt_test.cpp::test_selfloop_register_pressure) and an ISSUES.md entry. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5ab420392f
commit
82e5b8abf9
3 changed files with 115 additions and 4 deletions
|
|
@ -67,6 +67,11 @@ Updated: 2026-05-22 (post Apple Silicon JIT)
|
|||
- **File:** `mux/modules/engine/hir_codegen.cpp:696, 954, 968`
|
||||
- **Issue:** The linear-scan allocator writes `result.addr[iv.value]`, `result.reg[iv.value]`, and `result.spill_slot[iv.value]` without checking `iv.value < HIR_MAX_INSNS`. `iv.value` is a HIR instruction index and is normally bounded by the HIR builder, but any later pass that synthesizes extra virtuals (e.g., PHI rewrites, HIR_FCALL expansion) could push past `HIR_MAX_INSNS` and corrupt the adjacent allocator state. Add an explicit bounds assertion.
|
||||
|
||||
### ~~a64 warm-loop superblock corrupts registers when it over-commits the cache~~ FIXED (2026-06-04)
|
||||
- **File:** `mux/modules/engine/dbt_a64_sysv.cpp` (self-loop detection / pre-load).
|
||||
- **Issue:** When the RV64→ARM64 translator detects a self-loop it forms a "warm-loop" superblock that keeps the loop body's registers resident across the back-edge (`warm_entry`). The register cache has only `RC_NUM_SLOTS - RC_NUM_PINNED` = 4 free slots (a0–a3 are pinned). A loop that references more than 4 non-pinned registers cannot hold a consistent mapping across `warm_entry`: a loop-invariant, never-dirty register (e.g. the ÷10 magic-reciprocal divisor in itoa) gets pre-loaded and read at the loop top with no reload, then evicted mid-body for a working register; `rc_flush` at the back-edge only saves *dirty* registers, so on later iterations `warm_entry` reads a stale host register. Surfaced as `strlen()`/`add()` etc. producing garbage for ≥4-digit values when the rv64 softlib was built with a newer GCC (15.2) whose codegen used >4 live registers in the itoa ÷10 loop. The interpreter (`dbt_interp`) ran the identical machine code correctly, proving a translator bug, not a GCC bug.
|
||||
- **Fix:** Disable the warm-loop superblock when the loop's non-pinned `used` register count exceeds the free slots; fall back to per-iteration dispatch (flush/reload through ctx), which is always correct. Regression test: `dbt_test.cpp::test_selfloop_register_pressure` (differential interpreter-vs-translator on a ÷10 self-loop).
|
||||
|
||||
## Medium — JIT Memory & Offset Hygiene (New, 2026-04-10)
|
||||
|
||||
### Stale compiled entry leaked on `persistent_vm_t::attr_cache` replacement
|
||||
|
|
|
|||
|
|
@ -763,6 +763,30 @@ uint8_t *dbt_backend_translate_block(dbt_state_t *dbt, uint64_t guest_pc) {
|
|||
if (si.opcode == OP_SYSTEM) break;
|
||||
scan_pc += 4;
|
||||
}
|
||||
// A warm-loop superblock keeps the loop body's registers resident in
|
||||
// host registers across the back-edge (warm_entry). The cache only
|
||||
// has (RC_NUM_SLOTS - RC_NUM_PINNED) free slots for non-pinned guest
|
||||
// registers; if the loop references more than that, the cache cannot
|
||||
// hold a consistent mapping across iterations. Loop-invariant, never-
|
||||
// dirty registers then get evicted without being reloaded at the back-
|
||||
// edge, corrupting later iterations (e.g. a magic-reciprocal divisor in
|
||||
// an itoa loop). When the loop over-commits, fall back to ordinary
|
||||
// per-iteration dispatch, which flushes and reloads through ctx and is
|
||||
// always correct.
|
||||
if (self_loop) {
|
||||
const int free_slots = RC_NUM_SLOTS - RC_NUM_PINNED;
|
||||
int nonpinned_used = 0;
|
||||
for (int r = 1; r < 32; r++) {
|
||||
if (!used[r]) continue;
|
||||
bool pinned = false;
|
||||
for (int p = 0; p < RC_NUM_PINNED; p++)
|
||||
if (rc_pinned_guest[p] == r) { pinned = true; break; }
|
||||
if (!pinned) nonpinned_used++;
|
||||
}
|
||||
if (nonpinned_used > free_slots) {
|
||||
self_loop = false;
|
||||
}
|
||||
}
|
||||
if (self_loop) {
|
||||
// Pre-load frequently used registers into the cache.
|
||||
int loaded = 0;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
/*! \file dbt_test.cpp
|
||||
* \brief Standalone test harness for the RV64IMD interpreter.
|
||||
* \brief Standalone test harness for the RV64IMD interpreter and DBT.
|
||||
*
|
||||
* Compile:
|
||||
* g++ -std=c++17 -O2 -I../../include -o dbt_test dbt_test.cpp dbt_interp.cpp dbt_elf64.cpp
|
||||
* Compile (dbt.cpp + dbt_a64_sysv.cpp are needed for the differential
|
||||
* interpreter-vs-translator tests):
|
||||
* g++ -std=c++17 -O2 -I../../include -o dbt_test \
|
||||
* dbt_test.cpp dbt_interp.cpp dbt_elf64.cpp dbt.cpp dbt_a64_sysv.cpp
|
||||
*
|
||||
* Run:
|
||||
* ./dbt_test # hand-assembled tests only
|
||||
|
|
@ -10,7 +12,9 @@
|
|||
*
|
||||
* Tests hand-assembled RV64IMD instruction sequences against expected
|
||||
* results. Each test allocates a small memory buffer, writes machine
|
||||
* code, runs the interpreter, and checks register values.
|
||||
* code, runs the interpreter, and checks register values. Some tests also
|
||||
* run the same code through the host DBT translator and check that it
|
||||
* agrees with the interpreter (run_code_dbt).
|
||||
*/
|
||||
|
||||
#include "dbt_interp.h"
|
||||
|
|
@ -260,6 +264,34 @@ static TestResult run_code(const std::vector<uint32_t>& code,
|
|||
return result;
|
||||
}
|
||||
|
||||
// Run a code sequence through the host DBT translator and return guest
|
||||
// register `reg`. Used to differential-test the translator against the
|
||||
// reference interpreter (run_code). The code must set up its own registers
|
||||
// (dbt_run zeroes the guest context on entry) and end with ECALL.
|
||||
//
|
||||
static rv64_ctx_t g_dbt_exit_ctx;
|
||||
static int dbt_test_ecall2(rv64_ctx_t *ctx, void *) {
|
||||
g_dbt_exit_ctx = *ctx;
|
||||
return 0; // halt
|
||||
}
|
||||
|
||||
static uint64_t run_code_dbt(const std::vector<uint32_t>& code, int reg) {
|
||||
const size_t MEM_SIZE = 64 * 1024;
|
||||
std::vector<uint8_t> memory(MEM_SIZE, 0);
|
||||
for (size_t i = 0; i < code.size(); i++) {
|
||||
memcpy(memory.data() + i * 4, &code[i], 4);
|
||||
}
|
||||
|
||||
dbt_state_t dbt;
|
||||
if (dbt_init(&dbt, memory.data(), MEM_SIZE, dbt_test_ecall2, nullptr) != 0) {
|
||||
return ~0ULL;
|
||||
}
|
||||
dbt.max_dispatch = 1000000;
|
||||
dbt_run(&dbt, 0, MEM_SIZE - 16);
|
||||
dbt_cleanup(&dbt);
|
||||
return g_dbt_exit_ctx.x[reg];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------
|
||||
|
|
@ -1303,6 +1335,55 @@ static bool run_dbt_elf_test(const char *path) {
|
|||
// Main
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
// Self-loop with more live non-pinned registers than the a64 register
|
||||
// cache has free slots. The translator forms a "warm-loop" superblock that
|
||||
// keeps the loop body resident across the back-edge; with a0-a3 pinned, only
|
||||
// 4 slots are free, but this loop reads 5 loop-invariant registers (t1-t5).
|
||||
// A buggy translator evicts an invariant without reloading it at the back-
|
||||
// edge, corrupting later iterations. This was observed as itoa() producing
|
||||
// garbage digits for >=4-digit values (the ÷10 magic-reciprocal divisor was
|
||||
// the evicted invariant). Differential-tested against the interpreter.
|
||||
//
|
||||
static void test_selfloop_register_pressure() {
|
||||
printf("test_selfloop_register_pressure...\n");
|
||||
// Reproduces the warm-loop register-eviction bug exactly as itoa hit it:
|
||||
// a self-loop that divides by 10 via a magic-reciprocal multiply. The
|
||||
// divisor magic lives in a high-numbered, loop-invariant register (t5/x30)
|
||||
// so it survives the warm_entry pre-load and is read at the loop top with
|
||||
// no reload — but it is then evicted mid-body for a working register. A
|
||||
// translator that doesn't reconcile the cache at the back-edge reads a
|
||||
// stale host register for the magic on later iterations, corrupting the
|
||||
// quotient. Here we sum the decimal digits of a1 (>=4 digits to get
|
||||
// enough iterations) and differential-test against the interpreter.
|
||||
//
|
||||
// x30 = 0xCCCCCCCCCCCCCCCD (the /10 magic), x28 = digit sum.
|
||||
const int A1 = 11, Q = 5, T = 6, R = 7, SUM = 28, M = 30;
|
||||
auto MULHU = [](int rd, int rs1, int rs2) {
|
||||
return r_type(OP_REG, rd, 3, rs1, rs2, 0x01);
|
||||
};
|
||||
std::vector<uint32_t> code = {
|
||||
// a1 = 123456 (0x1E000 + 576)
|
||||
LUI(A1, 0x1E000), ADDI(A1, A1, 576),
|
||||
// M = 0xCCCCCCCCCCCCCCCD
|
||||
LUI(M, 0xCCCCD000), ADDI(M, M, -819), // M = 0xFFFFFFFFCCCCCCCD
|
||||
SLLI(Q, M, 32), ADD(M, Q, M), // M = 0xCCCCCCCCCCCCCCCD
|
||||
ADDI(SUM, 0, 0), // sum = 0
|
||||
// loop (self-loop back-edge below):
|
||||
MULHU(Q, A1, M), // q = high64(a1 * magic)
|
||||
SRLI(Q, Q, 3), // q = a1 / 10
|
||||
SLLI(T, Q, 2), ADD(T, T, Q), SLLI(T, T, 1), // t = q * 10
|
||||
SUB(R, A1, T), // r = a1 - q*10 (low digit)
|
||||
ADD(SUM, SUM, R), // sum += digit
|
||||
ADD(A1, Q, 0), // a1 = q
|
||||
BNE(A1, 0, -32), // while a1 != 0 (8 instrs back = -32)
|
||||
ECALL(),
|
||||
};
|
||||
uint64_t interp = run_code(code).state.x[SUM];
|
||||
uint64_t dbt = run_code_dbt(code, SUM);
|
||||
CHECK_EQ("self-loop regpressure: interpreter", interp, 21); // 1+2+3+4+5+6
|
||||
CHECK_EQ("self-loop regpressure: DBT matches interpreter", dbt, interp);
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
printf("RV64IMD Interpreter Test Suite\n");
|
||||
printf("==============================\n\n");
|
||||
|
|
@ -1339,6 +1420,7 @@ int main(int argc, char *argv[]) {
|
|||
test_fp_fma();
|
||||
test_fp_fclass();
|
||||
test_fp_fmv_dx();
|
||||
test_selfloop_register_pressure();
|
||||
|
||||
printf("\n==============================\n");
|
||||
printf("Hand-assembled: %d run, %d passed, %d failed\n",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue