using System; using System.Collections.Generic; using System.Text; using NESDecompiler.Core.CPU; using NESDecompiler.Core.Exceptions; using NESDecompiler.Core.ROM; namespace NESDecompiler.Core.Disassembly { /// /// Represents a disassembled instruction with its address and operands /// public class DisassembledInstruction { /// /// The address of this instruction in the ROM /// public ushort Address { get; set; } /// /// The CPU memory address this instruction maps to /// public ushort CPUAddress { get; set; } /// /// Information about this instruction's opcode /// public required InstructionInfo Info { get; init; } /// /// The raw bytes of this instruction (including operands) /// public byte[]? Bytes { get; set; } /// /// The operand bytes of this instruction /// public byte[] Operands => Bytes!.Length > 1 ? Bytes[1..] : Array.Empty(); /// /// The target address for branch and jump instructions /// public ushort? TargetAddress { get; set; } /// /// Potential label for this instruction /// public string? Label { get; set; } /// /// Potential comment for this instruction /// public string? Comment { get; set; } /// /// Whether this instruction is a potential function entry point /// public bool IsFunctionEntry { get; set; } /// /// Whether this instruction is a potential function exit point /// public bool IsFunctionExit => Info.Mnemonic == "RTS" || Info.Mnemonic == "RTI"; /// /// Whether this instruction is a branch instruction /// public bool IsBranch => Info.Type == InstructionType.Branch; /// /// Whether this instruction is a jump instruction /// public bool IsJump => Info.Mnemonic == "JMP" || Info.Mnemonic == "JSR"; /// /// Determines the order of this instruction within a single address space. This is mostly /// needed in the cases that additional instructions are needed to be added in the same /// address location at runtime. Can be used to add runtime hooks or to work around /// decompilation issues. Should be 0 for all native instructions from a ROM. /// public sbyte SubAddressOrder { get; set; } /// /// Returns a string representation of this instruction /// public override string ToString() { var sb = new StringBuilder(); if (!string.IsNullOrEmpty(Label)) { sb.AppendLine($"{Label}:"); } sb.Append($"{CPUAddress:X4} "); foreach (var b in Bytes!) { sb.Append($"{b:X2} "); } sb.Append(new string(' ', (3 - Bytes.Length) * 3 + 2)); sb.Append(Info.Mnemonic); if (Info.AddressingMode != AddressingMode.Implied && Info.AddressingMode != AddressingMode.Accumulator) { sb.Append(' '); if (Info.AddressingMode == AddressingMode.Relative && TargetAddress.HasValue) { sb.Append($"${TargetAddress.Value:X4}"); } else if (Operands.Length == 1) { string operandFormat = Info.GetOperandFormat(); sb.Append(string.Format(operandFormat, Operands[0])); } else if (Operands.Length == 2) { string operandFormat = Info.GetOperandFormat(); ushort value = (ushort)((Operands[1] << 8) | Operands[0]); sb.Append(string.Format(operandFormat, value)); } } if (!string.IsNullOrEmpty(Comment)) { sb.Append($" ; {Comment}"); } return sb.ToString(); } } /// /// Disassembles 6502 machine code into assembly language /// public class Disassembler { private ROMInfo romInfo; private byte[] codeData; private List instructions; private Dictionary addressToInstruction; private HashSet entryPoints; private HashSet referencedAddresses; private Dictionary labels; private int labelCounter; /// /// The list of disassembled instructions /// public IReadOnlyList Instructions => instructions; /// /// Maps CPU addresses to disassembled instructions /// public IReadOnlyDictionary AddressToInstruction => addressToInstruction; /// /// The list of entry points (e.g., reset vector, NMI vector) /// public IReadOnlySet EntryPoints => entryPoints; /// /// The list of addresses referenced by the code /// public IReadOnlySet ReferencedAddresses => referencedAddresses; /// /// Maps CPU addresses to labels /// public IReadOnlyDictionary Labels => labels; /// /// Creates a new disassembler for the specified ROM /// /// Information about the ROM /// The code data to disassemble public Disassembler(ROMInfo romInfo, byte[] codeData) { this.romInfo = romInfo ?? throw new ArgumentNullException(nameof(romInfo)); this.codeData = codeData ?? throw new ArgumentNullException(nameof(codeData)); instructions = new List(); addressToInstruction = new Dictionary(); entryPoints = new HashSet(); referencedAddresses = new HashSet(); labels = new Dictionary(); labelCounter = 0; if (romInfo.ResetVector != 0) { entryPoints.Add(romInfo.ResetVector); } foreach (var entryPoint in romInfo.EntryPoints) { entryPoints.Add(entryPoint); } } public void AddEntyPoint(ushort address) { if (address >= 0x8000) { entryPoints.Add(address); } } /// /// Disassembles the code data /// public void Disassemble() { LinearDisassembly(); TraceExecution(); IdentifyFunctions(); GenerateLabels(); EnsureReferencedAddressesAreDisassembled(); } /// /// Gets the disassembled instruction at the specified address /// /// The CPU address /// The disassembled instruction, or null if not found public DisassembledInstruction? GetInstructionAt(ushort address) { addressToInstruction.TryGetValue(address, out var instruction); return instruction; } /// /// Performs a linear disassembly of the code data /// private void LinearDisassembly(int offset = 0) { try { // ushort baseAddress = 0x8000; ushort baseAddress = (ushort)(0x10000 - romInfo.PRGROMSize); while (offset < codeData.Length) { ushort cpuAddress = (ushort)(baseAddress + offset); if (addressToInstruction.ContainsKey(cpuAddress)) { // We have already disassembled this instruction and progressed from here, // so we can stop. break; } byte opcode = codeData[offset]; var instructionInfo = InstructionSet.GetInstruction(opcode); if (!instructionInfo.IsValid) { offset++; continue; } if (offset + instructionInfo.Size > codeData.Length) { offset++; continue; } byte[] bytes = new byte[instructionInfo.Size]; Array.Copy(codeData, offset, bytes, 0, instructionInfo.Size); var instruction = new DisassembledInstruction { Address = (ushort)offset, CPUAddress = cpuAddress, Info = instructionInfo, Bytes = bytes }; CalculateTargetAddress(instruction); instructions.Add(instruction); addressToInstruction[cpuAddress] = instruction; offset += instructionInfo.Size; } } catch (Exception ex) { throw new DisassemblyException($"Error during linear disassembly: {ex.Message}", ex); } } /// /// Traces execution from known entry points /// private void TraceExecution(ushort? additionalTraceAddress = null) { try { var toTrace = new Queue(entryPoints); var traced = new HashSet(); if (additionalTraceAddress != null) { toTrace.Enqueue(additionalTraceAddress.Value); } while (toTrace.Count > 0) { ushort address = toTrace.Dequeue(); if (traced.Contains(address)) { continue; } traced.Add(address); if (!addressToInstruction.TryGetValue(address, out var instruction)) { continue; } if (entryPoints.Contains(address)) { instruction.IsFunctionEntry = true; } if (instruction.IsJump) { if (instruction.TargetAddress.HasValue) { ushort target = instruction.TargetAddress.Value; referencedAddresses.Add(target); if (instruction.Info.Mnemonic == "JSR") { entryPoints.Add(target); ushort returnAddress = (ushort)(address + instruction.Info.Size); toTrace.Enqueue(returnAddress); } toTrace.Enqueue(target); if (instruction.Info.Mnemonic == "JMP") { continue; } } } else if (instruction.IsBranch) { if (instruction.TargetAddress.HasValue) { ushort target = instruction.TargetAddress.Value; referencedAddresses.Add(target); toTrace.Enqueue(target); } } else if (instruction.IsFunctionExit) { continue; } ushort nextAddress = (ushort)(address + instruction.Info.Size); toTrace.Enqueue(nextAddress); } } catch (Exception ex) { throw new DisassemblyException($"Error during execution tracing: {ex.Message}", ex); } } /// /// Identifies functions and their boundaries /// private void IdentifyFunctions() { try { foreach (ushort entryPoint in entryPoints) { if (addressToInstruction.TryGetValue(entryPoint, out var instruction)) { instruction.IsFunctionEntry = true; } } } catch (Exception ex) { throw new DisassemblyException($"Error during function identification: {ex.Message}", ex); } } /// /// Generates labels for referenced addresses /// private void GenerateLabels() { try { foreach (ushort entryPoint in entryPoints) { if (addressToInstruction.TryGetValue(entryPoint, out var instruction)) { string label = $"sub_{entryPoint:X4}"; instruction.Label = label; labels[entryPoint] = label; } } foreach (ushort address in referencedAddresses) { if (!labels.ContainsKey(address) && addressToInstruction.TryGetValue(address, out var instruction)) { string label = $"loc_{labelCounter++:X4}"; instruction.Label = label; labels[address] = label; } } foreach (var instruction in instructions) { if (instruction.TargetAddress.HasValue) { ushort target = instruction.TargetAddress.Value; if (labels.TryGetValue(target, out string? label)) { instruction.Comment = $"-> {label}"; } } } } catch (Exception ex) { throw new DisassemblyException($"Error during label generation: {ex.Message}", ex); } } /// /// Calculates the target address for branch and jump instructions /// /// The instruction to process public static void CalculateTargetAddress(DisassembledInstruction instruction) { if (instruction.Info.AddressingMode == AddressingMode.Relative) { // Branch instructions use relative addressing // The offset is signed and relative to the next instruction sbyte offset = (sbyte)instruction.Operands[0]; ushort nextAddress = (ushort)(instruction.CPUAddress + instruction.Info.Size); instruction.TargetAddress = (ushort)(nextAddress + offset); } else if (instruction.IsJump && (instruction.Info.AddressingMode == AddressingMode.Absolute || instruction.Info.AddressingMode == AddressingMode.Indirect)) { if (instruction.Operands.Length == 2) { ushort target = (ushort)((instruction.Operands[1] << 8) | instruction.Operands[0]); instruction.TargetAddress = target; } } } private void EnsureReferencedAddressesAreDisassembled() { const int baseAddress = 0x8000; // Keep tracing until we no longer have unknown referenced addresses. Using a for loop // to ensure we don't get stuck in an infinite loop (can probably happen if one instruction // attempts to jump to an unknown instruction I think). for (var count = 0; count < 100; count++) { var unknownReferencedAddresses = referencedAddresses .Where(x => !addressToInstruction.ContainsKey(x)) .Where(x => x > baseAddress) .ToArray(); foreach (var referencedAddress in unknownReferencedAddresses) { var offset = referencedAddress - baseAddress; LinearDisassembly(offset); TraceExecution(referencedAddress); } // Update functions and labels IdentifyFunctions(); GenerateLabels(); } } /// /// Returns the disassembly as a formatted string /// public string ToAssemblyString() { var sb = new StringBuilder(); sb.AppendLine("; 6502 Disassembly"); sb.AppendLine($"; ROM: {romInfo}"); sb.AppendLine(); foreach (var instruction in instructions) { sb.AppendLine(instruction.ToString()); } return sb.ToString(); } } }