using NESDecompiler.Core.CPU; using NESDecompiler.Core.Disassembly; namespace NESDecompiler.Core.Decompilation; public static class FunctionDecompiler { /// /// Traces and decompiles a single function /// /// The CPU address of the entry point of the function to decompile /// All available regions of bytes that could contain instructions for the function public static DecompiledFunction Decompile(ushort functionAddress, IReadOnlyList codeRegions) { var instructions = new List(); var jumpAddresses = new HashSet(); var seenInstructions = new HashSet(); var addressQueue = new Queue([functionAddress]); while (addressQueue.TryDequeue(out var nextAddress)) { if (!seenInstructions.Add(nextAddress)) { if (nextAddress == functionAddress) { // This means a branch occurred that caused the flow to wrap around to instructions preceding // the function entrance. This usually happens when there is a jump/branch to right before the // entrypoint, usually due to decompiling in the middle of a loop. To fix this, we need to add // a jump back to the function entrypoint. if (functionAddress == 0x00) { const string message = "Wrap around instruction detected for a function at 0000, but that " + "doesn't make sense"; throw new InvalidOperationException(message); } var addressHigh = (functionAddress & 0xFF00) >> 8; var addressLow = functionAddress & 0x00FF; var jumpInstruction = new DisassembledInstruction { Info = InstructionSet.GetInstruction(0x4C), CPUAddress = nextAddress, Bytes = [0x4C, (byte)addressLow, (byte)addressHigh], TargetAddress = functionAddress, // Make sure they appear before the function address SubAddressOrder = -1, }; instructions.Add(jumpInstruction); } continue; } var instruction = GetNextInstruction(nextAddress, codeRegions); if (instruction == null) { // Consider no instruction the end of the function. This is usually the case // with an always taken branch continue; } instructions.Add(instruction); // Ensure the function entrypoint has a label if (instruction.CPUAddress == functionAddress && instruction.Label == null) { instruction.Label = $"sub_{functionAddress:X4}"; jumpAddresses.Add(functionAddress); } if (IsEndOfFunction(instruction)) { continue; } if (instruction.TargetAddress != null) { jumpAddresses.Add(instruction.TargetAddress.Value); addressQueue.Enqueue(instruction.TargetAddress.Value); } if (!instruction.IsJump) { addressQueue.Enqueue((ushort)(nextAddress + instruction.Info.Size)); } } // Add labels for any jump targets foreach (var instruction in instructions) { // Only real instructions should have a label, virtual ones should not if (jumpAddresses.Contains(instruction.CPUAddress) && instruction.SubAddressOrder == 0) { instruction.Label = $"loc_{instruction.CPUAddress:X4}"; } } return new DecompiledFunction(functionAddress, instructions, jumpAddresses); } private static DisassembledInstruction? GetNextInstruction(ushort address, IReadOnlyList regions) { var relevantRegion = regions .Where(x => x.BaseAddress <= address) .Where(x => x.BaseAddress + x.Bytes.Length > address) .FirstOrDefault(); if (relevantRegion == null) { var message = $"No code region contained the address 0x{address:X4}"; throw new InvalidOperationException(message); } var offset = address - relevantRegion.BaseAddress; var bytes = relevantRegion.Bytes.Span[offset..]; var info = InstructionSet.GetInstruction(bytes[0]); if (!info.IsValid) { var message = $"Warning: encountered unknown op code 0x{bytes[0]:X2} at address 0x{address:X4}"; Console.WriteLine(message); return null; } if (bytes.Length < info.Size) { var message = $"Opcode {info.Mnemonic} at address 0x{address:X4} requires {info.Size} bytes, but only " + $"{bytes.Length} are available"; throw new InvalidOperationException(message); } var instruction = new DisassembledInstruction { Address = (ushort)offset, CPUAddress = address, Info = info, Bytes = bytes[..info.Size].ToArray(), }; Disassembler.CalculateTargetAddress(instruction); return instruction; } private static bool IsEndOfFunction(DisassembledInstruction instruction) { // RTI and RTS are obviously the end of a function. We consider BRK and JSR // to be the end of a function as well because an RTI or RTS will do a function // call into the next instruction. This is required because RTI/RTS could be // returning based on a modified stack, and therefore we are not guaranteed to // be returning to the expected spot. if (instruction.Info.Mnemonic is "JSR" or "BRK" or "RTI" or "RTS") { return true; } // Since we don't know where we are jumping at compile time, this will be treated // as a function call, thus we consider it the end of the function. if (instruction.Info.AddressingMode == AddressingMode.Indirect) { return true; } return false; } }