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
117 changes: 76 additions & 41 deletions GhidraNes/src/main/java/ghidranes/GhidraNesLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@
import java.util.List;

import ghidra.app.util.Option;
import ghidra.app.util.OptionUtils;
import ghidra.app.util.bin.ByteProvider;
import ghidra.app.util.importer.MessageLog;
import ghidra.app.util.opinion.AbstractLibrarySupportLoader;
import ghidra.app.util.opinion.AbstractProgramWrapperLoader;
import ghidra.app.util.opinion.LoadSpec;
import ghidra.framework.model.DomainObject;
import ghidra.framework.store.LockException;
Expand All @@ -37,19 +38,30 @@
import ghidra.util.exception.DuplicateNameException;
import ghidra.util.exception.InvalidInputException;
import ghidra.util.task.TaskMonitor;
import ghidranes.errors.InvalidNesRomHeaderException;
import ghidranes.errors.NesRomException;
import ghidranes.errors.UnimplementedNesMapperException;
import ghidranes.mappers.NesMapper;
import ghidranes.util.MemoryBlockDescription;

/**
* This loader parses an iNES ROM file and maps the PRG and CHR rom appropriately
*/
public class GhidraNesLoader extends AbstractLibrarySupportLoader {
public class GhidraNesLoader extends AbstractProgramWrapperLoader {

public static final String LOADER_NAME = "NES ROM";

private static final String OPTION_NAME_MIRROR = "Create mirror blocks for RAM and IO";
private static final Boolean OPTION_DEFAULT_MIRROR = true;

// in theory you could (should?)"rebuild" the rom object in the methods that need
// it from the ByteProvider instead of using a local variable here
protected NesRom rom;

private Boolean wantMirrors = OPTION_DEFAULT_MIRROR;

@Override
public String getName() {
return "NES ROM";
return LOADER_NAME;
}

@Override
Expand All @@ -60,7 +72,7 @@ public Collection<LoadSpec> findSupportedLoadSpecs(ByteProvider provider) throws

try {
// Try to parse the ROM header (will throw an exception if parsing fails)
new NesRomHeader(bytes);
rom = new NesRom(bytes);

// If successful, add the load spec
LanguageCompilerSpecPair languageCompilerSpecPair = new LanguageCompilerSpecPair("6502:LE:16:default", "default");
Expand All @@ -70,29 +82,39 @@ public Collection<LoadSpec> findSupportedLoadSpecs(ByteProvider provider) throws
catch (NesRomException e) {
// If parsing failed, do not add the load spec
}

return loadSpecs;
}

@Override
protected void load(ByteProvider provider, LoadSpec loadSpec, List<Option> options,
Program program, TaskMonitor monitor, MessageLog log)
throws CancelledException, IOException {
InputStream bytes = provider.getInputStream(0);

// have to stash a local copy of the mirror option cause
// createDefaultMemoryBlocks() doesn't have access the load options
wantMirrors = shouldCreateMirrors(options);

InputStream bytes = provider.getInputStream(0);
NesRom rom;

try {
NesRomHeader header = new NesRomHeader(bytes);
rom = new NesRom(header, bytes);
rom = new NesRom(bytes);
} catch (NesRomException e) {
throw new RuntimeException(e);
}
NesMapper mapper;

try {
mapper = NesMapper.getMapper(rom.header.mapper);
mapper.apply(rom, program, monitor);
// create base ROM bank to overlay banks
int romPermissions =
MemoryBlockDescription.READ | MemoryBlockDescription.EXECUTE;
MemoryBlockDescription.uninitialized(0x8000, 0x8000, "PRG_ROM", romPermissions, false)
.create(program);

// map the banks
rom.applyMapper(program, monitor, options);
} catch (LockException | MemoryConflictException | AddressOverflowException
| DuplicateNameException | InvalidInputException | UnimplementedNesMapperException e) {
| DuplicateNameException | InvalidInputException | UnimplementedNesMapperException
| InvalidNesRomHeaderException e) {
throw new RuntimeException(e);
}
}
Expand All @@ -103,6 +125,12 @@ public List<Option> getDefaultOptions(ByteProvider provider, LoadSpec loadSpec,
List<Option> list =
super.getDefaultOptions(provider, loadSpec, domainObject, isLoadIntoProgram);

// general options
list.add(new Option(OPTION_NAME_MIRROR, OPTION_DEFAULT_MIRROR));

// identify ROM-specific options based on NES header info
list.addAll(rom.getLoadOptions());

return list;
}

Expand All @@ -116,6 +144,7 @@ protected void createDefaultMemoryBlocks(Program program, Language language, Mes
// NOTE: We skip the default memory blocks because Ghidra's default 6502 memory map
// differs from the NES's memory map
// super.createDefaultMemoryBlocks(program, language, log);
// TODO: consider creating an NES "language" with the correct memory map

try {
int ramPermissions =
Expand All @@ -128,36 +157,38 @@ protected void createDefaultMemoryBlocks(Program program, Language language, Mes
// TODO: Refactor mirrored sections!
MemoryBlockDescription.uninitialized(0x0000, 0x0800, "RAM", ramPermissions, false)
.create(program);
MemoryBlockDescription.byteMapped(0x0800, 0x0800, "RAM_MIRROR_1", ramPermissions, 0x0000)
.create(program);
MemoryBlockDescription.byteMapped(0x1000, 0x0800, "RAM_MIRROR_2", ramPermissions, 0x0000)
.create(program);
MemoryBlockDescription.byteMapped(0x1800, 0x0800, "RAM_MIRROR_3", ramPermissions, 0x0000)
.create(program);

if (wantMirrors) {
MemoryBlockDescription.byteMapped(0x0800, 0x0800, "RAM_MIRROR_1", ramPermissions, 0x0000)
.create(program);
MemoryBlockDescription.byteMapped(0x1000, 0x0800, "RAM_MIRROR_2", ramPermissions, 0x0000)
.create(program);
MemoryBlockDescription.byteMapped(0x1800, 0x0800, "RAM_MIRROR_3", ramPermissions, 0x0000)
.create(program);
}
MemoryBlockDescription.uninitialized(0x2000, 0x0008, "PPU", ppuPermissions, false)
.create(program);
MemoryBlockDescription.byteMapped(0x2008, 0x0008, "PPU_MIRROR_1", ppuPermissions, 0x2000)
.create(program);
MemoryBlockDescription.byteMapped(0x2010, 0x0010, "PPU_MIRROR_2", ppuPermissions, 0x2000)
.create(program);
MemoryBlockDescription.byteMapped(0x2020, 0x0020, "PPU_MIRROR_3", ppuPermissions, 0x2000)
.create(program);
MemoryBlockDescription.byteMapped(0x2040, 0x0040, "PPU_MIRROR_4", ppuPermissions, 0x2000)
.create(program);
MemoryBlockDescription.byteMapped(0x2080, 0x0080, "PPU_MIRROR_5", ppuPermissions, 0x2000)
.create(program);
MemoryBlockDescription.byteMapped(0x2100, 0x0100, "PPU_MIRROR_6", ppuPermissions, 0x2000)
.create(program);
MemoryBlockDescription.byteMapped(0x2200, 0x0200, "PPU_MIRROR_7", ppuPermissions, 0x2000)
.create(program);
MemoryBlockDescription.byteMapped(0x2400, 0x0400, "PPU_MIRROR_8", ppuPermissions, 0x2000)
.create(program);
MemoryBlockDescription.byteMapped(0x2800, 0x0800, "PPU_MIRROR_9", ppuPermissions, 0x2000)
.create(program);
MemoryBlockDescription.byteMapped(0x3000, 0x1000, "PPU_MIRROR_10", ppuPermissions, 0x2000)
.create(program);

if (wantMirrors) {
MemoryBlockDescription.byteMapped(0x2008, 0x0008, "PPU_MIRROR_1", ppuPermissions, 0x2000)
.create(program);
MemoryBlockDescription.byteMapped(0x2010, 0x0010, "PPU_MIRROR_2", ppuPermissions, 0x2000)
.create(program);
MemoryBlockDescription.byteMapped(0x2020, 0x0020, "PPU_MIRROR_3", ppuPermissions, 0x2000)
.create(program);
MemoryBlockDescription.byteMapped(0x2040, 0x0040, "PPU_MIRROR_4", ppuPermissions, 0x2000)
.create(program);
MemoryBlockDescription.byteMapped(0x2080, 0x0080, "PPU_MIRROR_5", ppuPermissions, 0x2000)
.create(program);
MemoryBlockDescription.byteMapped(0x2100, 0x0100, "PPU_MIRROR_6", ppuPermissions, 0x2000)
.create(program);
MemoryBlockDescription.byteMapped(0x2200, 0x0200, "PPU_MIRROR_7", ppuPermissions, 0x2000)
.create(program);
MemoryBlockDescription.byteMapped(0x2400, 0x0400, "PPU_MIRROR_8", ppuPermissions, 0x2000)
.create(program);
MemoryBlockDescription.byteMapped(0x2800, 0x0800, "PPU_MIRROR_9", ppuPermissions, 0x2000)
.create(program);
MemoryBlockDescription.byteMapped(0x3000, 0x1000, "PPU_MIRROR_10", ppuPermissions, 0x2000)
.create(program);
}
MemoryBlockDescription.uninitialized(0x4000, 0x0018, "APU_IO", apuIoPermissions, false)
.create(program);
} catch (LockException e) {
Expand All @@ -172,4 +203,8 @@ protected void createDefaultMemoryBlocks(Program program, Language language, Mes
throw new RuntimeException(e);
}
}

protected Boolean shouldCreateMirrors(List<Option> options) {
return OptionUtils.getBooleanOptionValue(OPTION_NAME_MIRROR, options, OPTION_DEFAULT_MIRROR);
}
}
129 changes: 116 additions & 13 deletions GhidraNes/src/main/java/ghidranes/NesRom.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,140 @@

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

import ghidra.app.util.Option;
import ghidra.framework.store.LockException;
import ghidra.program.model.address.AddressOverflowException;
import ghidra.program.model.address.AddressSpace;
import ghidra.program.model.listing.Program;
import ghidra.program.model.mem.MemoryConflictException;
import ghidra.program.model.symbol.SourceType;
import ghidra.program.model.symbol.Symbol;
import ghidra.program.model.symbol.SymbolTable;
import ghidra.util.Msg;
import ghidra.util.exception.CancelledException;
import ghidra.util.exception.DuplicateNameException;
import ghidra.util.exception.InvalidInputException;
import ghidra.util.task.TaskMonitor;
import ghidranes.errors.InvalidNesRomHeaderException;
import ghidranes.errors.NesRomEofException;
import ghidranes.errors.UnimplementedNesMapperException;
import ghidranes.mappers.NesMapper;
import ghidranes.util.Bank;
import ghidranes.util.BankAddressOption;
import ghidranes.util.ChrBankOption;
import ghidranes.util.NesMmio;

public class NesRom {
private static final int TRAINER_SIZE = 512;
private static final String CHR_BANK_OPTION_NAME = "CHR ROM handling";

public NesRomHeader header;
byte[] trainerBytes;
public byte[] trainer;
public byte[] prgRom;
public byte[] chrRom;

public NesRom(NesRomHeader romHeader, InputStream bytes) throws NesRomEofException, IOException {
if (romHeader.hasTrainer) {
trainerBytes = bytes.readNBytes(512);
if (trainerBytes.length < 512) {
private int prgRomSize;
private int prgBankSize;
private int prgBankCount;

private int chrRomSize;
private List<Integer> chrBankSizes;

public NesRom(InputStream bytes)
throws NesRomEofException, IOException, InvalidNesRomHeaderException, UnimplementedNesMapperException {

header = new NesRomHeader(bytes);
if (header.hasTrainer) {
trainer = bytes.readNBytes(TRAINER_SIZE);
if (trainer.length < TRAINER_SIZE) {
throw new NesRomEofException();
}
}
else {
trainerBytes = new byte[0];
trainer = null;
}

byte[] prgRomBytes = bytes.readNBytes(romHeader.prgRomSizeBytes);
if (prgRomBytes.length < romHeader.prgRomSizeBytes) {
prgRom = bytes.readNBytes(header.prgRomSizeBytes);
if (prgRom.length < header.prgRomSizeBytes) {
throw new NesRomEofException();
}

byte[] chrRomBytes = bytes.readNBytes(romHeader.chrRomSizeBytes);
if (chrRomBytes.length < romHeader.chrRomSizeBytes) {
chrRom = bytes.readNBytes(header.chrRomSizeBytes);
if (chrRom.length < header.chrRomSizeBytes) {
throw new NesRomEofException();
}
prgRomSize = header.getPrgRomSizeBytes();
prgBankSize = NesMapper.getPrgBankSize(header.mapper, prgRomSize);
prgBankCount = prgRomSize / prgBankSize;
Msg.info("NesRom", "PRG ROM size: " + prgRomSize + " bytes, " + prgBankCount + " banks of size " + prgBankSize + " bytes");
chrRomSize = header.getChrRomSizeBytes();
chrBankSizes = NesMapper.getChrBankSize(header.mapper);
Msg.info("NesRom", "CHR ROM size: " + chrRomSize + " bytes, possible bank sizes: " + chrBankSizes);
}

public List<Option> getLoadOptions() {
List<Option> list = new ArrayList<>();

// skip bank options if there is only one bank or bank size is 32k
// TODO: this might actually belong in the mapper class
// TODO: use fancy heuristics to guess the right base address for each bank
if (prgBankCount > 1 && prgBankSize < 0x8000) {
try {
for (int i = 0; i < prgBankCount; i++) {
String bankName = Bank.getPrgBankName(i, prgBankCount);
int defaultAddress = 0x8000;
if (i >= prgBankCount - 1) {
// if this is the last bank, default to the end of the PRG ROM
defaultAddress = 0x10000 - prgBankSize;
}
list.add(new BankAddressOption(bankName, "PRG bank base addresses", prgBankSize, 0x8000, defaultAddress));
}

} catch (Exception e) {
// ignore errors - no extra options in this case
}
}

if (chrRomSize > 0) {
// add CHR ROM bank options
list.add(new ChrBankOption(CHR_BANK_OPTION_NAME, chrBankSizes));
}

return list;
}

public void applyMapper(Program program, TaskMonitor monitor, List<Option> options)
throws LockException, MemoryConflictException, AddressOverflowException,
CancelledException, DuplicateNameException, InvalidInputException, UnimplementedNesMapperException, InvalidNesRomHeaderException
{
NesMapper mapper = NesMapper.getMapper(header.mapper);
mapper.setPrgBankAddresses(options);
mapper.setPrgBankCount(prgBankCount);
mapper.mapPrgRom(this, program, monitor);
mapper.mapChrRom(this, program, monitor, options);
mapMMIO(program, monitor, mapper);
mapper.mapVectors(program, monitor);
}

protected void mapMMIO(Program program, TaskMonitor monitor, NesMapper mapper)
throws InvalidInputException
/*throws LockException, MemoryConflictException, AddressOverflowException,
CancelledException, DuplicateNameException */ {

// TODO: mapper MMIO often overlaps PRG ROM - should add a label in
// each overlay address space so it gets applied consistently
AddressSpace addressSpace = program.getAddressFactory().getDefaultAddressSpace();
SymbolTable symbolTable = program.getSymbolTable();

this.header = romHeader;
this.prgRom = prgRomBytes;
this.chrRom = chrRomBytes;
ArrayList<NesMmio> registers = NesMmio.getDefaultRegisters(addressSpace);
registers.addAll(mapper.getMapperRegisters(addressSpace));
for (NesMmio register : registers) {
Symbol s = symbolTable.createLabel(register.address, register.name, SourceType.IMPORTED);
s.setPinned(true);
}
}

}
14 changes: 7 additions & 7 deletions GhidraNes/src/main/java/ghidranes/NesRomHeader.java
Original file line number Diff line number Diff line change
Expand Up @@ -106,27 +106,27 @@ public NesRomHeader(InputStream bytes) throws NesRomEofException, InvalidNesRomH
}
}

int getPrgRomSizeBytes() {
return this.prgRamSizeBytes;
public int getPrgRomSizeBytes() {
return this.prgRomSizeBytes;
}

int getChrRomSizeBytes() {
public int getChrRomSizeBytes() {
return this.chrRomSizeBytes;
}

int getPrgRamSizeBytes() {
public int getPrgRamSizeBytes() {
return this.prgRamSizeBytes;
}

boolean getHasPersistence() {
public boolean getHasPersistence() {
return this.hasPersistence;
}

boolean getHasTrainer() {
public boolean getHasTrainer() {
return this.hasTrainer;
}

int getMapper() {
public int getMapper() {
return this.mapper;
}
}
Loading