UNPKG

snes-disassembler

Version:

A Super Nintendo (SNES) ROM disassembler for 65816 assembly

596 lines 24.4 kB
"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