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.
- 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 —BFxxxbitfield ops,CAS,CHK2/CMP2, 32×32MULU.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/FTSTfamily in all 7 data formats (including packed BCD with k-factor),FMOVEM(both theFPnandFPCR/FPSR/FPIARregister-list forms), the transcendental and math-extension function sets,FMOVECR,FSINCOS, theFBcc/FDBcc/FScc/FTRAPcccondition family, andFSAVE/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, thePBcc/PDBcc/PScc/PTRAPcccondition family, and the 68040's own simplified single-word PMMU forms. - Optional auto-generated labels for branch/call targets within a disassembled range (
BRA l00001010plus a matchingInstruction.Label), with a caller-suppliedSymbolizername 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.
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.
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).
go get github.com/jenska/m68kdasmSingle-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)
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=[]
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) // 0Useful 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.
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 $00001234Using 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 $000AYou 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) // $00001234This 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) // 4114DecodeOptions.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: RTSLabels 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.
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.
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.
make build
make test
make fmtTests include assembler round trips, decoder dispatch parity, streaming decode coverage, metadata checks, symbolized rendering, and partial-error behavior.
The decoder uses a two-level dispatch mechanism:
- A top-level jump table partitions the opcode space by high nibble.
- 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/MMUcapability 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.
This project is licensed under the MIT License. See LICENSE for details.