Skip to content

Repository files navigation

m68kdasm

A Go disassembler for the Motorola 68000 family (68000, 68010, CPU32, 68020, 68030, 68040, 68060).

m68kdasm translates big-endian m68k machine code into readable assembly and structured decode metadata. It is intended for emulators, debuggers, trace tools, and binary-analysis workflows that need more than just formatted text.

Features

  • Fast opcode dispatch using a hierarchical jump table, gated per target CPU.
  • Full 68000 instruction coverage (every mnemonic in the standard 68000 opcode map), plus branches, arithmetic, logic, shifts, and BCD.
  • Full 68000 addressing-mode decoding, including PC-relative and immediate forms, plus the 68020+ full extension word (memory indirect, scaled/suppressed index, 0/16/32-bit base and outer displacements).
  • CPU-variant opcode support: MOVEC/MOVES/RTD (68010+), BGND (CPU32), and 68020+ additions — BFxxx bitfield ops, CAS, CHK2/CMP2, 32×32 MULU.L/MULS.L/DIVU.L/DIVS.L, PACK/UNPK, CALLM/RTM (68020/68030 only), TRAPcc, LINK.L, EXTB.L, CHK.L.
  • Optional FPU decoding (68881/68882, or the 68040/68060's built-in FPU): the full FMOVE/FADD/FSUB/FMUL/FDIV/FCMP/FABS/FNEG/FSQRT/FTST family in all 7 data formats (including packed BCD with k-factor), FMOVEM (both the FPn and FPCR/FPSR/FPIAR register-list forms), the transcendental and math-extension function sets, FMOVECR, FSINCOS, the FBcc/FDBcc/FScc/FTRAPcc condition family, and FSAVE/FRESTORE.
  • Optional PMMU decoding (68851, or the 68030's built-in PMMU): PMOVE's full register set, PMOVEFD, PFLUSHA/PFLUSH/PFLUSHS/PFLUSHR, PLOADR/PLOADW, PTESTR/PTESTW, PSAVE/PRESTORE, the PBcc/PDBcc/PScc/PTRAPcc condition family, and the 68040's own simplified single-word PMMU forms.
  • Optional auto-generated labels for branch/call targets within a disassembled range (BRA l00001010 plus a matching Instruction.Label), with a caller-supplied Symbolizer name always taking precedence over a synthetic one.
  • Exact decoded instruction length via Instruction.Size.
  • Decoded extension words via Instruction.ExtensionWords.
  • Structured metadata for mnemonic, operands, branch targets, immediates, and effective-address kinds.
  • Resolved effective-address targets for absolute and PC-relative operands.
  • Slice, io.ReaderAt, and callback-based decode entry points.
  • Precise partial-decode errors that report missing-byte counts.
  • Optional symbol formatting hooks for resolved addresses.
  • ELF helpers for disassembling 68000 ELF binaries.

Selecting a target CPU

DecodeOptions.CPU selects which 68k family member to decode for. The zero value, M68000, decodes plain 68000 opcodes only and is unaffected by any of the CPU-variant work described here.

inst, err := m68kdasm.DecodeWithOptions(data, address, m68kdasm.DecodeOptions{
    CPU: m68kdasm.M68020,
})

Available values: M68000, M68010, CPU32, M68020, M68030, M68040, M68060. Opcode availability isn't a strict "newer implies older" chain — CPU32 is a 68010-derived core with its own additions and a reduced 68020-style addressing mode, and CALLM/RTM are valid on 68020/68030 but were removed starting with the 68040 — so each opcode is tagged with the exact set of CPUs it decodes on rather than a minimum version.

Not yet implemented: CAS2, CPU32's TBLS/TBLU table-lookup family, and the 68040-specific MOVE16/CINV/CPUSH. See docs/design-cpu-variants.md for the full design and rationale.

Selecting FPU and PMMU support

DecodeOptions.FPU and DecodeOptions.MMU opt in to decoding the F-line coprocessor instruction set — the 68881/68882 FPU (or a 68040/68060's built-in FPU) and the 68851 PMMU (or a 68030's built-in PMMU), respectively. Both default to false, so F-line opcodes render as DC.W unless a caller explicitly asks for them. FPU and MMU presence are attached-coprocessor questions independent of CPU — a bare 68020 with an external 68881 and a 68040's built-in FPU both just set FPU: true.

inst, err := m68kdasm.DecodeWithOptions(data, address, m68kdasm.DecodeOptions{
    CPU: m68kdasm.M68020,
    FPU: true,
    MMU: true,
})

FPU coverage is complete for the mainstream instruction set; on the PMMU side, only PVALID remains unimplemented. See docs/design-fpu-mmu.md for the full design, delivery history, and the handful of real bit-encoding bugs this work found and fixed along the way (including one in the pre-existing integer Bcc/DBcc branch-target math).

Install

go get github.com/jenska/m68kdasm

API Overview

Single-instruction decode:

  • Decode(data []byte, address uint32)
  • DecodeWithOptions(data []byte, address uint32, opts DecodeOptions)
  • DecodeReaderAt(reader io.ReaderAt, address uint32)
  • DecodeReaderAtWithOptions(reader io.ReaderAt, address uint32, opts DecodeOptions)
  • DecodeFunc(read ReadFunc, address uint32)
  • DecodeFuncWithOptions(read ReadFunc, address uint32, opts DecodeOptions)

Sequential decode:

  • DisassembleRange(data []byte, startAddress uint32)
  • DisassembleRangeWithOptions(data []byte, startAddress uint32, opts DecodeOptions)

Quick Start

package main

import (
	"fmt"
	"log"

	"github.com/jenska/m68kdasm"
)

func main() {
	code := []byte{
		0x20, 0x7C, 0x00, 0x00, 0x21, 0x40, // MOVEA.L #$00002140, A0
		0x4E, 0x75, // RTS
	}

	instrs, err := m68kdasm.DisassembleRange(code, 0x1000)
	if err != nil {
		log.Fatal(err)
	}

	for _, inst := range instrs {
		fmt.Printf("%08X  %-24s size=%d ext=%v\n",
			inst.Address,
			inst.Assembly(),
			inst.Size,
			inst.ExtensionWords,
		)
	}
}

Example output:

00001000  MOVEA.L #$00002140, A0  size=6 ext=[0 8512]
00001006  RTS                      size=2 ext=[]

Structured Metadata

Each decoded instruction includes both rendered text and structured fields:

inst, err := m68kdasm.Decode([]byte{
	0x20, 0x7C, 0x00, 0x00, 0x21, 0x40, // MOVEA.L #$00002140, A0
}, 0x2000)
if err != nil {
	log.Fatal(err)
}

fmt.Println(inst.Mnemonic)              // MOVEA.L
fmt.Println(inst.Operands)              // #$00002140, A0
fmt.Println(inst.Size)                  // 6
fmt.Println(inst.ExtensionWords)        // [0 8512]
fmt.Println(inst.Metadata.MnemonicBase) // MOVEA
fmt.Println(inst.Metadata.SizeSuffix)   // L

src := inst.Metadata.Operands[0]
fmt.Println(src.Kind)                           // effective_address
fmt.Println(src.EffectiveAddress.Kind)          // immediate
fmt.Println(src.EffectiveAddress.Immediate.Value) // 8512

dst := inst.Metadata.Operands[1]
fmt.Println(dst.Kind)               // register
fmt.Println(dst.Register.Kind)      // address
fmt.Println(dst.Register.Number)    // 0

Useful metadata fields:

  • Instruction.Size: exact decoded byte length.
  • Instruction.Bytes: exact bytes consumed by the instruction.
  • Instruction.ExtensionWords: decoded words after the opcode word.
  • Instruction.Metadata.BranchTarget: resolved branch target when applicable.
  • Instruction.Metadata.ImmediateValues: immediate operands collected in structured form.
  • Instruction.Metadata.Operands: per-operand metadata, including effective-address details.
  • Operand.EffectiveAddress.ResolvedAddress: computed target for absolute and PC-relative effective addresses when available.

Streaming Decode

If your emulator or debugger fetches bytes from a bus instead of a prebuilt slice, you can decode directly from an io.ReaderAt or callback.

Using io.ReaderAt:

reader := bytes.NewReader([]byte{
	0x4E, 0xB9, 0x00, 0x00, 0x12, 0x34, // JSR $00001234
})

inst, err := m68kdasm.DecodeReaderAt(reader, 0x1000)
if err != nil {
	log.Fatal(err)
}

fmt.Println(inst.Assembly()) // JSR $00001234

Using a callback:

mem := []byte{0x67, 0x08} // BEQ.S $000A

inst, err := m68kdasm.DecodeFunc(func(address uint32, p []byte) (int, error) {
	if int(address) >= len(mem) {
		return 0, io.EOF
	}
	n := copy(p, mem[address:])
	if n < len(p) {
		return n, io.EOF
	}
	return n, nil
}, 0)
if err != nil {
	log.Fatal(err)
}

fmt.Println(inst.Assembly()) // BEQ.S $000A

Symbolized Output

You can keep raw metadata while rendering resolved addresses as symbols:

inst, err := m68kdasm.DecodeWithOptions([]byte{
	0x4E, 0xB9, 0x00, 0x00, 0x12, 0x34, // JSR $00001234
}, 0, m68kdasm.DecodeOptions{
	Symbolizer: m68kdasm.SymbolizeFunc(func(address uint32) (string, bool) {
		if address == 0x1234 {
			return "_bios_init", true
		}
		return "", false
	}),
})
if err != nil {
	log.Fatal(err)
}

fmt.Println(inst.Assembly())                 // JSR _bios_init
fmt.Println(inst.Metadata.Operands[0].Text)  // $00001234

This is useful for:

  • symbolized trace logs
  • debugger disassembly views
  • breakpoint or stop-condition logic based on structured targets

PC-relative operands participate in the same symbolization flow as absolute addresses once their resolved target is known:

inst, err := m68kdasm.DecodeWithOptions([]byte{
	0x4E, 0xBA, 0x00, 0x0E, // JSR (16,PC)
}, 0x1000, m68kdasm.DecodeOptions{
	Symbolizer: m68kdasm.SymbolizeFunc(func(address uint32) (string, bool) {
		if address == 0x1012 {
			return "_pc_target", true
		}
		return "", false
	}),
})
if err != nil {
	log.Fatal(err)
}

fmt.Println(inst.Assembly())                                // JSR _pc_target
fmt.Println(*inst.Metadata.Operands[0].EffectiveAddress.ResolvedAddress) // 4114

Auto-Generated Labels

DecodeOptions.Labels opts in to synthetic label generation for branch/call targets that fall within a disassembled range and land on a decoded instruction — no need to supply your own Symbolizer just to see readable branch targets in a listing.

instrs, err := m68kdasm.DisassembleRangeWithOptions([]byte{
	0x60, 0x02, // BRA.S $1004
	0x4E, 0x71, // NOP
	0x4E, 0x75, // RTS
}, 0x1000, m68kdasm.DecodeOptions{
	Labels: &m68kdasm.LabelOptions{},
})
if err != nil {
	log.Fatal(err)
}

fmt.Println(instrs[0].Assembly()) // BRA.S l00001004
fmt.Println(instrs[2].Label)      // l00001004
fmt.Println(instrs[2].String())
// l00001004:
// 00001004: RTS

Labels are created by branch/decrement-branch instructions (Bcc, BSR, DBcc, FBcc, FDBcc, PBcc, PDBcc) and by JSR/JMP/PEA/LEA targeting an absolute or PC-relative address — PEA/LEA are included specifically to catch indirect-call trampolines (LEA sub,A0 then JSR (A0)), which also means a LEA/PEA loading a plain data-buffer address gets labeled too, a deliberate trade-off. Once an address has a label, every operand referencing it renders that name, not just the operand that created it — a plain MOVE.L $addr,D0 picks up the same label a JSR elsewhere in the range established. A caller-supplied Symbolizer always takes precedence over a synthetic label; the default l prefix is configurable via LabelOptions.Prefix.

This is a DisassembleRange/DisassembleRangeWithOptions (and ELFDisassembler WithOptions) feature only — it needs the full instruction stream to find forward references, so DecodeOptions.Labels is silently a no-op on the single-instruction Decode* entry points. See docs/design-labels.md for the full design.

Partial Decode Errors

Truncated fetches return *PartialDecodeError with the missing-byte count and context:

_, err := m68kdasm.Decode([]byte{0x4E, 0x72}, 0x2000) // STOP without immediate word
if err != nil {
	var partial *m68kdasm.PartialDecodeError
	if errors.As(err, &partial) {
		fmt.Println(partial.Missing) // 2
		fmt.Println(partial.Context) // STOP immediate
	}
}

This is especially handy for trace logs and emulator diagnostics where you want to distinguish a broken fetch stream from an unknown opcode.

ELF Disassembly

Disassemble sections from a Motorola 68000 ELF binary:

package main

import (
	"fmt"
	"log"

	"github.com/jenska/m68kdasm"
)

func main() {
	elf, err := m68kdasm.OpenELF("program.elf")
	if err != nil {
		log.Fatal(err)
	}
	defer elf.Close()

	instrs, err := elf.DisassembleSection(".text")
	if err != nil {
		log.Fatal(err)
	}

	for _, inst := range instrs {
		fmt.Println(inst.String())
	}
}

DisassembleSection/DisassembleAllExecutableSections always decode as plain M68000 with no Symbolizer. Use DisassembleSectionWithOptions/DisassembleAllExecutableSectionsWithOptions to pass a full DecodeOptions (CPU/FPU/MMU selection, a Symbolizer, Labels, etc.) through to ELF-sourced disassembly.

Building And Testing

make build
make test
make fmt

Tests include assembler round trips, decoder dispatch parity, streaming decode coverage, metadata checks, symbolized rendering, and partial-error behavior.

Architecture

The decoder uses a two-level dispatch mechanism:

  1. A top-level jump table partitions the opcode space by high nibble.
  2. Per-region pattern tables apply masks in precedence order to select the final decoder, gated by target CPU and (for F-line coprocessor opcodes) the FPU/MMU capability flags.

The opcode table lives in internal/decoders/opcodetable.go. Instruction decoders are grouped by family across internal/decoders/*.go — integer arithmetic, logic, branches, and addressing modes in their own files, with fpu.go, pmmu.go, pmmu040.go, and pmmu_cond.go holding the FPU and PMMU coprocessor decoders.

License

This project is licensed under the MIT License. See LICENSE for details.

About

m68kdasm – 68k Disassembler (Go)

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages