snes-disassembler
Version:
A Super Nintendo (SNES) ROM disassembler for 65816 assembly
596 lines • 24.4 kB
JavaScript
"use strict";
/**
* Enhanced SNES Disassembly Engine
*
* Implements enhanced disassembly algorithms using MCP server insights
* for improved bank-aware addressing, function detection, and pattern recognition.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.EnhancedDisassemblyEngine = void 0;
const disassembler_1 = require("./disassembler");
const mcp_integration_1 = require("./mcp-integration");
const bank_handler_1 = require("./bank-handler");
const analysis_cache_1 = require("./analysis-cache");
class EnhancedDisassemblyEngine extends disassembler_1.SNESDisassembler {
constructor(romFile, options) {
super(romFile);
this.cachedRomInfo = null;
this.options = options;
// Get cartridge info from the parent class's ROM
const romInfo = this.getRomInfo();
this.bankHandler = new bank_handler_1.BankHandler(romInfo.cartridgeInfo);
this.enhancedCache = analysis_cache_1.globalROMCache;
this.analysis = {
vectors: [],
functions: [],
dataRegions: [],
bankLayout: []
};
this.stats = {
functionsDetected: 0,
crossReferences: 0,
labelsGenerated: 0,
bankTransitions: 0
};
this.analysisOptions = {
controlFlowAnalysis: false,
functionDetection: false,
dataStructureRecognition: false,
crossReferenceGeneration: false,
gamePatternRecognition: false
};
}
setAnalysisOptions(options) {
this.analysisOptions = options;
}
// Override getRomInfo to prevent recursion during analysis
getRomInfo() {
// Use cached ROM info if available to prevent recursive calls to this method
// This avoids circular dependencies during cache initialization.
if (this.cachedRomInfo) {
return this.cachedRomInfo;
}
// Get ROM info from parent class and cache it locally to avoid using analysis cache
// Local caching is sufficient and avoids introducing circular dependencies.
this.cachedRomInfo = super.getRomInfo();
return this.cachedRomInfo;
}
analyze() {
// Prevent recursion by checking if enhanced analysis is already running
if (this.isAnalyzing) {
console.warn('Enhanced analysis already in progress, skipping to prevent recursion.');
return { functions: [], data: [] };
}
this.isAnalyzing = true;
try {
// Perform base analysis
const baseAnalysis = super.analyze();
// Trigger enhanced analysis asynchronously (don't await to match signature)
this.performEnhancedAnalysis();
return baseAnalysis;
}
finally {
this.isAnalyzing = false;
}
}
async performEnhancedAnalysis() {
// Enhanced analysis using MCP insights
if (this.options.extractVectors) {
await this.extractInterruptVectors();
}
if (this.options.bankAware) {
await this.analyzeBankLayout();
}
if (this.analysisOptions.functionDetection) {
await this.detectFunctions();
}
if (this.analysisOptions.dataStructureRecognition) {
await this.analyzeDataStructures();
}
if (this.analysisOptions.gamePatternRecognition) {
await this.recognizeGamePatterns();
}
}
async extractInterruptVectors() {
const romInfo = this.getRomInfo();
// Check cache for vectors first
const cachedVectors = this.enhancedCache.getVectors(romInfo);
if (cachedVectors) {
this.analysis.vectors = cachedVectors;
return;
}
try {
// Use MCP server to get vector information
const vectorInfo = await (0, mcp_integration_1.callMCPTool)('extract_code', {
data: Array.from(romInfo.data.slice(-0x20)), // Last 32 bytes typically contain vectors
format: 'ca65',
extractVectors: true,
bankAware: this.options.bankAware
});
if (vectorInfo && vectorInfo.vectors) {
this.analysis.vectors = vectorInfo.vectors.map((v) => ({
name: v.name || `Vector_${v.address.toString(16).toUpperCase()}`,
address: v.address,
target: v.target
}));
}
else {
// Fallback to manual vector extraction
this.extractVectorsFallback();
}
// Cache the extracted vectors
this.enhancedCache.setVectors(romInfo, this.analysis.vectors);
}
catch (error) {
console.warn('MCP vector extraction failed, using fallback method');
this.extractVectorsFallback();
// Cache fallback results too
this.enhancedCache.setVectors(romInfo, this.analysis.vectors);
}
}
extractVectorsFallback() {
const romInfo = this.getRomInfo();
const romSize = romInfo.data.length;
const vectorArea = romInfo.data.slice(romSize - 0x20);
const vectors = [
{ name: 'RESET', offset: 0x1C },
{ name: 'IRQ', offset: 0x1E },
{ name: 'NMI', offset: 0x1A },
{ name: 'BRK', offset: 0x16 },
{ name: 'COP', offset: 0x14 }
];
vectors.forEach(vector => {
if (vector.offset < vectorArea.length - 1) {
const address = vectorArea[vector.offset] | (vectorArea[vector.offset + 1] << 8);
if (address >= 0x8000) { // Valid ROM address
this.analysis.vectors.push({
name: vector.name,
address: romSize - 0x20 + vector.offset,
target: address
});
}
}
});
}
async analyzeBankLayout() {
const romInfo = this.getRomInfo();
// Check cache for bank layout first
const cachedBankLayout = this.enhancedCache.getBankLayout(romInfo);
if (cachedBankLayout) {
this.analysis.bankLayout = cachedBankLayout;
return;
}
const romSize = romInfo.data.length;
const bankSize = 0x8000; // 32KB banks for LoROM
const numBanks = Math.ceil(romSize / bankSize);
for (let bank = 0; bank < numBanks; bank++) {
const bankStart = bank * bankSize;
const bankEnd = Math.min(bankStart + bankSize, romSize);
const bankData = romInfo.data.slice(bankStart, bankEnd);
// Analyze bank content using pattern recognition
const bankType = this.classifyBankContent(bankData, bank);
this.analysis.bankLayout.push({
bank,
start: bankStart,
end: bankEnd,
type: bankType
});
}
// Cache the bank layout analysis
this.enhancedCache.setBankLayout(romInfo, this.analysis.bankLayout);
}
classifyBankContent(bankData, bankNumber) {
// Analyze the first few bytes to determine bank type
const header = bankData.slice(0, 16);
let codeBytes = 0;
let dataBytes = 0;
let emptyBytes = 0;
// Sample every 16th byte to get a rough idea of content
for (let i = 0; i < bankData.length; i += 16) {
const byte = bankData[i];
if (byte === 0x00 || byte === 0xFF) {
emptyBytes++;
}
else if (this.isLikelyInstruction(byte)) {
codeBytes++;
}
else {
dataBytes++;
}
}
const total = codeBytes + dataBytes + emptyBytes;
if (total === 0)
return 'Empty';
const codeRatio = codeBytes / total;
const emptyRatio = emptyBytes / total;
if (emptyRatio > 0.8)
return 'Empty/Padding';
if (codeRatio > 0.6)
return 'Code';
if (bankNumber === 0)
return 'Header/Bootstrap';
return 'Data/Graphics';
}
isLikelyInstruction(byte) {
// Common 65816 opcodes that indicate code
const commonOpcodes = [
0xA9, 0xA5, 0xAD, 0xBD, 0xB9, // LDA variants
0x85, 0x8D, 0x9D, 0x99, // STA variants
0x4C, 0x6C, 0x20, // JMP, JSR
0x18, 0x38, 0x58, 0x78, // Flag operations
0xEA, 0x60, 0x40 // NOP, RTS, RTI
];
return commonOpcodes.includes(byte);
}
async detectFunctions() {
const romInfo = this.getRomInfo();
const analysisParams = {
generateLabels: this.options.generateLabels,
bankAware: this.options.bankAware
};
// Check cache for function analysis first
const cachedFunctions = this.enhancedCache.getFunctions(romInfo, analysisParams);
if (cachedFunctions) {
// Handle the case where cached functions is an object with functions array
const functionsArray = cachedFunctions.functions || [];
this.analysis.functions = functionsArray.map((f) => ({
name: f.name || `func_${f.address?.toString(16)?.toUpperCase() || 'unknown'}`,
address: f.address || 0,
size: f.size || 0,
type: f.type || 'Function',
description: f.description
}));
this.stats.functionsDetected = this.analysis.functions.length;
return;
}
try {
// Use enhanced pattern recognition to detect functions
const analysisChunks = [];
const chunkSize = 0x8000;
for (let offset = 0; offset < romInfo.data.length; offset += chunkSize) {
const chunk = romInfo.data.slice(offset, Math.min(offset + chunkSize, romInfo.data.length));
analysisChunks.push({
data: Array.from(chunk),
offset,
size: chunk.length
});
}
// Process chunks to find function patterns
for (const chunk of analysisChunks) {
const functions = await this.findFunctionsInChunk(chunk);
this.analysis.functions.push(...functions);
}
this.stats.functionsDetected = this.analysis.functions.length;
// Generate labels for detected functions
if (this.options.generateLabels) {
await this.generateFunctionLabels();
}
// Cache the function analysis results
this.enhancedCache.setFunctions(romInfo, { functions: this.analysis.functions, data: [] }, analysisParams);
}
catch (error) {
console.warn('Function detection failed:', error);
}
}
async findFunctionsInChunk(chunk) {
const functions = [];
const data = chunk.data;
for (let i = 0; i < data.length - 3; i++) {
// Look for JSR targets (common function entry pattern)
if (data[i] === 0x20) { // JSR absolute
const target = data[i + 1] | (data[i + 2] << 8);
const targetOffset = target - 0x8000 + chunk.offset;
const romInfo = this.getRomInfo();
if (targetOffset >= 0 && targetOffset < romInfo.data.length) {
// Check if this looks like a function entry
const functionBytes = romInfo.data.slice(targetOffset, targetOffset + 32);
if (this.isLikelyFunctionEntry(functionBytes)) {
const functionSize = this.estimateFunctionSize(targetOffset);
functions.push({
name: `func_${target.toString(16).toUpperCase()}`,
address: target,
offset: targetOffset,
size: functionSize,
type: 'Subroutine',
calledFrom: chunk.offset + i
});
}
}
}
}
return functions;
}
isLikelyFunctionEntry(bytes) {
if (bytes.length < 8)
return false;
// Common function entry patterns
const firstByte = bytes[0];
// Stack operations often start functions
if (firstByte === 0x48 || firstByte === 0xDA || firstByte === 0x5A) { // PHA, PHX, PHY
return true;
}
// Register setup
if (firstByte === 0xA9 || firstByte === 0xA2 || firstByte === 0xA0) { // LDA, LDX, LDY immediate
return true;
}
// Mode changes
if (firstByte === 0x18 || firstByte === 0x38 || firstByte === 0x78 || firstByte === 0x58) {
return true;
}
return false;
}
estimateFunctionSize(startOffset) {
let size = 0;
let offset = startOffset;
const maxSize = 0x200; // Reasonable maximum function size
const romInfo = this.getRomInfo();
while (size < maxSize && offset < romInfo.data.length) {
const byte = romInfo.data[offset];
// Look for function end patterns
if (byte === 0x60 || byte === 0x40) { // RTS or RTI
size += 1;
break;
}
// Estimate instruction size
const instructionSize = this.estimateInstructionSize(byte);
size += instructionSize;
offset += instructionSize;
}
return Math.min(size, maxSize);
}
estimateInstructionSize(opcode) {
// Simplified instruction size estimation
// This would need to be more sophisticated for production use
const threeByte = [0x4C, 0x20, 0xAD, 0x8D, 0xBD, 0x9D, 0xB9, 0x99]; // JMP, JSR, absolute modes
const twoByte = [0xA9, 0xA2, 0xA0, 0x85, 0x86, 0x84]; // Immediate, zero page
if (threeByte.includes(opcode))
return 3;
if (twoByte.includes(opcode))
return 2;
return 1; // Single byte instructions
}
async generateFunctionLabels() {
for (const func of this.analysis.functions) {
// Generate meaningful labels based on function characteristics
let labelName = func.name;
// Analyze function content to provide better names
const romInfo = this.getRomInfo();
// Calculate ROM offset from address (assuming LoROM mapping for now)
const romOffset = func.address >= 0x8000 ? func.address - 0x8000 : 0;
if (romOffset < romInfo.data.length) {
const functionData = romInfo.data.slice(romOffset, romOffset + Math.min(func.size, 64));
const analysis = this.analyzeFunctionContent(functionData);
if (analysis.type) {
labelName = `${analysis.type}_${func.address.toString(16).toUpperCase()}`;
func.type = analysis.type;
func.description = analysis.description;
}
}
func.name = labelName;
this.stats.labelsGenerated++;
}
}
analyzeFunctionContent(data) {
// Simple pattern matching for function types
const hasGraphicsOps = data.some(byte => [0x8D, 0x8F].includes(byte)); // STA to PPU registers
const hasAudioOps = data.some(byte => byte >= 0x2140 && byte <= 0x2143);
const hasControllerRead = data.some(byte => [0x4016, 0x4017].includes(byte));
if (hasGraphicsOps) {
return { type: 'Graphics', description: 'Handles graphics operations' };
}
if (hasAudioOps) {
return { type: 'Audio', description: 'Handles audio operations' };
}
if (hasControllerRead) {
return { type: 'Input', description: 'Reads controller input' };
}
return {};
}
async analyzeDataStructures() {
// Analyze ROM for common data structures
const structures = [];
// Look for pointer tables
const romInfo = this.getRomInfo();
for (let offset = 0; offset < romInfo.data.length - 32; offset += 2) {
if (this.isLikelyPointerTable(offset)) {
const tableSize = this.getPointerTableSize(offset);
structures.push({
type: 'Pointer Table',
start: offset,
end: offset + tableSize * 2,
size: tableSize * 2,
entries: tableSize
});
}
}
// Look for graphics data patterns
const graphicsRegions = this.findGraphicsDataRegions();
structures.push(...graphicsRegions);
this.analysis.dataRegions = structures;
}
isLikelyPointerTable(offset) {
// Check if we have consecutive valid ROM addresses
let validPointers = 0;
const romInfo = this.getRomInfo();
for (let i = 0; i < 8; i++) {
const ptr = romInfo.data.readUInt16LE(offset + i * 2);
if (ptr >= 0x8000 && ptr <= 0xFFFF) {
validPointers++;
}
}
return validPointers >= 6; // At least 6 out of 8 look valid
}
getPointerTableSize(offset) {
let size = 0;
const romInfo = this.getRomInfo();
while (offset + size * 2 < romInfo.data.length - 2) {
const ptr = romInfo.data.readUInt16LE(offset + size * 2);
if (ptr < 0x8000 || ptr > 0xFFFF) {
break;
}
size++;
// Reasonable limit
if (size > 256)
break;
}
return size;
}
findGraphicsDataRegions() {
const regions = [];
// Look for patterns typical of graphics data
const romInfo = this.getRomInfo();
for (let offset = 0x8000; offset < romInfo.data.length - 0x400; offset += 0x100) {
const chunk = romInfo.data.slice(offset, offset + 0x400);
if (this.isLikelyGraphicsData(chunk)) {
const regionEnd = this.findGraphicsRegionEnd(offset);
regions.push({
type: 'Graphics Data',
start: offset,
end: regionEnd,
size: regionEnd - offset
});
offset = regionEnd; // Skip ahead
}
}
return regions;
}
isLikelyGraphicsData(data) {
// Graphics data often has repeating patterns and limited value ranges
const valueCounts = new Array(256).fill(0);
for (const byte of data) {
valueCounts[byte]++;
}
// Count how many different values appear
const uniqueValues = valueCounts.filter(count => count > 0).length;
// Graphics data typically uses a limited palette
return uniqueValues < 64 && uniqueValues > 8;
}
findGraphicsRegionEnd(start) {
let end = start + 0x400;
const romInfo = this.getRomInfo();
while (end < romInfo.data.length - 0x100) {
const chunk = romInfo.data.slice(end, end + 0x100);
if (!this.isLikelyGraphicsData(chunk)) {
break;
}
end += 0x100;
}
return Math.min(end, romInfo.data.length);
}
async recognizeGamePatterns() {
// Use MCP server insights to recognize common game patterns
try {
// This would integrate with the zelda3 MCP server for ALTTP-specific patterns
// or other game-specific servers for pattern recognition
// For now, implement basic pattern recognition
this.recognizeCommonPatterns();
}
catch (error) {
console.warn('Game pattern recognition failed:', error);
this.recognizeCommonPatterns();
}
}
recognizeCommonPatterns() {
// Recognize common SNES game patterns
// Look for DMA patterns
this.findDMAPatterns();
// Look for PPU register usage patterns
this.findPPUPatterns();
// Look for common game loop patterns
this.findGameLoopPatterns();
}
findDMAPatterns() {
// Look for DMA setup patterns (0x43xx register writes)
const romInfo = this.getRomInfo();
for (let offset = 0; offset < romInfo.data.length - 8; offset++) {
if (romInfo.data[offset] === 0x8D) { // STA absolute
const addr = romInfo.data.readUInt16LE(offset + 1);
if (addr >= 0x4300 && addr <= 0x437F) {
// Found DMA register write
this.analysis.functions.push({
name: `DMA_Setup_${offset.toString(16).toUpperCase()}`,
address: offset + 0x8000,
size: 8,
type: 'DMA',
description: 'DMA channel setup'
});
}
}
}
}
findPPUPatterns() {
// Look for PPU register access patterns
const ppuRegisters = [
{ addr: 0x2100, name: 'INIDISP' },
{ addr: 0x2101, name: 'OBJSEL' },
{ addr: 0x2105, name: 'BGMODE' },
{ addr: 0x210B, name: 'BG1SC' }
];
for (const reg of ppuRegisters) {
this.findRegisterAccess(reg.addr, reg.name);
}
}
findRegisterAccess(address, name) {
const romInfo = this.getRomInfo();
for (let offset = 0; offset < romInfo.data.length - 3; offset++) {
if (romInfo.data[offset] === 0x8D) { // STA absolute
const addr = romInfo.data.readUInt16LE(offset + 1);
if (addr === address) {
// Found register access - could be part of a larger initialization routine
this.stats.crossReferences++;
}
}
}
}
findGameLoopPatterns() {
// Look for infinite loop patterns that might be main game loops
const romInfo = this.getRomInfo();
for (let offset = 0; offset < romInfo.data.length - 6; offset++) {
// Look for patterns like: loop: ... JMP loop
if (romInfo.data[offset] === 0x4C) { // JMP absolute
const target = romInfo.data.readUInt16LE(offset + 1);
const currentAddr = offset + 0x8000;
// If jumping backwards by a reasonable amount, might be a game loop
if (target < currentAddr && (currentAddr - target) < 0x100 && (currentAddr - target) > 0x10) {
this.analysis.functions.push({
name: `GameLoop_${currentAddr.toString(16).toUpperCase()}`,
address: target,
size: currentAddr - target + 3,
type: 'Main Loop',
description: 'Main game loop or state handler'
});
}
}
}
}
performROMAnalysis() {
return this.analysis;
}
getDisassemblyStats() {
return this.stats;
}
// Override the parent disassemble method to add enhanced features
disassemble(startAddress, endAddress) {
const baseResult = super.disassemble(startAddress, endAddress);
if (this.options.bankAware) {
return this.enhanceWithBankInfo(baseResult);
}
return baseResult;
}
enhanceWithBankInfo(disassemblyLines) {
return disassemblyLines.map(line => {
if (line.address !== undefined) {
const bankInfo = this.bankHandler.getBankInfo(line.address);
return {
...line,
bank: bankInfo.bank,
bankType: bankInfo.type,
physicalAddress: bankInfo.physicalAddress
};
}
return line;
});
}
}
exports.EnhancedDisassemblyEngine = EnhancedDisassemblyEngine;
//# sourceMappingURL=enhanced-disassembly-engine.js.map