mirror of
https://github.com/libretro/virtualjaguar-libretro.git
synced 2026-11-07 04:36:00 -10:00
Cybermorph's DSP spends ~91% of its instructions in a two-bit register poll (btst ; jr NE,out ; nop ; btst ; jr EQ,head) that idle skip rejected only for its second branch. Admit a jr whose target lies past the closing jr's delay slot, and back it with a taken-jr count: each probe iteration must take exactly one jr, the closing one, which closes the detour route the opcode-count check alone cannot see. Cybermorph RISC 2x: p50 5.49 -> 2.34 ms, p99 14.34 -> 11.13 ms on an M4 Max; host instructions per frame -45%. Skip on/off byte-identical across all 160 corpus carts; four titles newly skip. dsp_idle_probe_falsify gains Cybermorph's shape (must fire and still exit) and an exit-detour exploit (must not fire; fires with the check removed). Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
615a5588d4
commit
aaa9b5946c
2 files changed
+159
-10
No files matched your search
+52
-5
@@ -1778,7 +1778,14 @@ void DSPSyncToM68K(void)
|
||||
* Body = target .. jr inclusive, plus the inlined delay-slot
|
||||
* instruction. Static decode (register-independent) requires:
|
||||
* - every opcode on the whitelist below (no store of any kind, no
|
||||
* accumulator/latency opcode, no second branch);
|
||||
* accumulator/latency opcode, no `jump`), except that the body may
|
||||
* contain EXIT branches: a `jr` before the loop-closing one whose
|
||||
* target lies past the closing jr's delay slot (#952 -- Cybermorph
|
||||
* polls two bits of a register with `btst ; jr NE,out ; nop ;
|
||||
* btst ; jr EQ,head`). An exit that is never taken leaves the
|
||||
* body straight-line; the probe proves it was not taken (see the
|
||||
* EXIT-BRANCH CHECK in DSPIdleLoopProbe) and the fixed-point proof
|
||||
* below then proves it never will be;
|
||||
* - the decode walk lands exactly on the jr, so the body really is
|
||||
* straight-line (movei carries a 32-bit immediate, i.e. 3 words);
|
||||
* - no movei in the delay slot (its immediate would be fetched from
|
||||
@@ -1826,6 +1833,9 @@ void DSPSyncToM68K(void)
|
||||
* zero-delta registers stay put -- while each counter register is only
|
||||
* ever incremented by a constant. So iteration k's state is
|
||||
* S1 + (k-1)*delta for all k, and the branch condition never changes.
|
||||
* The same holds at every point INSIDE the body, so an exit jr sees the
|
||||
* same flags on every iteration: not taken from S1 to S2 (the probe
|
||||
* checks that) means not taken on any extrapolated iteration either.
|
||||
*
|
||||
* Then n = (cycles_remaining / cost) - 1 (always leave one full
|
||||
* iteration to execute normally), reg += n*delta, cycles -= n*cost,
|
||||
@@ -1874,6 +1884,14 @@ uint32_t dsp_idle_skip_opcodes = 0; /* opcodes NOT interpreted -- the
|
||||
* over a skip on purpose, so it does
|
||||
* NOT show the saving. */
|
||||
|
||||
/* Every taken `jr`, counted inside dsp_opcode_jr's taken branch rather
|
||||
* than in DSPExec's loop: a test there changed the loop's code generation
|
||||
* and cost Iron Soldier 2 +0.7% host instructions per frame (#952). Here
|
||||
* it measured +0.35% on Iron Soldier 2 (jr-heavy DSP, no idle skip to win
|
||||
* back) and +0.09% on Doom. Only differences are read, so it never needs
|
||||
* a reset. */
|
||||
static uint32_t dsp_jr_taken;
|
||||
|
||||
static int idleProbeStage; /* 0 = none, 1 = have S0, 2 = have S1 */
|
||||
static uint32_t idleProbeHead;
|
||||
static uint32_t idleProbeJr;
|
||||
@@ -1881,6 +1899,7 @@ static uint32_t *idleProbeBank; /* dsp_reg at S0 -- see theorem (3) */
|
||||
static int32_t idleProbeCyc0, idleProbeCyc1;
|
||||
static uint32_t idleProbeOpc0, idleProbeOpc1;
|
||||
static uint32_t idleProbeS0[64], idleProbeS1[64];
|
||||
static uint32_t idleProbeJrt0, idleProbeJrt1; /* dsp_jr_taken at S0/S1 */
|
||||
static uint64_t idleProbeLdxFix; /* dsp_load_wb_fixups at S0 */
|
||||
static uint64_t idleProbeDivFix; /* dsp_div_store_fixups at S0 */
|
||||
static uint8_t idleProbeFz0, idleProbeFn0, idleProbeFc0;
|
||||
@@ -1929,7 +1948,9 @@ static int dsp_idle_op_admitted(uint32_t idx)
|
||||
* PC-modifying opcode but the loop-closing jr itself is load-bearing
|
||||
* for the executed-path check below (idleBodyCount forces exactly
|
||||
* one route from head to jrAddr), not only for the store-freedom
|
||||
* this whitelist was originally written to guarantee. */
|
||||
* this whitelist was originally written to guarantee. The one
|
||||
* exception, a forward exit jr, is admitted by dsp_idle_decode
|
||||
* itself and backed by the probe's EXIT-BRANCH CHECK. */
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
@@ -2004,6 +2025,7 @@ static int dsp_idle_operands(uint32_t idx, uint32_t p1, uint32_t p2,
|
||||
src[(*nsrc)++] = cur + (int)p1;
|
||||
*dst = cur + (int)p2;
|
||||
return 1;
|
||||
case 53: /* exit jr -- reads flags only */
|
||||
case 57: /* nop */
|
||||
return 1;
|
||||
default:
|
||||
@@ -2049,7 +2071,18 @@ static int dsp_idle_decode(uint32_t head, uint32_t jrAddr)
|
||||
return 0;
|
||||
op = DSP_IDLE_FETCH(pc);
|
||||
idx = (uint32_t)(op >> 10);
|
||||
if (!dsp_idle_op_admitted(idx))
|
||||
if (idx == 53)
|
||||
{
|
||||
/* Exit branch: only a jr that leaves the loop entirely, past
|
||||
* the closing jr's delay slot. Anything landing inside
|
||||
* [head, jr+2] would make a second route around the loop. */
|
||||
int32_t off = (int32_t)((op >> 5) & 0x1F);
|
||||
if (off & 0x10)
|
||||
off -= 0x20;
|
||||
if (pc + 2 + (uint32_t)(off * 2) <= jrAddr + 2)
|
||||
return 0;
|
||||
}
|
||||
else if (!dsp_idle_op_admitted(idx))
|
||||
return 0;
|
||||
idleBodyIdx[n] = (uint8_t)idx;
|
||||
idleBodyP1[n] = (uint8_t)((op >> 5) & 0x1F);
|
||||
@@ -2284,6 +2317,7 @@ static int32_t DSPIdleLoopProbe(int32_t cycles, uint32_t head, uint32_t jrAddr)
|
||||
idleProbeFc0 = dsp_flag_c;
|
||||
idleProbeCyc0 = cycles;
|
||||
idleProbeOpc0 = dsp_exec_opcode_count;
|
||||
idleProbeJrt0 = dsp_jr_taken;
|
||||
idleProbeHead = head;
|
||||
idleProbeJr = jrAddr;
|
||||
idleProbeBank = dsp_reg;
|
||||
@@ -2301,6 +2335,7 @@ static int32_t DSPIdleLoopProbe(int32_t cycles, uint32_t head, uint32_t jrAddr)
|
||||
idleProbeFc1 = dsp_flag_c;
|
||||
idleProbeCyc1 = cycles;
|
||||
idleProbeOpc1 = dsp_exec_opcode_count;
|
||||
idleProbeJrt1 = dsp_jr_taken;
|
||||
idleProbeStage = 2;
|
||||
return cycles;
|
||||
}
|
||||
@@ -2386,8 +2421,19 @@ static int32_t DSPIdleLoopProbe(int32_t cycles, uint32_t head, uint32_t jrAddr)
|
||||
* array.) Any other route from one arrival to the next must execute
|
||||
* the whole body AND at least one further instruction to get back,
|
||||
* so it costs strictly more. Requiring exact equality therefore
|
||||
* admits the straight-line traversal and nothing else. */
|
||||
if (opcost != (uint32_t)idleBodyCount + 1)
|
||||
* admits the straight-line traversal and nothing else.
|
||||
*
|
||||
* EXIT-BRANCH CHECK (#952). That argument needs a branch-free body.
|
||||
* With an exit jr in it, a taken exit can leave, run undecoded code
|
||||
* (a store) and come back through a `jump` to the closing jr with
|
||||
* exactly the straight-line opcode count -- dsp_idle_probe_falsify
|
||||
* program D is such a route. So also require that each probe
|
||||
* iteration took exactly ONE jr, the closing one: a taken exit is a
|
||||
* taken jr and would make it two. With every exit untaken, the body
|
||||
* is straight-line again and the count argument above applies. */
|
||||
if (opcost != (uint32_t)idleBodyCount + 1
|
||||
|| idleProbeJrt1 - idleProbeJrt0 != 1
|
||||
|| dsp_jr_taken - idleProbeJrt1 != 1)
|
||||
{
|
||||
dsp_idle_memo_reject(head, jrAddr);
|
||||
return cycles;
|
||||
@@ -2730,6 +2776,7 @@ INLINE static void dsp_opcode_jr(void)
|
||||
int32_t delayed_pc = dsp_pc + (offset * 2);
|
||||
uint16_t ds_opcode;
|
||||
uint32_t ds_index;
|
||||
dsp_jr_taken++; /* idle-skip exit-branch check (#952) */
|
||||
/* Same retire-window commits as dsp_opcode_jump. */
|
||||
if (dspFlagsRetireDelay)
|
||||
{
|
||||
|
||||
@@ -38,6 +38,22 @@
|
||||
* Only the executed-path check (opcost == idleBodyCount + 1)
|
||||
* rejects it.
|
||||
*
|
||||
* C. a loop with an exit branch (#952, Cybermorph's DSP wait loop)
|
||||
* -> the probe MUST fire, and the loop must still leave once the
|
||||
* polled register changes.
|
||||
* head: btst #0,r22 ; jr NE,out ; nop ; btst #1,r22 ; jr EQ,head
|
||||
* nop <- delay slot
|
||||
*
|
||||
* D. the exit-branch exploit -> the probe MUST NOT fire.
|
||||
* head: subq #1,r1 ; jr EQ,out ; nop x5 ; jr always,head ; nop
|
||||
* out: store r3,(r4) ; movei #1,r1 ; jump (r6) ; nop
|
||||
* r6 = the closing jr. The exit is taken every time, the detour
|
||||
* runs an undecoded store and re-enters through a `jump` (never
|
||||
* probed), and the route costs 9 opcodes, exactly the 7 decoded
|
||||
* + delay slot + 1 of a straight pass. Registers and flags match
|
||||
* at every arrival, so only the exit-branch check (one taken jr per
|
||||
* iteration) rejects it.
|
||||
*
|
||||
* Build: cc -O2 -Wall -std=c99 $(INCFLAGS) -o test/tools/dsp_idle_probe_falsify \
|
||||
* test/tools/dsp_idle_probe_falsify.c test/harness/harness.c -ldl -lm
|
||||
* Needs the wide test ABI (make TEST_EXPORTS=1). Uses test/roms/yarc.j64,
|
||||
@@ -54,6 +70,7 @@
|
||||
/* RISC opcode indices (src/jerry/dsp.c dsp_opcode[]) */
|
||||
#define OP_ADDQT 3
|
||||
#define OP_SUBQ 6
|
||||
#define OP_BTST 13
|
||||
#define OP_STORE 47
|
||||
#define OP_JUMP 52
|
||||
#define OP_JR 53
|
||||
@@ -63,12 +80,16 @@
|
||||
/* Branch condition codes: 0 = always, 1 = NE (fails when Z is set). */
|
||||
#define CC_ALWAYS 0
|
||||
#define CC_NE 1
|
||||
#define CC_EQ 2
|
||||
|
||||
#define DSP_RAM_BASE 0x00F1B000u
|
||||
|
||||
#define PROG_A_OFF 0x0100 /* $F1B100 */
|
||||
#define PROG_B_OFF 0x0200 /* $F1B200 */
|
||||
#define PROG_C_OFF 0x0300 /* $F1B300 */
|
||||
#define PROG_C_OUT 0x0320 /* $F1B320 -- C's exit target */
|
||||
#define SCRATCH_OFF 0x0400 /* $F1B400 -- the exploit's store target */
|
||||
#define PROG_D_OFF 0x0500 /* $F1B500 */
|
||||
|
||||
static uint8_t *dsp_ram;
|
||||
static uint32_t *p_dsp_pc;
|
||||
@@ -127,6 +148,47 @@ static void build_prog_b(void)
|
||||
setreg(5, DSP_RAM_BASE + PROG_B_OFF); /* jump target = head */
|
||||
}
|
||||
|
||||
static void build_prog_c(void)
|
||||
{
|
||||
/* jr offsets: target = (jr + 2) + off * 2. */
|
||||
put16(PROG_C_OFF + 0, enc(OP_BTST, 0, 22)); /* btst #0,r22 */
|
||||
put16(PROG_C_OFF + 2, enc(OP_JR, 14, CC_NE)); /* jr ne,out */
|
||||
put16(PROG_C_OFF + 4, enc(OP_NOP, 0, 0));
|
||||
put16(PROG_C_OFF + 6, enc(OP_BTST, 1, 22)); /* btst #1,r22 */
|
||||
put16(PROG_C_OFF + 8, enc(OP_JR, 0x1B /* -5 */, CC_EQ)); /* jr eq,head */
|
||||
put16(PROG_C_OFF + 10, enc(OP_NOP, 0, 0)); /* (delay slot) */
|
||||
/* out: park in an idle loop of its own so the PC stays readable. */
|
||||
put16(PROG_C_OUT + 0, enc(OP_ADDQT, 1, 7)); /* addqt #1,r7 */
|
||||
put16(PROG_C_OUT + 2, enc(OP_JR, 0x1E /* -2 */, CC_ALWAYS));
|
||||
put16(PROG_C_OUT + 4, enc(OP_NOP, 0, 0));
|
||||
|
||||
setreg(22, 0); /* nothing to do yet */
|
||||
}
|
||||
|
||||
static void build_prog_d(void)
|
||||
{
|
||||
unsigned i;
|
||||
|
||||
put16(PROG_D_OFF + 0, enc(OP_SUBQ, 1, 1)); /* subq #1,r1 */
|
||||
put16(PROG_D_OFF + 2, enc(OP_JR, 7, CC_EQ)); /* jr eq,out */
|
||||
for (i = 4; i <= 12; i += 2)
|
||||
put16(PROG_D_OFF + i, enc(OP_NOP, 0, 0));
|
||||
put16(PROG_D_OFF + 14, enc(OP_JR, 0x18 /* -8 */, CC_ALWAYS)); /* jr head */
|
||||
put16(PROG_D_OFF + 16, enc(OP_NOP, 0, 0)); /* (delay slot) */
|
||||
/* out = head + 18 */
|
||||
put16(PROG_D_OFF + 18, enc(OP_STORE, 4, 3)); /* store r3,(r4) */
|
||||
put16(PROG_D_OFF + 20, enc(OP_MOVEI, 0, 1)); /* movei #1,r1 */
|
||||
put16(PROG_D_OFF + 22, 0x0001); /* LSW */
|
||||
put16(PROG_D_OFF + 24, 0x0000); /* MSW */
|
||||
put16(PROG_D_OFF + 26, enc(OP_JUMP, 6, CC_ALWAYS)); /* jump (r6) */
|
||||
put16(PROG_D_OFF + 28, enc(OP_NOP, 0, 0)); /* (delay slot) */
|
||||
|
||||
setreg(1, 1);
|
||||
setreg(3, 0xDEADBEEFu);
|
||||
setreg(4, DSP_RAM_BASE + SCRATCH_OFF);
|
||||
setreg(6, DSP_RAM_BASE + PROG_D_OFF + 14); /* jump target = closing jr */
|
||||
}
|
||||
|
||||
/* Run one hand-assembled program from `off` for `cycles`, returning how
|
||||
* many times the fast-forward fired. */
|
||||
static uint32_t run_prog(uint32_t off, int32_t cycles,
|
||||
@@ -148,11 +210,13 @@ static uint32_t run_prog(uint32_t off, int32_t cycles,
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
harness_config cfg = HARNESS_CONFIG_DEFAULT;
|
||||
harness_result res[3];
|
||||
harness_result res[6];
|
||||
unsigned nres = 0;
|
||||
uint32_t fires_a, fires_b, rej_a, rej_b, ops_a, ops_b;
|
||||
uint32_t fires_c, fires_d, rej_c, rej_d, ops_c, ops_d, pc_c;
|
||||
static char detail_a[192], detail_b[192], detail_c[192];
|
||||
int ok_a, ok_b;
|
||||
static char detail_d[192], detail_e[192], detail_f[192];
|
||||
int ok_a, ok_b, ok_c, ok_d, ok_e;
|
||||
|
||||
cfg.frames = 2;
|
||||
cfg.quiet = 1;
|
||||
@@ -182,14 +246,26 @@ int main(int argc, char **argv)
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Build each program just before it runs: B and D both use r1. */
|
||||
build_prog_a();
|
||||
build_prog_b();
|
||||
|
||||
fires_a = run_prog(PROG_A_OFF, 20000, &rej_a, &ops_a);
|
||||
build_prog_b();
|
||||
fires_b = run_prog(PROG_B_OFF, 20000, &rej_b, &ops_b);
|
||||
build_prog_c();
|
||||
fires_c = run_prog(PROG_C_OFF, 20000, &rej_c, &ops_c);
|
||||
/* The polled bit arrives: C must leave the loop it was skipping. */
|
||||
setreg(22, 1);
|
||||
p_dspexec(200);
|
||||
pc_c = *p_dsp_pc;
|
||||
build_prog_d();
|
||||
fires_d = run_prog(PROG_D_OFF, 20000, &rej_d, &ops_d);
|
||||
|
||||
ok_a = (fires_a > 0);
|
||||
ok_b = (fires_b == 0);
|
||||
ok_c = (fires_c > 0);
|
||||
ok_d = (pc_c >= DSP_RAM_BASE + PROG_C_OUT
|
||||
&& pc_c <= DSP_RAM_BASE + PROG_C_OUT + 4);
|
||||
ok_e = (fires_d == 0);
|
||||
|
||||
snprintf(detail_a, sizeof detail_a,
|
||||
"genuinely idle loop (addqt/jr/nop) fired %u time(s), "
|
||||
@@ -216,7 +292,33 @@ int main(int argc, char **argv)
|
||||
res[nres].detail = detail_c;
|
||||
nres++;
|
||||
|
||||
snprintf(detail_d, sizeof detail_d,
|
||||
"loop with an exit jr (btst/jr ne/nop/btst/jr eq) fired %u "
|
||||
"time(s), %u reject(s), %u opcodes charged",
|
||||
fires_c, rej_c, ops_c);
|
||||
res[nres].status = ok_c ? "PASS" : "FAIL";
|
||||
res[nres].name = "exit_branch_loop_fires";
|
||||
res[nres].detail = detail_d;
|
||||
nres++;
|
||||
|
||||
snprintf(detail_e, sizeof detail_e,
|
||||
"after r22 bit 0 set, DSP PC $%06X (must be in $%06X..$%06X)",
|
||||
pc_c, DSP_RAM_BASE + PROG_C_OUT, DSP_RAM_BASE + PROG_C_OUT + 4);
|
||||
res[nres].status = ok_d ? "PASS" : "FAIL";
|
||||
res[nres].name = "exit_branch_still_exits";
|
||||
res[nres].detail = detail_e;
|
||||
nres++;
|
||||
|
||||
snprintf(detail_f, sizeof detail_f,
|
||||
"taken exit + undecoded store + jump back, same opcode count, "
|
||||
"fired %u time(s) (must be 0), %u reject(s), %u opcodes charged",
|
||||
fires_d, rej_d, ops_d);
|
||||
res[nres].status = ok_e ? "PASS" : "FAIL";
|
||||
res[nres].name = "taken_exit_detour_rejected";
|
||||
res[nres].detail = detail_f;
|
||||
nres++;
|
||||
|
||||
harness_report(&cfg, res, nres);
|
||||
harness_shutdown(&cfg);
|
||||
return (ok_a && ok_b) ? 0 : 1;
|
||||
return (ok_a && ok_b && ok_c && ok_d && ok_e) ? 0 : 1;
|
||||
}
|
||||
Reference in new issue
Block a user