[Prong A] Three 32-bit ABI PPCBUG siblings corrected to canary semantics
Second differential audit, lead prong: hunt siblings of PPCBUG-020 (the
word-form ALU truncation fixed in 341196a, whose "32-bit ABI / MSR.SF=0"
premise was false — Xenon is a 64-bit core). Found three more band-aids of the
same class, each verified against the canary oracle. All three are genuine
oracle/ISA divergences but INERT on Sylpheed's lockstep trace (sylpheed_n50m
golden digest unchanged; no re-baseline). Fixed + directed tests anyway to
close the band-aid class (per audit decision).
1. slw/srw shift-count mask (PPCBUG-044 site). Ours tested the full u32 count
`< 32`; canary InstrEmit_slwx/srwx mask `rb & 0x3F` then test bit 5. A count
like 0x40 (low-6-bits 0) must pass the value through, not zero it. Fixed both
to `& 0x3F`. The 32-bit CR0 i32-view is unchanged (genuinely 32-bit).
2. sraw/srawi result extension (PPCBUG-041/042/043 "writeback truncation").
Ours zero-extended the 32-bit arithmetic-shift result (`result as u32 as u64`);
PowerISA + canary InstrEmit_srawx/srawix SIGN-extend it (`f.SignExtend`, the
`(i64.s)&¬m` fill). 0x80000000>>1 is now 0xFFFFFFFF_C0000000, not
0x00000000_C0000000. CA math and CR0 view byte-identical.
3. mtspr CTR width (PPCBUG-054). Ours stored `val as u32 as u64`, dropping the
upper 32 bits; CTR is a 64-bit SPR and canary InstrEmit_mtspr stores the full
GPR (`f.StoreCTR(rt)`). A later `mfspr rX, CTR` now round-trips correctly.
bdnz/bcctr still consume only CTR's low 32 bits (the bcx zero-TEST truncation
at line ~922 MATCHES canary's `f.Truncate(ctr, INT32_TYPE)` — left untouched).
Tests: updated srawx_negative_value_sign_extends_upper,
srawix_high_count_negative_input_sign_extends_all_ones, and
mtspr_ctr_keeps_full_64_bits (formerly premise-defending the bugs —
reading-error #24). Added slwx/srwx 6-bit-mask tests, mfspr_ctr round-trip, and
the rlwinm MB>ME wraparound-mask test (plan-requested gap closure). 665/665.
Left correct (re-confirmed vs canary, do NOT touch): bcx/bclr CTR 32-bit test,
divw/divwu zero-extend quotient (canary f.ZeroExtend, ISA upper undefined),
extsb/extsh, logical-NOT chain, mulhw/mulhwu, srawx 0x3F mask, pixel pack/unpack.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -626,7 +626,12 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) -
|
|||||||
PpcOpcode::slwx => {
|
PpcOpcode::slwx => {
|
||||||
// PPCBUG-044: 32-bit ABI CR0 view. A result with bit 31 set
|
// PPCBUG-044: 32-bit ABI CR0 view. A result with bit 31 set
|
||||||
// (e.g. 0x80000000) is negative in i32 view but positive in i64.
|
// (e.g. 0x80000000) is negative in i32 view but positive in i64.
|
||||||
let sh = ctx.gpr[instr.rb()] as u32;
|
// Shift amount is RB[58:63] (6 bits): if >=32 the result is zeroed,
|
||||||
|
// else shift by the low bits. Matches canary InstrEmit_slwx, which
|
||||||
|
// masks `rb & 0x3F` then tests bit 5 — NOT a full-u32 `< 32` test
|
||||||
|
// (a count like 0x40 has low-6-bits 0 and must pass the value
|
||||||
|
// through, not zero it).
|
||||||
|
let sh = ctx.gpr[instr.rb()] as u32 & 0x3F;
|
||||||
ctx.gpr[instr.ra()] = if sh < 32 {
|
ctx.gpr[instr.ra()] = if sh < 32 {
|
||||||
((ctx.gpr[instr.rs()] as u32) << sh) as u64
|
((ctx.gpr[instr.rs()] as u32) << sh) as u64
|
||||||
} else { 0 };
|
} else { 0 };
|
||||||
@@ -636,7 +641,9 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) -
|
|||||||
PpcOpcode::srwx => {
|
PpcOpcode::srwx => {
|
||||||
// PPCBUG-044: 32-bit ABI CR0 view (zero-extended right shift can never
|
// PPCBUG-044: 32-bit ABI CR0 view (zero-extended right shift can never
|
||||||
// have bit 31 set, but use the canonical form for consistency).
|
// have bit 31 set, but use the canonical form for consistency).
|
||||||
let sh = ctx.gpr[instr.rb()] as u32;
|
// Shift amount masked to RB[58:63] (6 bits) to match canary
|
||||||
|
// InstrEmit_srwx (`rb & 0x3F`, test bit 5).
|
||||||
|
let sh = ctx.gpr[instr.rb()] as u32 & 0x3F;
|
||||||
ctx.gpr[instr.ra()] = if sh < 32 {
|
ctx.gpr[instr.ra()] = if sh < 32 {
|
||||||
((ctx.gpr[instr.rs()] as u32) >> sh) as u64
|
((ctx.gpr[instr.rs()] as u32) >> sh) as u64
|
||||||
} else { 0 };
|
} else { 0 };
|
||||||
@@ -644,37 +651,46 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) -
|
|||||||
ctx.pc += 4;
|
ctx.pc += 4;
|
||||||
}
|
}
|
||||||
PpcOpcode::srawx => {
|
PpcOpcode::srawx => {
|
||||||
// PPCBUG-041+043 coupled: 32-bit ABI writeback truncation + CR0 i32.
|
// sraw: 32-bit arithmetic shift right. Per PowerISA the 32-bit result
|
||||||
// CA logic is independently correct (uses u32 shifted-out test).
|
// is SIGN-extended into the full 64-bit RA (`RA <- r&m | (i64.s)&¬m`),
|
||||||
|
// matching canary InstrEmit_srawx (`v = f.SignExtend(v, INT64_TYPE)`).
|
||||||
|
// Earlier ours zero-extended (`result as u32 as u64`) — the PPCBUG-041
|
||||||
|
// "writeback truncation" band-aid — which corrupts any negative shift
|
||||||
|
// result consumed as a 64-bit value. CA logic is independently correct
|
||||||
|
// (uses the u32 shifted-out test) and the CR0 view is unchanged (the
|
||||||
|
// sign-extended i64 has the same i32 view).
|
||||||
let rs = ctx.gpr[instr.rs()] as i32;
|
let rs = ctx.gpr[instr.rs()] as i32;
|
||||||
let sh = ctx.gpr[instr.rb()] as u32 & 0x3F;
|
let sh = ctx.gpr[instr.rb()] as u32 & 0x3F;
|
||||||
if sh == 0 {
|
let result: i32 = if sh == 0 {
|
||||||
ctx.gpr[instr.ra()] = rs as u32 as u64;
|
|
||||||
ctx.xer_ca = 0;
|
ctx.xer_ca = 0;
|
||||||
|
rs
|
||||||
} else if sh < 32 {
|
} else if sh < 32 {
|
||||||
let result = rs >> sh;
|
|
||||||
ctx.xer_ca = if rs < 0 && (rs as u32) << (32 - sh) != 0 { 1 } else { 0 };
|
ctx.xer_ca = if rs < 0 && (rs as u32) << (32 - sh) != 0 { 1 } else { 0 };
|
||||||
ctx.gpr[instr.ra()] = result as u32 as u64;
|
rs >> sh
|
||||||
} else {
|
} else {
|
||||||
ctx.gpr[instr.ra()] = if rs < 0 { 0xFFFF_FFFFu64 } else { 0 };
|
// sh >= 32: result is all sign bits of rs.
|
||||||
ctx.xer_ca = if rs < 0 { 1 } else { 0 };
|
ctx.xer_ca = if rs < 0 { 1 } else { 0 };
|
||||||
}
|
rs >> 31
|
||||||
if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as u32 as i32 as i64); }
|
};
|
||||||
|
ctx.gpr[instr.ra()] = result as i64 as u64;
|
||||||
|
if instr.rc_bit() { ctx.update_cr_signed(0, result as i64); }
|
||||||
ctx.pc += 4;
|
ctx.pc += 4;
|
||||||
}
|
}
|
||||||
PpcOpcode::srawix => {
|
PpcOpcode::srawix => {
|
||||||
// PPCBUG-042+043 coupled: same shape as srawx for the sh-immediate form.
|
// srawi: same as srawx for the sh-immediate form (sh in 0..31).
|
||||||
|
// Sign-extend the 32-bit result into the full 64-bit RA per PowerISA /
|
||||||
|
// canary InstrEmit_srawix.
|
||||||
let rs = ctx.gpr[instr.rs()] as i32;
|
let rs = ctx.gpr[instr.rs()] as i32;
|
||||||
let sh = instr.sh();
|
let sh = instr.sh();
|
||||||
if sh == 0 {
|
let result: i32 = if sh == 0 {
|
||||||
ctx.gpr[instr.ra()] = rs as u32 as u64;
|
|
||||||
ctx.xer_ca = 0;
|
ctx.xer_ca = 0;
|
||||||
|
rs
|
||||||
} else {
|
} else {
|
||||||
let result = rs >> sh;
|
|
||||||
ctx.xer_ca = if rs < 0 && (rs as u32) << (32 - sh) != 0 { 1 } else { 0 };
|
ctx.xer_ca = if rs < 0 && (rs as u32) << (32 - sh) != 0 { 1 } else { 0 };
|
||||||
ctx.gpr[instr.ra()] = result as u32 as u64;
|
rs >> sh
|
||||||
}
|
};
|
||||||
if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as u32 as i32 as i64); }
|
ctx.gpr[instr.ra()] = result as i64 as u64;
|
||||||
|
if instr.rc_bit() { ctx.update_cr_signed(0, result as i64); }
|
||||||
ctx.pc += 4;
|
ctx.pc += 4;
|
||||||
}
|
}
|
||||||
PpcOpcode::sldx => {
|
PpcOpcode::sldx => {
|
||||||
@@ -1611,7 +1627,12 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) -
|
|||||||
match spr {
|
match spr {
|
||||||
crate::context::spr::XER => ctx.set_xer(val as u32),
|
crate::context::spr::XER => ctx.set_xer(val as u32),
|
||||||
crate::context::spr::LR => ctx.lr = val,
|
crate::context::spr::LR => ctx.lr = val,
|
||||||
crate::context::spr::CTR => ctx.ctr = val as u32 as u64,
|
// CTR is a 64-bit SPR — store the full GPR, matching canary
|
||||||
|
// InstrEmit_mtspr (`f.StoreCTR(rt)`, no truncation). The PPCBUG-054
|
||||||
|
// `val as u32 as u64` band-aid dropped the upper 32 bits, which a
|
||||||
|
// later `mfspr rX, CTR` would read back wrong. (bdnz/bcctr only
|
||||||
|
// ever consume CTR's low 32 bits, so branching is unaffected.)
|
||||||
|
crate::context::spr::CTR => ctx.ctr = val,
|
||||||
crate::context::spr::DEC => ctx.dec = val as u32,
|
crate::context::spr::DEC => ctx.dec = val as u32,
|
||||||
crate::context::spr::TBL_WRITE => {
|
crate::context::spr::TBL_WRITE => {
|
||||||
ctx.timebase = (ctx.timebase & 0xFFFF_FFFF_0000_0000) | (val & 0xFFFF_FFFF);
|
ctx.timebase = (ctx.timebase & 0xFFFF_FFFF_0000_0000) | (val & 0xFFFF_FFFF);
|
||||||
@@ -5545,9 +5566,74 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn srawx_negative_value_zero_extends_upper() {
|
fn slwx_shift_count_masks_to_6_bits() {
|
||||||
// PPCBUG-041+043: srawx of negative i32 by 1 produces a negative i32;
|
// slw masks the shift count to RB[58:63] (6 bits): a count of 0x40 has
|
||||||
// writeback must zero-extend to u64 (not sign-extend).
|
// low-6-bits 0, so the value passes through unchanged — it must NOT be
|
||||||
|
// zeroed by a naive full-u32 `>= 32` test. Matches canary InstrEmit_slwx.
|
||||||
|
let mut ctx = PpcContext::new();
|
||||||
|
let mut mem = TestMem::new();
|
||||||
|
ctx.gpr[3] = 0x0000_1234u64;
|
||||||
|
ctx.gpr[4] = 0x40; // count & 0x3F == 0 → shift by 0
|
||||||
|
// slwx r5, r3, r4 (XO=24)
|
||||||
|
let raw = (31u32 << 26) | (3 << 21) | (5 << 16) | (4 << 11) | (24 << 1);
|
||||||
|
write_instr(&mut mem, 0, raw);
|
||||||
|
ctx.pc = 0;
|
||||||
|
step(&mut ctx, &mut mem);
|
||||||
|
assert_eq!(ctx.gpr[5], 0x0000_1234u64, "0x40 masks to 0 → passthrough");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn slwx_count_32_to_63_zeroes() {
|
||||||
|
// A masked count in [32,63] (bit 5 set) zeroes the result.
|
||||||
|
let mut ctx = PpcContext::new();
|
||||||
|
let mut mem = TestMem::new();
|
||||||
|
ctx.gpr[3] = 0xFFFF_FFFFu64;
|
||||||
|
ctx.gpr[4] = 0x60; // & 0x3F = 0x20 (32) → zero
|
||||||
|
let raw = (31u32 << 26) | (3 << 21) | (5 << 16) | (4 << 11) | (24 << 1);
|
||||||
|
write_instr(&mut mem, 0, raw);
|
||||||
|
ctx.pc = 0;
|
||||||
|
step(&mut ctx, &mut mem);
|
||||||
|
assert_eq!(ctx.gpr[5], 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn srwx_shift_count_masks_to_6_bits() {
|
||||||
|
// srw, same 6-bit mask. Count 0x48 → low-6-bits = 8 → logical >> 8.
|
||||||
|
let mut ctx = PpcContext::new();
|
||||||
|
let mut mem = TestMem::new();
|
||||||
|
ctx.gpr[3] = 0x0000_FF00u64;
|
||||||
|
ctx.gpr[4] = 0x48; // & 0x3F = 8
|
||||||
|
// srwx r5, r3, r4 (XO=536)
|
||||||
|
let raw = (31u32 << 26) | (3 << 21) | (5 << 16) | (4 << 11) | (536 << 1);
|
||||||
|
write_instr(&mut mem, 0, raw);
|
||||||
|
ctx.pc = 0;
|
||||||
|
step(&mut ctx, &mut mem);
|
||||||
|
assert_eq!(ctx.gpr[5], 0x0000_00FFu64, "0x48 masks to 8 → >>8");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rlwinm_mb_greater_than_me_wraparound_mask() {
|
||||||
|
// rlwinm with MB > ME produces a wraparound mask covering bits
|
||||||
|
// [0..ME] ∪ [MB..31] (a "split" mask). PowerISA MASK(mb,me) wraps when
|
||||||
|
// mb > me. Here rotate by 0, MB=28, ME=3 → mask = 0xF000000F.
|
||||||
|
let mut ctx = PpcContext::new();
|
||||||
|
let mut mem = TestMem::new();
|
||||||
|
ctx.gpr[3] = 0xFFFF_FFFFu64;
|
||||||
|
// rlwinm r5, r3, SH=0, MB=28, ME=3 (opcode 21)
|
||||||
|
let raw = (21u32 << 26) | (3 << 21) | (5 << 16) | (0 << 11) | (28 << 6) | (3 << 1);
|
||||||
|
write_instr(&mut mem, 0, raw);
|
||||||
|
ctx.pc = 0;
|
||||||
|
step(&mut ctx, &mut mem);
|
||||||
|
assert_eq!(ctx.gpr[5], 0x0000_0000_F000_000Fu64,
|
||||||
|
"MB>ME wraparound mask = bits [0..3] | [28..31]");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn srawx_negative_value_sign_extends_upper() {
|
||||||
|
// sraw of negative i32 by 1 produces a negative i32 result that PowerISA
|
||||||
|
// SIGN-extends into the full 64-bit RA (canary InstrEmit_srawx uses
|
||||||
|
// `f.SignExtend`). 0x80000000 >> 1 = 0xC0000000 (i32) → 0xFFFFFFFF_C0000000.
|
||||||
|
// (Was 0x00000000_C0000000 under the PPCBUG-041 zero-extend band-aid.)
|
||||||
let mut ctx = PpcContext::new();
|
let mut ctx = PpcContext::new();
|
||||||
let mut mem = TestMem::new();
|
let mut mem = TestMem::new();
|
||||||
ctx.gpr[3] = 0x8000_0000u64; // i32::MIN
|
ctx.gpr[3] = 0x8000_0000u64; // i32::MIN
|
||||||
@@ -5557,14 +5643,15 @@ mod tests {
|
|||||||
write_instr(&mut mem, 0, raw);
|
write_instr(&mut mem, 0, raw);
|
||||||
ctx.pc = 0;
|
ctx.pc = 0;
|
||||||
step(&mut ctx, &mut mem);
|
step(&mut ctx, &mut mem);
|
||||||
assert_eq!(ctx.gpr[5], 0x0000_0000_C000_0000u64);
|
assert_eq!(ctx.gpr[5], 0xFFFF_FFFF_C000_0000u64);
|
||||||
assert!(ctx.cr[0].lt);
|
assert!(ctx.cr[0].lt);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn srawix_high_count_negative_input_yields_low32_all_ones() {
|
fn srawix_high_count_negative_input_sign_extends_all_ones() {
|
||||||
// PPCBUG-042+043: srawi with count=31 on negative input → low 32 bits
|
// srawi count=31 on negative input → result is -1 (0xFFFFFFFF as i32),
|
||||||
// all ones (0xFFFFFFFF), upper 32 zero (was u64::MAX before fix).
|
// sign-extended to the full 64-bit RA: 0xFFFFFFFF_FFFFFFFF (canary
|
||||||
|
// InstrEmit_srawix). Was 0x00000000_FFFFFFFF under the zero-extend band-aid.
|
||||||
let mut ctx = PpcContext::new();
|
let mut ctx = PpcContext::new();
|
||||||
let mut mem = TestMem::new();
|
let mut mem = TestMem::new();
|
||||||
ctx.gpr[3] = 0x8000_0000u64;
|
ctx.gpr[3] = 0x8000_0000u64;
|
||||||
@@ -5573,7 +5660,7 @@ mod tests {
|
|||||||
write_instr(&mut mem, 0, raw);
|
write_instr(&mut mem, 0, raw);
|
||||||
ctx.pc = 0;
|
ctx.pc = 0;
|
||||||
step(&mut ctx, &mut mem);
|
step(&mut ctx, &mut mem);
|
||||||
assert_eq!(ctx.gpr[5], 0x0000_0000_FFFF_FFFFu64);
|
assert_eq!(ctx.gpr[5], 0xFFFF_FFFF_FFFF_FFFFu64);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -6549,12 +6636,13 @@ mod tests {
|
|||||||
assert_eq!(ctx.pc, 4);
|
assert_eq!(ctx.pc, 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
// PPCBUG-054: mtspr CTR must truncate the source GPR to 32 bits, matching
|
// CTR is a 64-bit SPR. mtspr CTR stores the full GPR (canary
|
||||||
// canary's `f.Truncate(ctr, INT32_TYPE)`. Prevents upstream 64-bit GPR
|
// InstrEmit_mtspr: `f.StoreCTR(rt)`, no truncation). The bdnz/bclr zero-TEST
|
||||||
// pollution from poisoning the 32-bit CTR counter independently of the
|
// still truncates to 32 bits (separate, canary-faithful — see the bcx tests
|
||||||
// bcx zero-test fix.
|
// above); the earlier PPCBUG-054 store-side truncation was a band-aid that a
|
||||||
|
// later `mfspr rX, CTR` would read back wrong.
|
||||||
#[test]
|
#[test]
|
||||||
fn mtspr_ctr_truncates_to_32_bits() {
|
fn mtspr_ctr_keeps_full_64_bits() {
|
||||||
let mut ctx = PpcContext::new();
|
let mut ctx = PpcContext::new();
|
||||||
let mut mem = TestMem::new();
|
let mut mem = TestMem::new();
|
||||||
ctx.gpr[3] = 0xFFFF_FFFF_8000_0001;
|
ctx.gpr[3] = 0xFFFF_FFFF_8000_0001;
|
||||||
@@ -6564,7 +6652,26 @@ mod tests {
|
|||||||
write_instr(&mut mem, 0, raw);
|
write_instr(&mut mem, 0, raw);
|
||||||
ctx.pc = 0;
|
ctx.pc = 0;
|
||||||
step(&mut ctx, &mut mem);
|
step(&mut ctx, &mut mem);
|
||||||
assert_eq!(ctx.ctr, 0x8000_0001);
|
assert_eq!(ctx.ctr, 0xFFFF_FFFF_8000_0001);
|
||||||
|
}
|
||||||
|
|
||||||
|
// mfspr rX, CTR must read back the full 64-bit CTR (round-trips the value
|
||||||
|
// mtspr stored). This is the observable consequence of the mtspr fix.
|
||||||
|
#[test]
|
||||||
|
fn mfspr_ctr_reads_full_64_bits() {
|
||||||
|
let mut ctx = PpcContext::new();
|
||||||
|
let mut mem = TestMem::new();
|
||||||
|
ctx.gpr[3] = 0xFFFF_FFFF_8000_0001;
|
||||||
|
// mtspr CTR, r3 then mfspr r5, CTR
|
||||||
|
let spr_swapped = ((9u32 & 0x1F) << 5) | ((9u32 >> 5) & 0x1F);
|
||||||
|
let mt = (31u32 << 26) | (3 << 21) | (spr_swapped << 11) | (467 << 1);
|
||||||
|
let mf = (31u32 << 26) | (5 << 21) | (spr_swapped << 11) | (339 << 1);
|
||||||
|
write_instr(&mut mem, 0, mt);
|
||||||
|
write_instr(&mut mem, 4, mf);
|
||||||
|
ctx.pc = 0;
|
||||||
|
step(&mut ctx, &mut mem);
|
||||||
|
step(&mut ctx, &mut mem);
|
||||||
|
assert_eq!(ctx.gpr[5], 0xFFFF_FFFF_8000_0001);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ───────────────────────────────────────────────────────────────────────
|
// ───────────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user