From 50369ddf28a56a9208e7b83f4cbebb39df0e7f7c Mon Sep 17 00:00:00 2001 From: Benjamin Herrenschmidt Date: Thu, 1 Oct 2026 16:44:11 +0200 Subject: [PATCH] Fix 68020 DIVS.L/DIVU.L overflow and signed 64-bit division DIVS.L of $80000000 by -1 in the 32-bit form used a native i32 division, which panics ("attempt to divide with overflow") and takes the emulator down. A program feeding edge cases to the Toolbox fixed-point routines hit this on a Mac II with System 4.1. While there: - the 64-bit signed form zero-extended the divisor, so a negative divisor gave a wrong quotient; - overflow was only tested on the 64-bit forms, and accepted any quotient whose high long was 0 or $FFFFFFFF: an unsigned quotient above 32 bits, or a signed one outside the i32 range, went through. Do signed divisions in 64 bits with a sign-extended divisor, checked for i64::MIN / -1, and report overflow (V set, operands unchanged) whenever the quotient doesn't fit in 32 bits. Signed-off-by: Benjamin Herrenschmidt --- core/src/cpu_m68k/cpu.rs | 40 ++++++------- core/src/cpu_m68k/tests/divl.rs | 99 +++++++++++++++++++++++++++++++++ core/src/cpu_m68k/tests/mod.rs | 1 + 3 files changed, 116 insertions(+), 24 deletions(-) create mode 100644 core/src/cpu_m68k/tests/divl.rs diff --git a/core/src/cpu_m68k/cpu.rs b/core/src/cpu_m68k/cpu.rs index 29053c48..c99ea385 100644 --- a/core/src/cpu_m68k/cpu.rs +++ b/core/src/cpu_m68k/cpu.rs @@ -4142,33 +4142,25 @@ where self.prefetch_pump()?; - let (quotient, remainder) = match (extword.signed(), extword.size()) { - (false, false) => { - // 32-bit unsigned - (dividend / divisor, dividend % divisor) - } - (true, false) => { - // 32-bit signed - ( - ((dividend as u32 as i32) / (divisor as u32 as i32)) as i64 as u64, - ((dividend as u32 as i32) % (divisor as u32 as i32)) as i64 as u64, - ) - } - (false, true) => { - // 64-bit unsigned - (dividend / divisor, dividend % divisor) - } - (true, true) => { - // 64-bit signed - ( - ((dividend as i64) / (divisor as i64)) as u64, - ((dividend as i64) % (divisor as i64)) as u64, - ) + let (quotient, remainder, overflow) = if extword.signed() { + let a = if extword.size() { + dividend as i64 + } else { + i64::from(dividend as u32 as i32) + }; + let b = i64::from(divisor as u32 as i32); + // None for i64::MIN / -1, which overflows too + match a.checked_div(b) { + Some(q) => (q as u64, (a % b) as u64, i32::try_from(q).is_err()), + None => (0, 0, true), } + } else { + let q = dividend / divisor; + (q, dividend % divisor, q > u64::from(u32::MAX)) }; - // Check overflow conditions on 64-bit divisions if the result exceeds 32-bit - if extword.size() && ![u32::MIN, u32::MAX].contains(&((quotient >> 32) as u32)) { + // The quotient must fit in 32 bits; the operands are left unchanged + if overflow { debug!("DIV.l overflow"); self.regs.sr.set_v(true); return Ok(()); diff --git a/core/src/cpu_m68k/tests/divl.rs b/core/src/cpu_m68k/tests/divl.rs new file mode 100644 index 00000000..557480dc --- /dev/null +++ b/core/src/cpu_m68k/tests/divl.rs @@ -0,0 +1,99 @@ +//! 68020 long division (DIVS.L/DIVU.L) tests + +use crate::bus::Address; +use crate::bus::testbus::Testbus; +use crate::cpu_m68k::{CpuM68020Fpu, M68020_ADDRESS_MASK}; +use crate::types::Long; + +type TestCpu = CpuM68020Fpu>; + +const SIGNED: u16 = 0x0800; +const SIZE_64: u16 = 0x0400; + +/// Runs DIVx.L D2,D1:D0 (or DIVx.L D2,D0 without `SIZE_64`) and returns the CPU +fn divl(flags: u16, d1: Long, d0: Long, d2: Long) -> TestCpu { + const PC: Address = 0x1000; + let ext = flags | if flags & SIZE_64 != 0 { 1 } else { 0 }; + let bus = Testbus::new(M68020_ADDRESS_MASK); + let mut cpu = TestCpu::new(bus); + + for (i, word) in [0x4C42, ext, 0x4E71, 0x4E71].into_iter().enumerate() { + let addr = PC + (i as Address * 2); + cpu.bus.mem.insert(addr, (word >> 8) as u8); + cpu.bus.mem.insert(addr + 1, word as u8); + } + + cpu.regs.isp = 0x800; + cpu.regs.sr.set_supervisor(true); + cpu.regs.sr.set_int_prio_mask(7); + cpu.set_pc(PC).expect("set_pc failed"); + cpu.prefetch_refill().expect("prefetch_refill failed"); + + cpu.regs.write_d(0, d0); + cpu.regs.write_d(1, d1); + cpu.regs.write_d(2, d2); + cpu.step().unwrap(); + cpu +} + +fn regs(cpu: &TestCpu) -> (Long, Long) { + (cpu.regs.read_d(1), cpu.regs.read_d(0)) +} + +#[test] +fn divs_l_32_min_by_minus_one_overflows() { + let cpu = divl(SIGNED, 0x1234_5678, 0x8000_0000, 0xFFFF_FFFF); + assert!(cpu.regs.sr.v()); + assert_eq!(regs(&cpu), (0x1234_5678, 0x8000_0000)); +} + +#[test] +fn divs_l_32() { + let cpu = divl(SIGNED, 0, -7i32 as Long, 2); + assert!(!cpu.regs.sr.v()); + assert!(cpu.regs.sr.n()); + assert_eq!(cpu.regs.read_d::(0), -3i32 as Long); +} + +#[test] +fn divs_l_64_negative_divisor() { + // 0x1_0000_0000 / -2 = -0x8000_0000 + let cpu = divl(SIGNED | SIZE_64, 1, 0, -2i32 as Long); + assert!(!cpu.regs.sr.v()); + assert!(cpu.regs.sr.n()); + assert_eq!(regs(&cpu), (0, 0x8000_0000)); + + // -7 / -2 = 3, remainder -1 + let cpu = divl(SIGNED | SIZE_64, 0xFFFF_FFFF, -7i32 as Long, -2i32 as Long); + assert!(!cpu.regs.sr.v()); + assert_eq!(regs(&cpu), (0xFFFF_FFFF, 3)); +} + +#[test] +fn divs_l_64_overflow() { + // 0x1_0000_0000 / 2 = 0x8000_0000 doesn't fit a signed long + let cpu = divl(SIGNED | SIZE_64, 1, 0, 2); + assert!(cpu.regs.sr.v()); + assert_eq!(regs(&cpu), (1, 0)); + + // i64::MIN / -1 + let cpu = divl(SIGNED | SIZE_64, 0x8000_0000, 0, 0xFFFF_FFFF); + assert!(cpu.regs.sr.v()); + assert_eq!(regs(&cpu), (0x8000_0000, 0)); +} + +#[test] +fn divu_l_64() { + // 0x1_0000_0000 / 2 = 0x8000_0000 + let cpu = divl(SIZE_64, 1, 0, 2); + assert!(!cpu.regs.sr.v()); + assert_eq!(regs(&cpu), (0, 0x8000_0000)); +} + +#[test] +fn divu_l_64_overflow() { + // 0xFFFF_FFFF_0000_0000 / 1: the quotient's high long is all ones + let cpu = divl(SIZE_64, 0xFFFF_FFFF, 0, 1); + assert!(cpu.regs.sr.v()); + assert_eq!(regs(&cpu), (0xFFFF_FFFF, 0)); +} diff --git a/core/src/cpu_m68k/tests/mod.rs b/core/src/cpu_m68k/tests/mod.rs index 06947200..0029ffa2 100644 --- a/core/src/cpu_m68k/tests/mod.rs +++ b/core/src/cpu_m68k/tests/mod.rs @@ -1,3 +1,4 @@ +mod divl; mod group2_exceptions; mod illegal_exception; mod interrupt;