fix(cpu): accept ARM multiplies whose destination equals Rm

MUL/MLA with Rd=Rm and long multiplies with RdHi or RdLo equal to Rm returned
UnsupportedInstruction and stopped the machine. The ARM7TDMI reads every
operand before writing the result, so these forms execute normally. R15
operands and RdHi=RdLo remain diagnostics.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhfHfHEuzv2AUTd6erW1gU
This commit is contained in:
0xrsydn 2026-10-14 08:38:10 +07:00
commit b535c08313
3 changed files with 23 additions and 3 deletions

View file

@ -256,9 +256,10 @@ impl Cpu {
let rs = ((instruction >> 8) & 15) as usize; let rs = ((instruction >> 8) & 15) as usize;
let rm = (instruction & 15) as usize; let rm = (instruction & 15) as usize;
let accumulate = instruction & (1 << 21) != 0; let accumulate = instruction & (1 << 21) != 0;
if [rd, rn, rs, rm].contains(&15) || rd == rm || (!accumulate && rn != 0) { if [rd, rn, rs, rm].contains(&15) || (!accumulate && rn != 0) {
return Err(self.unsupported(instruction)); return Err(self.unsupported(instruction));
} }
// Rd may equal Rm: every operand is read before the destination is written.
let mut result = self.registers[rm].wrapping_mul(self.registers[rs]); let mut result = self.registers[rm].wrapping_mul(self.registers[rs]);
if accumulate { if accumulate {
result = result.wrapping_add(self.registers[rn]); result = result.wrapping_add(self.registers[rn]);
@ -278,7 +279,7 @@ impl Cpu {
let lo = ((instruction >> 12) & 15) as usize; let lo = ((instruction >> 12) & 15) as usize;
let rs = ((instruction >> 8) & 15) as usize; let rs = ((instruction >> 8) & 15) as usize;
let rm = (instruction & 15) as usize; let rm = (instruction & 15) as usize;
if [hi, lo, rs, rm].contains(&15) || hi == lo || hi == rm || lo == rm { if [hi, lo, rs, rm].contains(&15) || hi == lo {
return Err(self.unsupported(instruction)); return Err(self.unsupported(instruction));
} }
let mut result = if instruction & (1 << 22) != 0 { let mut result = if instruction & (1 << 22) != 0 {

View file

@ -487,6 +487,23 @@ fn long_multiply_supports_signed_unsigned_and_accumulation() {
} }
} }
#[test]
fn multiplies_read_every_operand_before_writing_an_aliased_destination() {
for (instruction, expected) in [
(0xe000_0190, [u32::MAX - 1, 2, 9, 5]), // MUL r0,r0,r1
(0xe020_3190, [3, 2, 9, 5]), // MLA r0,r0,r1,r3
(0xe083_0190, [u32::MAX - 1, 2, 9, 1]), // UMULL r0,r3,r0,r1 (RdLo=Rm)
(0xe080_2190, [1, 2, u32::MAX - 1, 5]), // UMULL r2,r0,r0,r1 (RdHi=Rm)
(0xe0a3_0190, [u32::MAX - 2, 2, 9, 7]), // UMLAL r0,r3,r0,r1 (+5:0xffffffff)
] {
let mut memory = program(&[instruction]);
let mut cpu = Cpu::new(ROM_START);
cpu.registers[..4].copy_from_slice(&[u32::MAX, 2, 9, 5]);
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[..4], expected, "{instruction:08x}");
}
}
#[test] #[test]
fn long_multiply_zero_flag_uses_both_halves() { fn long_multiply_zero_flag_uses_both_halves() {
let mut memory = program(&[0xe093_2190]); let mut memory = program(&[0xe093_2190]);
@ -510,7 +527,6 @@ fn invalid_or_unimplemented_encodings_do_not_change_state_or_memory() {
0xe790_1012, // illegal register-specified memory shift 0xe790_1012, // illegal register-specified memory shift
0xe790_100f, // memory offset register PC 0xe790_100f, // memory offset register PC
0xe1c0_10d0, // ARMv5 doubleword load, not an ARM7 signed store 0xe1c0_10d0, // ARMv5 doubleword load, not an ARM7 signed store
0xe000_0190, // MUL Rd=Rm
0xe082_2190, // UMULL RdHi=RdLo 0xe082_2190, // UMULL RdHi=RdLo
0xe8f0_0001, // LDM user-bank transfer with writeback 0xe8f0_0001, // LDM user-bank transfer with writeback
0xe100_f090, // SWP with PC destination 0xe100_f090, // SWP with PC destination

View file

@ -299,6 +299,9 @@ Other S-bit block transfers access User registers while using the current mode's
User-bank writeback, S-bit empty lists, and User-mode S-bit transfers return diagnostics. User-bank writeback, S-bit empty lists, and User-mode S-bit transfers return diagnostics.
These restrictions avoid unpredictable or unverified forms. These restrictions avoid unpredictable or unverified forms.
ARM multiplies accept a destination equal to the first operand: `MUL`/`MLA` with Rd=Rm and long multiplies with RdHi or RdLo equal to Rm.
Every operand is read before the result is written. R15 operands and RdHi=RdLo remain diagnostics.
`Cpu::enter_exception` explicitly supports software interrupt, undefined instruction, prefetch abort, data abort, IRQ, and FIQ entry. `Cpu::enter_exception` explicitly supports software interrupt, undefined instruction, prefetch abort, data abort, IRQ, and FIQ entry.
For synchronous exceptions, call it with PC at the faulting instruction. For synchronous exceptions, call it with PC at the faulting instruction.
For IRQ/FIQ, call it between instructions, with PC at the next instruction. For IRQ/FIQ, call it between instructions, with PC at the next instruction.