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
3 changes: 2 additions & 1 deletion MANUAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,8 @@ Instructions that jump/call across hunks or through a register jump-vector table

| Key | Action |
|-----|--------|
| `v` | Toggle range selection (`Esc` cancels active selection) |
| `v` | Toggle range selection (`Esc` cancels active selection); when code instructions are selected, the status bar displays aggregate byte size, instruction count, and MC68000 execution cycles + `(R/W)` bus words |
| `V` | Select (or cancel selection of) the current subroutine and display its aggregate byte size, instruction count, and MC68000 execution cycles + `(R/W)` bus words in the status bar |
| `c` | Mark cursor (or selection) as `CODE` (`confidence = 100`) and run incremental analysis |
| `b` | Mark as `DATA_B` (1 byte) |
| `w` | Mark as `DATA_W` (2 bytes) |
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Amiga M68k Reverse Engineering System — terminal-native, keyboard-first disass
- **Source-linkable `; XREF:` comments** (`; XREF: hunk5:LAB_5_24E84 (jsr 498(a5))`) and automatic cross-hunk / jump-vector target hints (`; -> hunk3:LAB_3_26432`)
- **Static Call Graph CLI (`hunks -m <module> cg`)** with automatic `d16(a5)` jump-vector resolution, bottom-up `--ready` worklists, `--leaves`, `--anchors` (strings & `$DFFxxx` hardware), `--suspect` audits, and `--code` caller/callee inspection
- **Standalone Amiga music module scanner (`hunks rip <file>`, experimental)** with 26 ExoticRipper recognizers (including sandboxed self-executing M68000 driver probes for David Whittaker and Final Music System)
- **Interactive TUI (`hunks -m <module> tui`)** with full-width Sub-Row Code View, inline 16-byte `dc.b` hex/ASCII grid, 3-tier EOL comment priority, exact MC68000 instruction cycle & R/W bus timing, interactive **Amiga Bitplane Image Viewer & Ripper (`B`)** (Kitty/iTerm2/Sixel/Half-Block pixel graphics, `BPLCON0` 1–7 planes, EHB/HAM6, Copper/`$0RGB` palette extraction, PNG & `FORM ILBM` export), experimental **TUI Music Ripper (`M`)** (scan, jump, rip, & carve), virtual segment `incbin` carves (`v` + `K` in editor, `K` in asset viewers with metadata block comments and round-trip on `B`), configurable truecolor themes (`~/.config/hunks/theme.json`), vertical scrollbar, floating modal overlays, byte-exact cursor splitting, cross-hunk `Enter` navigation, `x` XREFs modal, `f` Functions filter, and struct/alias editors
- **Interactive TUI (`hunks -m <module> tui`)** with full-width Sub-Row Code View, inline 16-byte `dc.b` hex/ASCII grid, 3-tier EOL comment priority, exact MC68000 single-instruction, block-selection (`v`), and subroutine (`V`) cycle & R/W bus timing, interactive **Amiga Bitplane Image Viewer & Ripper (`B`)** (Kitty/iTerm2/Sixel/Half-Block pixel graphics, `BPLCON0` 1–7 planes, EHB/HAM6, Copper/`$0RGB` palette extraction, PNG & `FORM ILBM` export), experimental **TUI Music Ripper (`M`)** (scan, jump, rip, & carve), virtual segment `incbin` carves (`v` + `K` in editor, `K` in asset viewers with metadata block comments and round-trip on `B`), configurable truecolor themes (`~/.config/hunks/theme.json`), vertical scrollbar, floating modal overlays, byte-exact cursor splitting, cross-hunk `Enter` navigation, `x` XREFs modal, `f` Functions filter, and struct/alias editors
- **100% bit-for-bit reassembly** verified with `vasmm68k_mot`

## Quick Start
Expand Down
35 changes: 18 additions & 17 deletions internal/db/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -1135,24 +1135,25 @@ func (c *Controller) FunctionLabelAddrs() []uint32 {
return out
}
sql1 := fmt.Sprintf(
`SELECT DISTINCT l.addr FROM labels l JOIN cells c ON c.seg = l.seg AND c.addr = l.addr
WHERE c.seg IN (%s) AND c.type = 'CODE' AND (
l.is_exported = 1
OR EXISTS (SELECT 1 FROM edges e WHERE e.seg IN (%s) AND e.target_addr = l.addr AND e.type = 'CALL')
OR EXISTS (SELECT 1 FROM xrefs x WHERE x.seg IN (%s) AND x.target_addr = l.addr)
) ORDER BY l.addr ASC`,
inClause, inClause, inEmpty,
`SELECT DISTINCT addr FROM (
SELECT l.addr AS addr FROM labels l JOIN cells c ON c.seg = l.seg AND c.addr = l.addr
WHERE c.seg IN (%s) AND c.type = 'CODE' AND (
l.kind = 'SUB'
OR l.is_exported = 1
OR EXISTS (SELECT 1 FROM edges e WHERE e.seg IN (%s) AND e.target_addr = l.addr AND e.type = 'CALL')
OR (
EXISTS (SELECT 1 FROM xrefs x WHERE x.seg IN (%s) AND x.target_addr = l.addr AND x.type != 'CODE_BRANCH')
AND NOT EXISTS (SELECT 1 FROM xrefs xb WHERE xb.seg IN (%s) AND xb.target_addr = l.addr AND xb.type = 'CODE_BRANCH')
)
)
UNION
SELECT f.entry_addr AS addr FROM functions f JOIN cells c ON c.seg = f.seg AND c.addr = f.entry_addr
WHERE c.seg IN (%s) AND c.type = 'CODE'
AND NOT EXISTS (SELECT 1 FROM xrefs xb WHERE xb.seg IN (%s) AND xb.target_addr = f.entry_addr AND xb.type = 'CODE_BRANCH')
) ORDER BY addr ASC`,
inClause, inClause, inEmpty, inEmpty, inClause, inEmpty,
)
out := queryInto(sql1)
if len(out) == 0 {
sql2 := fmt.Sprintf(
`SELECT DISTINCT l.addr FROM labels l JOIN cells c ON c.seg = l.seg AND c.addr = l.addr
WHERE c.seg IN (%s) AND c.type = 'CODE' ORDER BY l.addr ASC`,
inClause,
)
out = queryInto(sql2)
}
return out
return queryInto(sql1)
}

func (c *Controller) searchBoundsAndClause(rng *AddrRange, col string, includeEmpty bool) (uint32, uint32, string) {
Expand Down
201 changes: 200 additions & 1 deletion internal/tui/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ package tui

import (
"fmt"
"maps"
"os"
"slices"
"strconv"
"strings"

tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/x/term"
Expand Down Expand Up @@ -247,6 +251,197 @@ func (a *App) ToggleSelection() {
}
}

// SelectSubroutine selects (or cancels selection of) the subroutine enclosing the cursor.
func (a *App) SelectSubroutine() {
funcStart, lastInsnAddr, ok := a.subroutineBoundsAtCursor()
if !ok {
a.StatusMsg = "No subroutine at cursor"
return
}
if selStart, selEnd, hasSel := a.GetSelectionRange(); hasSel && selStart == funcStart && selEnd == lastInsnAddr {
a.SelectionStart = nil
a.StatusMsg = "Selection cancelled"
return
}
a.SelectionStart = &funcStart
a.GotoAddress(lastInsnAddr)
}

func isSubroutineTerminator(mnem string) bool {
op, _ := m68k.ParseMnemonic(mnem)
switch op {
case m68k.OpRTS, m68k.OpRTE, m68k.OpRTR, m68k.OpJMP, m68k.OpBRA, m68k.OpILLEGAL:
return true
}
return false
}

func branchTargetAddr(row *ListingRow, curAddr uint32) (uint32, bool) {
if len(row.RawBytes) >= 2 {
if insn, ok := m68k.Decode(row.RawBytes, curAddr); ok {
if insn.IsBranch() && !insn.IsCall() {
for _, op := range insn.Operands() {
if op.Mode == m68k.EABranch || op.Mode == m68k.EAPCDisp16 {
return op.UVal, true
}
}
if row.RefTarget != nil {
return *row.RefTarget, true
}
}
return 0, false
}
}
op, _ := m68k.ParseMnemonic(row.Mnem)
if (op == m68k.OpBcc || op == m68k.OpDBcc || op == m68k.OpBRA) && row.RefTarget != nil {
return *row.RefTarget, true
}
return 0, false
}

func (a *App) subroutineBoundsAtCursor() (uint32, uint32, bool) {
cur, ok := a.CursorAddr()
if !ok {
return 0, 0, false
}

var codeIdxs []int
curCodePos := -1
for i := range a.DisasmRows {
r := &a.DisasmRows[i]
if r.Kind == SubRowCode && r.Addr != nil {
if *r.Addr == cur && curCodePos < 0 {
curCodePos = len(codeIdxs)
}
codeIdxs = append(codeIdxs, i)
}
}
if len(codeIdxs) == 0 || curCodePos < 0 {
return 0, 0, false
}

funcSet := make(map[uint32]struct{})
if a.DB != nil {
for _, fa := range a.DB.FunctionLabelAddrs() {
funcSet[fa] = struct{}{}
}
}
if a.Proj != nil {
for _, ep := range a.Proj.Data.EntryPoints {
if ep.Segment == "" || strings.EqualFold(ep.Segment, a.Proj.ActiveSegmentName) {
funcSet[ep.Addr] = struct{}{}
}
}
}
funcAddrs := slices.Sorted(maps.Keys(funcSet))

subStartPos := 0
var maxBranchTarget uint32
for pos := 0; pos < len(codeIdxs); pos++ {
curRow := &a.DisasmRows[codeIdxs[pos]]
curAddr := *curRow.Addr
curSize := max(curRow.Size, 2)
nextAddr := curAddr + curSize

if brTgt, isBr := branchTargetAddr(curRow, curAddr); isBr && brTgt > curAddr {
idx, _ := slices.BinarySearch(funcAddrs, curAddr+1)
crossesFunc := idx < len(funcAddrs) && funcAddrs[idx] <= brTgt
if !crossesFunc && brTgt > maxBranchTarget {
maxBranchTarget = brTgt
}
}

isEnd := false
if pos+1 >= len(codeIdxs) {
isEnd = true
} else {
nextRow := &a.DisasmRows[codeIdxs[pos+1]]
if *nextRow.Addr != nextAddr {
isEnd = true
} else if _, isFunc := funcSet[*nextRow.Addr]; isFunc {
isEnd = true
} else if isSubroutineTerminator(curRow.Mnem) && maxBranchTarget < nextAddr {
isEnd = true
}
}

if isEnd {
if pos >= curCodePos {
startAddr := *a.DisasmRows[codeIdxs[subStartPos]].Addr
endAddr := *a.DisasmRows[codeIdxs[pos]].Addr
return startAddr, endAddr, true
}
subStartPos = pos + 1
maxBranchTarget = 0
}
}

return 0, 0, false
}

func formatRangeUint32(lo, hi uint32) string {
if lo == hi {
return strconv.FormatUint(uint64(lo), 10)
}
return strconv.FormatUint(uint64(lo), 10) + "-" + strconv.FormatUint(uint64(hi), 10)
}

func formatBlockTiming(minCyc, maxCyc, minR, maxR, minW, maxW uint32) string {
if maxCyc == 0 {
return "-- cyc"
}
return fmt.Sprintf("%s (%s/%s) cyc",
formatRangeUint32(minCyc, maxCyc),
formatRangeUint32(minR, maxR),
formatRangeUint32(minW, maxW),
)
}

func (a *App) selectionBlockStats(start, end uint32) (size uint32, insns int, timingStr string) {
maxEndByte := end
var minCyc, maxCyc uint32
var minR, maxR uint32
var minW, maxW uint32

for i := range a.DisasmRows {
row := &a.DisasmRows[i]
if row.Kind != SubRowCode || row.Addr == nil {
continue
}
rStart := *row.Addr
rEnd := rStart + max(row.Size, 1) - 1
if rStart > end || rEnd < start {
continue
}
if rEnd > maxEndByte {
maxEndByte = rEnd
}
insns++
t := row.Timing
switch t.Kind {
case m68k.TimingBranch, m68k.TimingRange:
minCyc += uint32(min(t.Primary.Cycles, t.Secondary.Cycles))
maxCyc += uint32(max(t.Primary.Cycles, t.Secondary.Cycles))
minR += uint32(min(t.Primary.Reads, t.Secondary.Reads))
maxR += uint32(max(t.Primary.Reads, t.Secondary.Reads))
minW += uint32(min(t.Primary.Writes, t.Secondary.Writes))
maxW += uint32(max(t.Primary.Writes, t.Secondary.Writes))
default:
minCyc += uint32(t.Primary.Cycles)
maxCyc += uint32(t.Primary.Cycles)
minR += uint32(t.Primary.Reads)
maxR += uint32(t.Primary.Reads)
minW += uint32(t.Primary.Writes)
maxW += uint32(t.Primary.Writes)
}
}
size = maxEndByte - start + 1
if insns > 0 {
timingStr = formatBlockTiming(minCyc, maxCyc, minR, maxR, minW, maxW)
}
return size, insns, timingStr
}

// GetSelectionRange returns the ordered inclusive (start, end) selection range if active.
func (a *App) GetSelectionRange() (uint32, uint32, bool) {
if a.SelectionStart == nil {
Expand Down Expand Up @@ -297,7 +492,11 @@ func (a *App) GetStatusLine() string {
return " No selection"
}
if start, end, selOk := a.GetSelectionRange(); selOk {
size := end - start + 1
size, insns, timingStr := a.selectionBlockStats(start, end)
if insns > 0 {
return fmt.Sprintf(" SELECT: %s–%s (%dB, %d insns) │ %s │ c/b/w/l/s/p/K to apply, Esc cancel",
a.FormatAddr(start), a.FormatAddr(end), size, insns, timingStr)
}
return fmt.Sprintf(" SELECT: %s–%s (%dB) │ c/b/w/l/s/p/K to apply, Esc cancel",
a.FormatAddr(start), a.FormatAddr(end), size)
}
Expand Down
2 changes: 2 additions & 0 deletions internal/tui/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,8 @@ func (a *App) HandleKey(key string) bool {
}
case "v":
a.ToggleSelection()
case "V":
a.SelectSubroutine()
case "esc":
if a.SelectionStart != nil {
a.SelectionStart = nil
Expand Down
2 changes: 1 addition & 1 deletion internal/tui/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -992,7 +992,7 @@ func renderModal(buf *screenBuffer, a *App, screen rect) {
" # Cycle format (HEX/DEC/BIN)",
"",
"Type Marking:",
" c Code b Byte w Word l Long v Select",
" c Code b Byte w Word l Long v/V Select/Sub",
" s String p Pointer P Ptr table u Undefine",
" K Carve selection->incbin (on carve: uncarve)",
"",
Expand Down
Loading
Loading