Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 19 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
# m68kdasm

A Go disassembler for the Motorola 68000 CPU.
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.
- Broad 68000 instruction coverage including branches, arithmetic, logic, shifts, BCD, and control flow.
- Full 68000 addressing-mode decoding, including PC-relative and immediate forms.
- 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`.
- 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.
Expand All @@ -18,6 +19,20 @@ A Go disassembler for the Motorola 68000 CPU.
- 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.

```go
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: FPU (68881/68882/68040/68060 built-in) and PMMU (68851/68030) coprocessor instructions, `CAS2`, and CPU32's `TBLS`/`TBLU` table-lookup family, and the 68040-specific `MOVE16`/`CINV`/`CPUSH`. See [docs/design-cpu-variants.md](docs/design-cpu-variants.md) for the full design and rationale.

## Install

```bash
Expand Down
93 changes: 93 additions & 0 deletions cpu_variant_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package m68kdasm

import "testing"

func TestCPUOptionDefaultsToM68000(t *testing.T) {
if CPU(0) != M68000 {
t.Fatalf("expected zero value of CPU to be M68000")
}
}

func TestMOVES_RequiresCPU010OrLater(t *testing.T) {
// MOVES.W D0,(A1)
data := []byte{0x0E, 0x51, 0x00, 0x00}

inst, err := DecodeWithOptions(data, 0x1000, DecodeOptions{})
if err != nil {
t.Fatalf("unexpected error decoding with default (M68000) options: %v", err)
}
if inst.Mnemonic != "DC.W" {
t.Fatalf("expected MOVES to be unrecognized on M68000, got %q", inst.Assembly())
}

inst, err = DecodeWithOptions(data, 0x1000, DecodeOptions{CPU: M68010})
if err != nil {
t.Fatalf("unexpected error decoding with M68010: %v", err)
}
if inst.Assembly() != "MOVES.W D0, (A1)" {
t.Fatalf("got %q", inst.Assembly())
}
}

func TestBGND_OnlyOnCPU32(t *testing.T) {
data := []byte{0x4A, 0xFA}

inst, err := DecodeWithOptions(data, 0x1000, DecodeOptions{CPU: CPU32})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if inst.Assembly() != "BGND" {
t.Fatalf("got %q", inst.Assembly())
}

// On every other CPU the same bytes still fall through to the legacy TST
// family match (mode 7/reg 2 = PC displacement, hence the extra word),
// just not as BGND.
inst, err = DecodeWithOptions([]byte{0x4A, 0xFA, 0x00, 0x00}, 0x1000, DecodeOptions{CPU: M68020})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if inst.Assembly() == "BGND" {
t.Fatalf("BGND should not decode on M68020")
}
}

func TestRTD_RequiresCPU010OrLater(t *testing.T) {
data := []byte{0x4E, 0x74, 0x00, 0x08}

inst, err := DecodeWithOptions(data, 0x1000, DecodeOptions{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if inst.Mnemonic != "DC.W" {
t.Fatalf("expected RTD to be unrecognized on M68000, got %q", inst.Assembly())
}

inst, err = DecodeWithOptions(data, 0x1000, DecodeOptions{CPU: M68010})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if inst.Assembly() != "RTD #8" {
t.Fatalf("got %q", inst.Assembly())
}
}

func TestMOVEC_RequiresCPU010OrLater(t *testing.T) {
data := []byte{0x4E, 0x7A, 0x88, 0x01} // MOVEC VBR,A0

inst, err := DecodeWithOptions(data, 0x1000, DecodeOptions{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if inst.Mnemonic != "DC.W" {
t.Fatalf("expected MOVEC to be unrecognized on M68000, got %q", inst.Assembly())
}

inst, err = DecodeWithOptions(data, 0x1000, DecodeOptions{CPU: CPU32})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if inst.Assembly() != "MOVEC VBR, A0" {
t.Fatalf("got %q", inst.Assembly())
}
}
4 changes: 2 additions & 2 deletions disasm.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ func decodeInstruction(initial []byte, address uint32, reader addressReader, opt
}

opcode := binary.BigEndian.Uint16(data[:2])
decoder := decoders.FindDecoder(opcode)
decoder := decoders.FindDecoder(opcode, opts.CPU)
if decoder == nil {
return finalizeInstruction(decoders.DecodeUnknown(data, address, opcode), opts), nil
}
Expand All @@ -133,7 +133,7 @@ func decodeInstruction(initial []byte, address uint32, reader addressReader, opt
Bytes: data[:2],
}

err := decoder(data, opcode, decoderInst)
err := decoder(data, opcode, decoderInst, opts.CPU)
if err == nil {
return finalizeInstruction(decoderInst, opts), nil
}
Expand Down
6 changes: 3 additions & 3 deletions disasm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -376,11 +376,11 @@ func TestDecodeSymbolizerFormatsResolvedAddresses(t *testing.T) {
}

func TestDecodeSymbolizerFormatsPCRelativeAddresses(t *testing.T) {
data := []byte{0x4E, 0xBA, 0x00, 0x0E} // JSR (16,PC) at 0x1000 resolves to 0x1012
data := []byte{0x4E, 0xBA, 0x00, 0x10} // JSR (16,PC) at 0x1000 resolves to 0

inst, err := DecodeWithOptions(data, 0x1000, DecodeOptions{
Symbolizer: SymbolizeFunc(func(address uint32) (string, bool) {
if address == 0x1012 {
if address == 0x1014 {
return "_pc_target", true
}
return "", false
Expand All @@ -397,7 +397,7 @@ func TestDecodeSymbolizerFormatsPCRelativeAddresses(t *testing.T) {
t.Fatalf("Rohoperand wurde unerwartet überschrieben: %+v", inst.Metadata.Operands[0])
}
resolved := inst.Metadata.Operands[0].EffectiveAddress.ResolvedAddress
if resolved == nil || *resolved != 0x1012 {
if resolved == nil || *resolved != 0x1014 {
t.Fatalf("PC-relatives Ziel wurde nicht korrekt aufgelöst: %+v", inst.Metadata.Operands[0].EffectiveAddress)
}
}
Expand Down
Loading
Loading