MuForce

MuForce is the mmu.library-based successor of Michael Sinz's Enforcer. It is part of the MuTools shipped in the NDK 3.2 MuTools/ drawer (this article describes MuForce 46.4 (23.01.2021), documented by MuForce.guide 1.17). Like Enforcer, it uses the MMU to catch accesses to the zero page and to non-existent memory and prints a report — a "hit" — for each one. Unlike Enforcer, it does not program the MMU directly: it asks mmu.library to set up the tables, so it can run alongside other mmu.library clients such as MuFastRom, virtual-memory software and MuGuardianAngel.

The guide calls this release "a V40 release of Michael Sinz's Enforcer V37" and is blunt about what it is for:

MuForce is provided for software testing. In this role it is vital. Software that causes MuForce hits may not be able to run on newer hardware. [...] The system can NOT survive software that causes hits.

However, MuForce is NOT a system protector.

MuForce has no ill effects on correctly working software. If a program fails while MuForce is running, the program is at fault.

What MuForce catches

Event Reported as
Read or write of the zero page (the low memory area, including the vector table) BYTE/WORD/LONG READ from / ... WRITE to hit
Read or write of address space not mapped to RAM, ROM or a known board same, with the faulting address
Writes into the ROM area MuManual ReadMe, release 42.9.1: "Marked the ROM area as WRITEPROTECTED rather than ROM on exit, causing GURUs on erroneous writes into that area."
Calls to exec Alert() Alert !! Alert xxxxxxxx report. Disable with NOALERTPATCH.
Other 680x0 exceptions the task does not handle itself (illegal instruction, line A/F, privilege violation, ...) exception report, then the normal "Software error" requester. Disable with NOGURUPATCH.
Exceptions raised in supervisor mode only with CAPTURESUPER
Writes to code write-protected by MuLink write hit (MuLink'd programs switch to MAPP_WRITEPROTECTED protection when MuForce is active)

Accesses that hit allocatable but currently free memory are not MuForce's job. Those are caught by MuGuardianAngel, which marks free pages invalid and lets MuForce report the resulting fault.

Two rules apply to reads and writes:

  • An illegal read returns 0 by default, or $ABADFEED with DEADLY.
  • An illegal write is not carried out, and the program continues after the hit.
  • With FATALHITS, MuForce does not continue the program. It passes the fault on to the default exception handler.

Requirements and installation

  • An MMU. MuForce contains no CPU-specific code itself. mmu.library refuses to install it on CPUs without a working MMU (for example 68EC030 or 68EC040). The 68LC040 has an MMU and is supported.
  • mmu.library in LIBS:. On a 68040 or 68060, mmu.library also needs 68040.library or 68060.library.
  • No other MMU debugger. Remove Enforcer, CyberGuard and similar tools.
  • Optional: SegTracker. If SegTracker is already running when MuForce starts, MuForce uses its public seglist tracking to name the hunk and offset of addresses.

Start MuForce after SetPatch and, if you use it, after SegTracker:

run >NIL: <NIL: MuForce STACKLINES=4 DISPC SHOWPC

MuForce can also be started from Workbench, in which case it reads the same options from its icon's tooltypes. The NDK includes the project icons MuForce, MuForce_Off and MuForce_Window. Stop MuForce with MuForce QUIT (or OFF), or by sending CTRL-C to its process. Since mmu.library release 42.11, MuForce cannot quit while MuGuardianAngel is running.

On a Bridge-Board 68030 system, MuForce QUIET right after SetPatch only opens and locks mmu.library so that the data cache can be enabled. It reports nothing.

Options

Shell template (from the 46.4 binary):

QUIET/S,TINY/S,SMALL/S,SHOWPC/S,DISRANGE/K/N,DISPC=SHOWDIS/S,STACKLINES/K/N,
STACKCHECK/S,AREGCHECK/S,DREGCHECK/S,DATESTAMP/S,DEADLY/S,FATALHITS/S,FSPACE/S,
VALIDZERO/S,VERBOSE/S,LED/K/N,PARALLEL/S,RAWIO/S,FILE/K,STDIO/S,BUFFERSIZE/K/N,
INTRO/K,PRIORITY/K/N,NOALERTPATCH/S,NOGURUPATCH/S,CAPTURESUPER/S,NEWVBR/S,
DISABLEBELL/S,PREPMUNGWALL/S,ON/S,QUIT=OFF/S

The tooltype list also accepts WINDOW/K.

Report content

Option Effect
TINY Prints only the first line of each hit.
SMALL Prints the hit line, the USP: line and the Name: line, with no registers or stack.
SHOWPC Adds PC-8: and PC *:, the 8 longwords before and from the PC. Ignored in QUIET, TINY and SMALL modes.
DISPC (= SHOWDIS) Adds a disassembly around the fault. Requires disassembler.library.
DISRANGE=n Sets roughly how many bytes to disassemble before and after the fault. Default 32.
STACKLINES=n Sets the number of Stck: lines (8 longwords each). Default 2, 0 disables, no upper limit. Also applies to alert reports.
STACKCHECK Checks every displayed stack longword against SegTracker and prints a ----> line for each match. Also applies to alert reports.
AREGCHECK / DREGCHECK Same check for the address and data registers.
DATESTAMP Prints date and time before each hit. The time is accurate to about ±1 s, because it is refreshed once a second by the MuForce task.
INTRO=string Prints this string at the start of every hit, for example INTRO="*NBad Program!".
VERBOSE Prints the MMU setup at startup, much like MuScan.

Behaviour

Option Effect
DEADLY Illegal reads return $ABADFEED instead of 0, which provokes follow-on hits.
FATALHITS Reports the hit, then passes the fault to the default exception handler instead of continuing. Useful with a debugger.
VALIDZERO Leaves the zero page unrestricted. This makes MuForce miss most hits. It exists only for running ShapeShifter, because MacOS keeps globals there.
FSPACE Makes the $00F00000 area writable, for boards placed in "F-space".
NEWVBR Always relocates and rebuilds the vector base. By default MuForce reuses an already relocated VBR.
NOALERTPATCH Does not patch exec Alert().
NOGURUPATCH Does not capture other 68k exceptions.
CAPTURESUPER Also captures exceptions raised in supervisor mode. In an interrupt, MuForce can print the report but cannot stop the interrupt.
PREPMUNGWALL Compatibility hack that makes the old MungWall believe Enforcer is running. The guide advises against it: MungWall is obsolete (use MuGuardianAngel instead), the hack is not needed if MungWall starts first, and it can upset Sushi.
QUIET Reports nothing (Bridge-Board use, see above).
ON / QUIT=OFF Turns MuForce on (the default) or off.

Output channel

Option Effect
(default) Writes directly to the serial port hardware. MuForce does not call RawIOInit(); the OS or the user must set up the port, as with Enforcer.
PARALLEL Writes to the parallel port hardware.
RAWIO Buffers the report and outputs it from the MuForce process through exec RawPutChar(), so tools such as Sushi or Sashimi can redirect it. A hit that ends in a crash may not be printed.
FILE=name Writes to a file, for example CON:0/0/640/100/HIT/AUTO/WAIT, or * for the starting console. The guide warns against files on disk, which can be corrupted if the system crashes.
STDIO Writes to standard output (Shell only).
BUFFERSIZE=n Sets the buffer for RAWIO, FILE and STDIO. The minimum and default is 8192.
PRIORITY=n Sets the I/O task priority. Default 99; values outside −127..127 fall back to the default.
LED=n Sets how long the power LED flashes per hit: about 1.3 µs × n. Default 1; 0 leaves the LED alone.
DISABLEBELL Suppresses the BEL character (0x07) that is otherwise sent with each hit.
WINDOW=path Tooltype only: the output window for a Workbench start, for example CON:0/0/640/100/MuForce. Default NIL:. Combine it with FILE=* to see hits in that window.

The special I/O modes need the buffer because no OS calls can be made at fault time. If the faulting task is in Forbid() or Disable(), the MuForce task cannot run, and hits pile up in the buffer until it overflows.

Reading a hit

A typical report produced with DATESTAMP SHOWPC DISPC:

03-Jul-99  14:12:23
WORD WRITE to  00000000        data=0000       PC: 01044774
USP : 010D07B8 SR: 0004  (U0)(-)(-)  TCB: 0101A8C8
Data: DDDD0000 DDDD1111 DDDD2222 DDDD3333 01044722 DDDD5555 DDDD6666 DDDD7777
Addr: AAAA0000 AAAA1111 AAAA2222 AAAA3333 AAAA4444 01044722 00002920 001FFFC4
Stck: 00000000 00FA06D6 00001000 0101B2AC 01F80000 00E9A8F0 01077004 0A0A0104
Stck: 30E80000 010D09F0 010D07F0 010D0BF0 00000200 000000BC 00000020 010D0814
PC-8: 2222263C DDDD3333 280D2A3C DDDD5555 2C3CDDDD 66662E3C DDDD7777 31C00000
PC *: 4EAEFF7C 20144EAE FF8811C1 01014EAE FF7621C0 01024EAE FF822E3C 35000000
01044770 :  31c0 0000                  move.w d0,$0.w
01044774 : *4eae ff7c                  jsr -$84(a6)
01044778 :  2014                       move.l (a4),d0
Name: "Initial CLI"  CLI: "lawbreaker"  Hunk 0000 Offset 00000074
Line Meaning
date Present only with DATESTAMP.
first line Access type and size, the illegal address, the data written (for writes) and the PC. Sizes are BYTE, WORD and LONG; a plain READ/WRITE is reported for MOVEM, double or extended FMOVE and FMOVEM. (INST) marks an instruction fetch.
USP: The user stack pointer (A7 is not repeated in Addr:) and the status register.
(U0)(F)(D) First field: U = user or S = supervisor mode, followed by the interrupt level 0–7. F means the task was in Forbid(). D means it was in Disable().
TCB: The faulting task's struct Task.
Data: / Addr: D0–D7 and A0–A6.
Stck: The stack back-trace (STACKLINES). If the stack itself is invalid, MuForce prints a message saying so.
----> SegTracker lines: one for the PC, then one for each stack longword that hits a tracked seglist. Read them bottom-up as call order, or top-down as a back-trace. Needs SegTracker loaded before MuForce.
PC-8: / PC *: Raw code around the PC (SHOWPC).
disassembly Shown with DISPC. * marks the PC.
Name: The task name; for a CLI process also the command name; and, if the PC lies in the process's own seglist, its hunk and offset. This works for any LoadSeg()ed program and does not need SegTracker. Interrupt code shows as Processor Interrupt Level x.

Alert reports have the form Alert !! Alert 35000000 TCB: ... CTX: ... USP: .... Decode the alert number with exec/alerts.h (see Alerts and Gurus). CTX: is the address of the task's mmu.library MMUContext (see mmu/context.h). Since 46.4, alert and guru reports also print the PC at which the fault was detected.

To turn the hunk and offset into a source line, use the FindHit documentation that the guide refers to for SegTracker output, together with your linker map.

The reported PC is not always the faulting instruction

Read the PC with care. How exact it is depends on the CPU:

CPU PC accuracy
68020 + 68851 / 68030 Faults are continued, not rerun. On write faults the PC may already point after the instruction.
68040 Write faults happen after the instruction executes, except for MOVEM and wide FPU moves. Often one or more further instructions have run too. After a branch or JSR, the PC can point to the branch target. A delayed write fault can be reported after a later read fault, and the library may call its handler two or three times for one instruction. A MOVEM, fmove.x or fmovem usually shows as a single hit.
68060 Full-restart model: faults are taken before the instruction executes, so the PC is "usually" exact.

The guide's example shows the 040 case: a move.b d1,$101.w is reported at a PC inside a jump table, because the CPU had already jumped away.

Two further CPU notes from the guide:

  • MOVE16 and double indirection. On the 68040/060, an access fault caused by MOVE16 cannot be handled and produces a real guru. Double-indirect modes such as move.l ([a4]),d0 can return wrong data after a fault because of a firmware bug in these CPUs. Avoid both in code that may fault.
  • Low memory on 68040/060. The MMU page size is 4K or 8K, so protecting the low 1K means marking the first 4K invalid. The library emulates accesses to the valid 3K, and MuForce tries to move structures out of that page so the system keeps a reasonable speed. Some structures, such as the graphics copper list, cannot be moved. Caching AbsExecBase in a local variable avoids slow emulated reads of location 4.

System patches

MuForce and mmu.library install the following:

Patched By Purpose
ColdReboot() 2.0+ mmu.library Tears down the MMU tables before reboot.
Alert() 1.3+ mmu.library Removes itself on dead-end alerts.
Alert() 1.3+ MuForce Reports alerts like hits (NOALERTPATCH disables).
CachePreDMA(), CachePostDMA(), CacheControl() mmu.library 68040 cache handling and logical-to-physical translation for DMA.
AddTask(), exec and dos default exception handlers MuForce (40.21+) Captures unhandled 68k exceptions (NOGURUPATCH disables).
Exception vectors 3–7, 10, 13, 14, 24, 32–47 (TRAP #0–15), 56–58 MuForce, with CAPTURESUPER Captures supervisor-mode exceptions. Each one is first redirected to the task's own trap code.
Vector 25, Level 1 Interrupt Autovector MuForce, always Output handler.

When NOGURUPATCH is not set, an uncaught exception is printed and the task then either goes to the DOS "Software error" requester or halts in Wait(SIGBREAKF_CTRL_E). In the halted state a post-mortem debugger can pick it up: D2 holds the guru number, A2 the user stack pointer, and D0–D7/A0–A6 from the fault are pushed on the user stack.

Tools that patch the same vectors can cause trouble. If MuForce crashes on exit, the MuManual ReadMe points to "SaferPatches" clones as the usual cause.

Tuning the MMU table for MuForce

MuForce marks as invalid everything the library considers unused. Hardware that does not AutoConfig may need its address range declared with MuSetCacheMode, for example:

MuSetCacheMode from 0x00f00000 size 0x00080000 CACHEINHIBIT IO

The opposite also applies. If a third-party 68040/68060.library maps ranges for hardware you do not have, mark them invalid so MuForce catches accesses there. Do this only after MuForce is installed:

MuSetCacheMode from 0xfff00000 size 0x00100000 invalid
MuSetCacheMode from 0x00f00000 size 0x00080000 invalid

If the machine locks up under MuForce, run MuForce VERBOSE. If the output does not list $00F00000 as a board address, try FSPACE.

Writing debuggers that cooperate with MuForce

Trapping hits

A debugger must start after MuForce; give a detached MuForce a few seconds to build its tables. The preferred interface is an mmu.library exception hook on MMUEH_SEGFAULT (constants in mmu/exceptions.h, tags in mmu/mmutags.h). This is the setup shown in the guide:

MuForceHook = AddContextHook(MADTAG_CONTEXT, ctx,
                             MADTAG_TYPE,    MMUEH_SEGFAULT,
                             MADTAG_CODE,    &NewException,
                             MADTAG_DATA,    getreg(REG_A4),
                             /* MADTAG_TASK, mytask  -- for non-global debuggers */
                             MADTAG_NAME,    muname,
                             MADTAG_PRI,     -64,
                             TAG_DONE);
if (MuForceHook) {
    ActivateException(MuForceHook);
    /* ready to receive hits */
}
/* teardown: DeactivateException(), then RemContextHook() */

The handler is called with the struct ExceptionData in A0 and the MADTAG_DATA value in A4. It may use D0–D1/A0–A1/A4–A5. It must return with D0 = 0 and the Z flag set once it has handled the fault.

MuForce's own handler works like this:

  • It prints the report.
  • It stores the value to be returned for a read in exd_Data and sets EXDB_READBACK in exd_Flags. This lets the faulting instruction finish (a read gets the stored value, a write counts as done).

Instead of EXDB_READBACK, a debugger can set EXDB_CALL and put a function pointer in exd_ReturnPC. The library then calls that routine in user mode, where it can, for example, signal the debugger and wait. The library's exceptions.doc autodoc has the details.

Not causing hits

TypeOfMem() returning 0 does not prove an address is invalid, because ROM, chip registers and boards are not in the memory list. The Enforcer advice to scan expansion.library is no longer reliable either, since some boards bypass AutoConfig. Ask mmu.library instead:

  1. Get the context: DefaultContext() for normal applications, CurrentContext() for an external task, or exd_Context inside a hook (legal there).
  2. Call GetPagePropertiesA() on the address. If the range crosses a page boundary (GetPageSize()), call it on the end address as well.
  3. Treat the address as unsafe if MAPP_IO, MAPP_SWAPPED or MAPP_INVALID is set.

This works on any CPU, from interrupts and exceptions. Code in MuLink'd programs is write-protected for user mode only, so a debugger setting breakpoints must write them in supervisor mode and then flush the cache.

Writing MuForce-clean code

  • Never dereference NULL or an uninitialised pointer. In C, check every allocation and OpenLibrary() result before use.
  • Do not read AbsExecBase (location 4) repeatedly. Cache SysBase once at startup.
  • Do not probe hardware addresses or memory you did not allocate. Use mmu.library or documented OS calls to find hardware.
  • Do not write into code or ROM. Self-modifying code must call CacheClearU() and cannot be MuLink'd.
  • Test with DEADLY from time to time. A program that only survives because bad reads return 0 has a bug.
  • Run MuGuardianAngel as well. MuForce alone does not see accesses to freed or unallocated RAM.

See Also


Sources: NDK 3.2 R4 MuTools/MuForce.guide (1.17, 23.01.2021), MuTools/MuForce (46.4 template string), MuTools/MuLink.readme, MuManual/ReadMe, MuManual/Include/mmu/exceptions.h, MuManual/Include/mmu/mmutags.h, MuManual/Include/mmu/context.h Updated: 2026-11-13