Skip to content

Commit a99c77d

Browse files
committed
add -base flag for custom base address in binary mode
1 parent 6a5ed70 commit a99c77d

File tree

11 files changed

+264
-71
lines changed

11 files changed

+264
-71
lines changed

README.md

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -105,45 +105,47 @@ usage: retrodisasm [options] [file]
105105
106106
Parameters:
107107
-i string
108-
input ROM file
108+
input ROM file
109109
-o string
110-
output .asm file (default: <input>.asm, use - for stdout)
110+
output .asm file (default: <input>.asm, use - for stdout)
111111
-c string
112-
ca65 linker config file
112+
ca65 linker config file
113113
-cdl string
114-
Code/Data log file (.cdl)
114+
Code/Data log file (.cdl)
115115
-batch string
116-
batch process files matching pattern (e.g. *.nes)
116+
batch process files matching pattern (e.g. *.nes)
117117
118118
Options:
119119
-a string
120-
assembler format: asm6, ca65, nesasm, retroasm (default: ca65)
120+
assembler format: asm6, ca65, nesasm, retroasm (default: ca65)
121121
-s string
122-
target system: nes, chip8 (default: auto-detect)
122+
target system: nes, chip8 (default: auto-detect)
123123
-binary
124-
treat input as raw binary without header
124+
treat input as raw binary without header
125+
-base string
126+
base address for -binary mode in hex (e.g. 0200, 8000)
125127
-verify
126-
verify output by reassembling and comparing to input
128+
verify output by reassembling and comparing to input
127129
-debug
128-
enable debug logging
130+
enable debug logging
129131
-q
130-
quiet mode
132+
quiet mode
131133
132134
Output options:
133135
-nohexcomments
134-
omit hex opcode bytes in comments
136+
omit hex opcode bytes in comments
135137
-nooffsets
136-
omit file offsets in comments
138+
omit file offsets in comments
137139
-output-unofficial
138-
use mnemonics for unofficial opcodes (incompatible with -verify)
140+
use mnemonics for unofficial opcodes (incompatible with -verify)
139141
-stop-at-unofficial
140-
stop tracing at unofficial opcodes unless branched to
142+
stop tracing at unofficial opcodes unless branched to
141143
-z
142-
include trailing zero bytes in banks
144+
include trailing zero bytes in banks
143145
144146
Positional arguments:
145147
file
146-
file to disassemble
148+
file to disassemble
147149
```
148150

149151
### System-Specific Options

internal/arch/m6502/m6502.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@ type Arch6502 struct {
7575
consts *consts.Consts
7676
codeBaseAddress uint16
7777
complementaryBranchPairs []ComplementaryBranchPair
78-
complementaryBranchSecondAddrs set.Set[uint16] // addresses of second branches in complementary pairs
78+
complementaryBranchSecondAddrs set.Set[uint16] // addresses of second branches in complementary pairs
79+
options options.Disassembler // Stored options for early access (before dependency injection)
7980
}
8081

8182
// InjectDependencies sets the required dependencies for this architecture.
@@ -87,6 +88,12 @@ func (ar *Arch6502) InjectDependencies(deps Dependencies) {
8788
ar.consts = deps.Consts
8889
}
8990

91+
// SetOptions sets disassembler options before dependency injection.
92+
// This is needed because BankWindowSize is called before InjectDependencies.
93+
func (ar *Arch6502) SetOptions(opts options.Disassembler) {
94+
ar.options = opts
95+
}
96+
9097
// SetCodeBaseAddress sets the code base address for this architecture.
9198
func (ar *Arch6502) SetCodeBaseAddress(address uint16) {
9299
ar.codeBaseAddress = address
@@ -172,6 +179,11 @@ func (ar *Arch6502) PostProcessCode() error {
172179
}
173180

174181
// BankWindowSize returns the bank window size.
182+
// Returns 0 for binary mode to use single-bank mode (no bank windowing).
175183
func (ar *Arch6502) BankWindowSize(_ *cartridge.Cartridge) int {
176-
return 0x2000 // TODO calculate dynamically
184+
// Debug: check if options.Binary is set
185+
if ar.options.Binary {
186+
return 0 // Single-bank mode for binary files
187+
}
188+
return 0x2000 // Multi-bank mode for NES ROMs
177189
}

internal/arch/m6502/params.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,9 +163,16 @@ func paramReadWord(dis disasm, address uint16) (uint16, []byte, error) {
163163
}
164164

165165
// ReadMemory reads a byte from memory using NES-specific memory mapping.
166+
// In binary mode, all reads go through the mapper directly.
166167
func (ar *Arch6502) ReadMemory(address uint16) (byte, error) {
167168
var value byte
168169

170+
// In binary mode, always use mapper (no NES-specific memory mapping)
171+
if ar.options.Binary {
172+
value = ar.mapper.ReadMemory(address)
173+
return value, nil
174+
}
175+
169176
switch {
170177
case address < 0x2000:
171178
value = ar.dis.Cart().CHR[address]

internal/arch/m6502/vectors.go

Lines changed: 124 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@ package m6502
33
import (
44
"fmt"
55

6+
"github.com/retroenv/retrodisasm/internal/options"
67
"github.com/retroenv/retrodisasm/internal/program"
78
"github.com/retroenv/retrogolib/arch/cpu/m6502"
89
"github.com/retroenv/retrogolib/arch/system/nes"
910
"github.com/retroenv/retrogolib/log"
1011
)
1112

13+
const resetLabel = "Reset"
14+
1215
func (ar *Arch6502) Initialize() error {
1316
if err := ar.initializeIrqHandlers(); err != nil {
1417
return fmt.Errorf("initializing IRQ handlers: %w", err)
@@ -18,80 +21,148 @@ func (ar *Arch6502) Initialize() error {
1821

1922
// initializeIrqHandlers reads the 3 IRQ handler addresses and adds them to the addresses to be
2023
// followed for execution flow. Multiple handler can point to the same address.
21-
// nolint:funlen
2224
func (ar *Arch6502) initializeIrqHandlers() error {
2325
opts := ar.dis.Options()
2426
handlers := program.Handlers{
2527
NMI: "0",
26-
Reset: "Reset",
28+
Reset: resetLabel,
2729
IRQ: "0",
2830
}
2931

32+
// In binary mode, skip reading NMI/IRQ vectors from ROM
33+
// as they don't exist in raw binary files
34+
if opts.Binary {
35+
return ar.initializeBinaryMode(opts, handlers)
36+
}
37+
38+
nmi, err := ar.initializeNMIHandler(&handlers)
39+
if err != nil {
40+
return err
41+
}
42+
43+
reset, err := ar.initializeResetHandler(&handlers)
44+
if err != nil {
45+
return err
46+
}
47+
48+
irq, err := ar.initializeIRQHandler(&handlers)
49+
if err != nil {
50+
return err
51+
}
52+
53+
ar.resolveSharedHandlers(&handlers, nmi, reset, irq)
54+
ar.calculateCodeBaseAddress(reset)
55+
ar.queueHandlersToParse(nmi, reset, irq)
56+
57+
ar.dis.SetHandlers(handlers)
58+
return nil
59+
}
60+
61+
func (ar *Arch6502) initializeNMIHandler(handlers *program.Handlers) (uint16, error) {
3062
nmi, err := ar.dis.ReadMemoryWord(m6502.NMIAddress)
3163
if err != nil {
32-
return fmt.Errorf("reading NMI address: %w", err)
64+
return 0, fmt.Errorf("reading NMI address: %w", err)
3365
}
34-
if nmi != 0 {
35-
ar.logger.Debug("NMI handler", log.Hex("address", nmi))
36-
offsetInfo := ar.mapper.OffsetInfo(nmi)
37-
if offsetInfo != nil {
38-
offsetInfo.Label = "NMI"
39-
offsetInfo.SetType(program.CallDestination)
40-
}
41-
handlers.NMI = "NMI"
66+
if nmi == 0 {
67+
return 0, nil
4268
}
4369

44-
var reset uint16
45-
if opts.Binary {
46-
reset = uint16(nes.CodeBaseAddress)
47-
} else {
48-
reset, err = ar.dis.ReadMemoryWord(m6502.ResetAddress)
49-
if err != nil {
50-
return fmt.Errorf("reading reset address: %w", err)
51-
}
70+
ar.logger.Debug("NMI handler", log.Hex("address", nmi))
71+
offsetInfo := ar.mapper.OffsetInfo(nmi)
72+
if offsetInfo != nil {
73+
offsetInfo.Label = "NMI"
74+
offsetInfo.SetType(program.CallDestination)
75+
}
76+
handlers.NMI = "NMI"
77+
return nmi, nil
78+
}
79+
80+
func (ar *Arch6502) initializeResetHandler(handlers *program.Handlers) (uint16, error) {
81+
reset, err := ar.dis.ReadMemoryWord(m6502.ResetAddress)
82+
if err != nil {
83+
return 0, fmt.Errorf("reading reset address: %w", err)
5284
}
5385

5486
ar.logger.Debug("Reset handler", log.Hex("address", reset))
5587
offsetInfo := ar.mapper.OffsetInfo(reset)
5688
if offsetInfo != nil {
5789
if offsetInfo.Label != "" {
58-
handlers.NMI = "Reset"
90+
handlers.NMI = resetLabel
5991
}
60-
offsetInfo.Label = "Reset"
92+
offsetInfo.Label = resetLabel
6193
offsetInfo.SetType(program.CallDestination)
6294
}
95+
return reset, nil
96+
}
6397

98+
func (ar *Arch6502) initializeIRQHandler(handlers *program.Handlers) (uint16, error) {
6499
irq, err := ar.dis.ReadMemoryWord(m6502.IrqAddress)
65100
if err != nil {
66-
return fmt.Errorf("reading IRQ address: %w", err)
101+
return 0, fmt.Errorf("reading IRQ address: %w", err)
67102
}
68-
if irq != 0 {
69-
ar.logger.Debug("IRQ handler", log.Hex("address", irq))
70-
offsetInfo = ar.mapper.OffsetInfo(irq)
71-
if offsetInfo != nil {
72-
if offsetInfo.Label == "" {
73-
offsetInfo.Label = "IRQ"
74-
handlers.IRQ = "IRQ"
75-
} else {
76-
handlers.IRQ = offsetInfo.Label
77-
}
78-
offsetInfo.SetType(program.CallDestination)
103+
if irq == 0 {
104+
return 0, nil
105+
}
106+
107+
ar.logger.Debug("IRQ handler", log.Hex("address", irq))
108+
offsetInfo := ar.mapper.OffsetInfo(irq)
109+
if offsetInfo != nil {
110+
if offsetInfo.Label == "" {
111+
offsetInfo.Label = "IRQ"
112+
handlers.IRQ = "IRQ"
113+
} else {
114+
handlers.IRQ = offsetInfo.Label
79115
}
116+
offsetInfo.SetType(program.CallDestination)
80117
}
118+
return irq, nil
119+
}
81120

121+
func (ar *Arch6502) resolveSharedHandlers(handlers *program.Handlers, nmi, reset, irq uint16) {
82122
if nmi == reset {
83123
handlers.NMI = handlers.Reset
84124
}
85125
if irq == reset {
86126
handlers.IRQ = handlers.Reset
87127
}
128+
}
129+
130+
func (ar *Arch6502) queueHandlersToParse(nmi, reset, irq uint16) {
131+
if nmi != 0 {
132+
ar.dis.AddAddressToParse(nmi, nmi, 0, nil, false)
133+
}
134+
if reset != 0 {
135+
ar.dis.AddAddressToParse(reset, reset, 0, nil, false)
136+
}
137+
if irq != 0 {
138+
ar.dis.AddAddressToParse(irq, irq, 0, nil, false)
139+
}
140+
}
88141

89-
ar.calculateCodeBaseAddress(reset)
142+
// initializeBinaryMode sets up handlers for binary mode disassembly.
143+
func (ar *Arch6502) initializeBinaryMode(opts options.Disassembler, handlers program.Handlers) error {
144+
// Use custom base address if specified, otherwise use default NES code base
145+
var reset uint16
146+
if opts.BaseAddress != 0 {
147+
reset = opts.BaseAddress
148+
} else {
149+
reset = uint16(nes.CodeBaseAddress)
150+
}
151+
152+
ar.logger.Debug("Binary mode reset handler", log.Hex("address", reset))
153+
154+
// Set code base address before accessing offsets
155+
ar.dis.SetCodeBaseAddress(reset)
156+
ar.dis.SetVectorsStartAddress(m6502.InterruptVectorStartAddress)
157+
158+
offsetInfo := ar.mapper.OffsetInfo(reset)
159+
if offsetInfo != nil {
160+
offsetInfo.Label = resetLabel
161+
offsetInfo.SetType(program.CallDestination)
162+
}
90163

91-
// add IRQ handlers to be parsed after the code base address has been calculated
92-
ar.dis.AddAddressToParse(nmi, nmi, 0, nil, false)
164+
// Add reset address to parse
93165
ar.dis.AddAddressToParse(reset, reset, 0, nil, false)
94-
ar.dis.AddAddressToParse(irq, irq, 0, nil, false)
95166

96167
ar.dis.SetHandlers(handlers)
97168
return nil
@@ -103,7 +174,23 @@ func (ar *Arch6502) initializeIrqHandlers() error {
103174
// address is calculated. This ensures that jsr instructions will result in the same opcode, as it
104175
// is based on the code base address.
105176
func (ar *Arch6502) calculateCodeBaseAddress(resetHandler uint16) {
177+
// Check if user specified a custom base address
178+
opts := ar.dis.Options()
179+
if opts.BaseAddress != 0 {
180+
// Use custom base address from options
181+
ar.dis.SetCodeBaseAddress(opts.BaseAddress)
182+
ar.dis.SetVectorsStartAddress(m6502.InterruptVectorStartAddress)
183+
return
184+
}
185+
106186
cart := ar.dis.Cart()
187+
if cart == nil || len(cart.PRG) == 0 {
188+
// No cart data (binary mode), use default
189+
ar.dis.SetCodeBaseAddress(0x8000)
190+
ar.dis.SetVectorsStartAddress(m6502.InterruptVectorStartAddress)
191+
return
192+
}
193+
107194
halfPrg := len(cart.PRG) % 0x8000
108195
codeBaseAddress := uint16(0x8000 + halfPrg)
109196
vectorsStartAddress := uint16(m6502.InterruptVectorStartAddress)

internal/cli/cli.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"os"
88
"slices"
9+
"strconv"
910
"strings"
1011

1112
"github.com/retroenv/retrodisasm/internal/assembler"
@@ -100,7 +101,24 @@ func createDisasmOptions(opts options.Program) options.Disassembler {
100101
disasmOptions.AssemblerSupportsUnofficial = false
101102
}
102103

104+
// Parse base address if provided
105+
if opts.BaseAddress != "" {
106+
var baseAddr uint64
107+
var err error
108+
// Try parsing with 0x prefix
109+
if strings.HasPrefix(opts.BaseAddress, "0x") || strings.HasPrefix(opts.BaseAddress, "0X") {
110+
baseAddr, err = strconv.ParseUint(opts.BaseAddress[2:], 16, 16)
111+
} else {
112+
// Parse as hex without prefix
113+
baseAddr, err = strconv.ParseUint(opts.BaseAddress, 16, 16)
114+
}
115+
if err == nil && baseAddr <= 0xFFFF {
116+
disasmOptions.BaseAddress = uint16(baseAddr)
117+
}
118+
}
119+
103120
// Apply output flag settings
121+
disasmOptions.Binary = opts.Binary
104122
disasmOptions.HexComments = !opts.NoHexComments
105123
disasmOptions.OffsetComments = !opts.NoOffsets
106124
disasmOptions.OutputUnofficialAsMnemonics = opts.OutputUnofficial

0 commit comments

Comments
 (0)