4 Commits
Author SHA1 Message Date
ApfelTeeSaft 9aec398919 Implement "IsSelfModifying" flag 2026-01-21 18:30:40 +01:00
ApfelTeeSaft 8242ffb49b Merge pull request #13 from KallDrexx/loopback_instruction_fix
Loopback jumps should occur on the same cpu address of the jump
2026-01-17 17:55:48 +01:00
KallDrexx e76cec3d6d Loopback jumps should occur on the same cpu address of the jump
When a function is decompiled in the middle of a function, and that function
has a jump point prior to the function's entry point, we need to add a
fake virtual instruction that jumps back to the start of the function.

The virtual instruction needs to be the last instruction in the
ordered instruction set.

We previously accomplished that by adding an instruction at the
location of the function entrypiont minus one. However, this was
failing in cases where a system would cause an interrupt right on
the virtual address. Emulators would then save the virtual
instruction's address to the stack, and jump back into that address
once RTI occurs.

This fails because the virtual address can't be decompiled, because
legit code doesn't exist at that address.

To fix this, I updated the `SubAddressOrder` property to allow for negative
values. This allows the loopback instruction to be on the correct CPUAddress
while still being ordered as expected.

Also ensured that virtual addresses do not get labels, as they are not
actually valid jump targets.
2026-01-17 11:44:40 -05:00
ApfelTeeSaft 26af1f4aa6 close enough ig 2025-10-23 11:34:11 +02:00
4 changed files with 103 additions and 8 deletions
+73
View File
@@ -0,0 +1,73 @@
# Changelog
All notable changes to this project will be documented in this file.
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/).
---
## [v1.1.1] - 2025-10-18
### Added
- Decompilation can now start from the **first byte of a code region** (#12).
### Changed
- Invalid instructions are now considered the **end of a function trace** (#11).
- Improved stability of function tracing and edge-case instruction handling.
### Fixed
- Minor internal decompiler logic bugs.
- General performance and reliability improvements across builds.
### Notes
- All Windows builds now include both GUI and CLI versions.
- Linux and macOS builds are CLI-only.
- All binaries are **self-contained** and **do not require a .NET runtime**.
**Contributors:**
[@KallDrexx](https://github.com/KallDrexx)
---
## [v1.1.0] - 2025-10-12
### Added
- Implemented **single function decompiler** with proper tracing.
- Added support for **sub-address instructions** and **virtual instructions** (#10).
- Added **CI/CD workflow** (`build-release.yml`) for automated builds and packaging.
- Added **multiple platform releases**:
- Windows (x64, x86, ARM64) with GUI + CLI
- Linux (x64, ARM64) CLI
- macOS (x64, ARM64) CLI
### Changed
- Improved function discovery to handle **wraparound and disassembly boundaries** (#9).
- Reworked `ToString()` formatting for instructions for clarity.
- Improved tracing logic for **unreferenced instruction analysis** (#6).
- Decompiler now directly jumps to instructions that appear within other instructions.
### Fixed
- Fixed incorrect ordering of instructions in output.
- Fixed 16KB ROMs not decompiling (#7).
- Fixed nullability warnings.
- Fixed various stability issues in the decompiler core.
**Contributors:**
[@ApfelTeeSaft](https://github.com/ApfelTeeSaft), [@KallDrexx](https://github.com/KallDrexx)
---
## [v1.0.0] - 2025-05-15
### Added
- **Initial release** of the NES Decompiler.
- Included both **CLI** and **GUI** builds for Windows (x64).
- Added base decompilation engine and ROM handling logic.
- Added initial README and documentation.
**Contributors:**
[@ApfelTeeSaft](https://github.com/ApfelTeeSaft)
---
## [Unreleased]
- Planned improvements to function boundary detection.
- Optimizations for recursive instruction analysis.
- Additional architecture support under evaluation.
@@ -23,6 +23,12 @@ public class DecompiledFunction
/// </summary>
public IReadOnlyDictionary<ushort, string> JumpTargets { get; }
/// <summary>
/// Indicates that this function is expected to mutate instruction bytes at runtime and should
/// avoid JIT compilation.
/// </summary>
public bool IsSelfModifying { get; set; }
public DecompiledFunction(
ushort address,
IReadOnlyList<DisassembledInstruction> instructions,
@@ -32,13 +38,17 @@ public class DecompiledFunction
JumpTargets = instructions
.Where(x => jumpTargets.Contains(x.CPUAddress))
.Where(x => x.Label != null)
.Where(x => x.SubAddressOrder == 0) // only real instructions should be jumped to
.ToDictionary(x => x.CPUAddress, x => x.Label!);
// We need to order the instructions so that the starting instruction is the first one encountered.
// We can't just rely on the CPU address, because a function may jump to a code point earlier than
// the first instruction.
var entryPointInstructions = instructions.Where(x => x.CPUAddress == address)
.Where(x => x.SubAddressOrder >= 0);
var initialInstructions = instructions
.Where(x => x.CPUAddress >= address)
.Where(x => x.CPUAddress > address)
.OrderBy(x => x.CPUAddress)
.ThenBy(x => x.SubAddressOrder);
@@ -47,6 +57,17 @@ public class DecompiledFunction
.OrderBy(x => x.CPUAddress)
.ThenBy(x => x.SubAddressOrder); // real instructions before virtual ones
OrderedInstructions = initialInstructions.Concat(trailingInstructions).ToArray();
// If there was a loopback jump point at the function address, put that here. This is required
// because if an emulator is executing a virtual loopback instruction and an IRQ occurs, this
// will cause the virtual instruction to be saved to the stack, and that can cause the entry
// point to be wrong.
var loopbackInstructions = instructions.Where(x => x.CPUAddress == address)
.Where(x => x.SubAddressOrder < 0);
OrderedInstructions = entryPointInstructions
.Concat(initialInstructions)
.Concat(trailingInstructions)
.Concat(loopbackInstructions)
.ToArray();
}
}
@@ -26,7 +26,7 @@ public static class FunctionDecompiler
// 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
// a jump back to the function entrypoint.
if (functionAddress == 0x00)
{
const string message = "Wrap around instruction detected for a function at 0000, but that " +
@@ -41,12 +41,12 @@ public static class FunctionDecompiler
var jumpInstruction = new DisassembledInstruction
{
Info = InstructionSet.GetInstruction(0x4C),
CPUAddress = (ushort)(nextAddress - 1),
CPUAddress = nextAddress,
Bytes = [0x4C, (byte)addressLow, (byte)addressHigh],
TargetAddress = functionAddress,
// Make sure they appear after any instruction that already occupies that address
SubAddressOrder = 1,
// Make sure they appear before the function address
SubAddressOrder = -1,
};
instructions.Add(jumpInstruction);
@@ -92,7 +92,8 @@ public static class FunctionDecompiler
// Add labels for any jump targets
foreach (var instruction in instructions)
{
if (jumpAddresses.Contains(instruction.CPUAddress))
// 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}";
}
@@ -78,7 +78,7 @@ namespace NESDecompiler.Core.Disassembly
/// 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.
/// </summary>
public byte SubAddressOrder { get; set; }
public sbyte SubAddressOrder { get; set; }
/// <summary>
/// Returns a string representation of this instruction