28 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
Matthew Shapiro bc3da6b960 Decompilation should be possible from the first byte of a code region (#12) 2025-10-17 20:30:54 -04:00
Matthew Shapiro c71aa4d6b2 Consider an invalid instruction the end of a function trace (#11)
When tracing a function, previously we were throwing an exception
if we encountered an invalid / unknown op code. This is needed because
we don't know how many bytes the instruction contains and thus can't
accurately predict where the next instruction would be.

However, some roms (like super mario bros) use an always taken
branch instruction to save bytes instead of a jump. In this case
the next byte after the branch is an invalid op code that will never
actually be hit.

So this change makes it so that invalid operations act as an end
of function markers, but throw a warning in the console. That
allows unconditional/always branches to work, and for games that
actually use these unofficial op codes they are able to get hints
in the debug window.
2025-10-12 14:31:27 -04:00
Matthew Shapiro bcd9ed4b87 Merge pull request #10 from KallDrexx/support_virtual_instructions
Allow for sub address instructions.



When a wrap around scenario is detected during single function tracing, if the address prior to the "entry point" is a single byte, then we do not have any space to add the required jump call.

This fixes that by adding the concept of sub address instructions. This allows adding instructions at runtime that get sorted correctly against the real instructions from the ROM.

This not only solves the wrapping issue, but also allows for adding hooks at runtime.
2025-10-11 23:17:12 -04:00
KallDrexx c0380bd198 Allow for sub address instructions.
When a wrap around scenario is detected during single function tracing,
if the address prior to the "entry point" is a single byte, then we do
not have any space to add the required jump call.

This fixes that by adding the concept of sub address instructions. This
allows adding instructions at runtime that get sorted correctly against
the real instructions from the ROM.

This not only solves the wrapping issue, but also allows for adding hooks
at runtime.
2025-10-11 23:11:37 -04:00
Matthew Shapiro c05b7199c9 Merge pull request #9 from KallDrexx/single_function_decompile_wraparound
Fix wraparound bug
2025-10-11 21:10:03 -04:00
KallDrexx ab9a1fa313 Fix wraparound bug
Since the entry point for analysis could be in the middle of a loop,
we need to guarantee that a jump is dedicated to the entrypoint, so
that an instruction that comes before the "entrypoint" will redirect
back to the entrypoint after execution
2025-10-11 21:00:22 -04:00
Matthew Shapiro bbcd2a6aad Merge pull request #8 from KallDrexx/single_function_decompile
Add code path to decompile/disassemble a single function
2025-10-11 16:03:41 -04:00
KallDrexx 826747aacb Fixed incorrect ordering of instructions 2025-10-11 15:49:10 -04:00
KallDrexx 8e811e2cbc Some fixes 2025-10-10 23:26:38 -04:00
KallDrexx 6d3ec6c2c8 Initial single function decompiler implementation 2025-10-10 23:06:03 -04:00
ApfelTeeSaft 11b9943c09 Update build-release.yml 2025-10-09 14:04:39 +02:00
ApfelTeeSaft 54b9e3d3df Update build-release.yml 2025-10-09 14:01:44 +02:00
ApfelTeeSaft a9f323722f Add CI/CD workflow 2025-10-09 13:58:18 +02:00
ApfelTeeSaft e8a57399ce Merge pull request #7 from KallDrexx/16kb_fix
Fix 16KB roms not decompiling
2025-09-29 08:47:45 +02:00
KallDrexx 9068403d56 Fix 16KB roms not decompiling
Roms with only 16KB of program code had the incorrect base address during the disassembly process. This caused the instructions not to be found.

Fixed by making sure the base address is adjusted based on the prgRomData size.
2025-09-28 21:16:37 -04:00
ApfelTeeSaft bc76d20895 Merge pull request #6 from KallDrexx/loop_unreferenced_instruction_analysis
Loop EnsureReferencedAddressesAreDisassembled
2025-09-28 20:23:11 +02:00
KallDrexx 40743c6889 Loop EnsureReferencedAddressesAreDisassembled
`EnsureReferencedAddressesAreDisassembled is needed to be run because
a jump/branch may occur that targets interwoven instructions that would
not have been picked up on the first pass.

However, an interwoven instruction may actually end up with a branch
into another interwoven instruction. These instructions wouldn't be
picked seen by the end of the 2nd pass.

So the logic in this function is now set to loop (with a maximum) until
all target addresses that are directly referenced have been
disassembled.
2025-09-28 11:20:12 -04:00
ApfelTeeSaft 32e3d78e6d Merge pull request #5 from KallDrexx/nmi_decompiling
Decompile direct jumps to instructions that are in between other instructions
2025-09-26 14:19:51 +02:00
KallDrexx b627bc2a48 Decompiler direct jumps to instructions that are in between other instructions 2025-09-25 21:59:20 -04:00
ApfelTeeSaft 1e9899b9bc Merge pull request #4 from KallDrexx/nmi_decompiling
Add decompililng of other entrypoints, and add specific support for NMI and IRQ vectors
2025-09-25 02:52:33 +02:00
KallDrexx e592c31a0c Add decompililng of other entrypoints, and add specific support for NMI and IRQ vectors 2025-09-24 20:33:38 -04:00
KallDrexx b968b151d0 Fix nullability warnings 2025-09-10 21:30:08 -04:00
ApfelTeeSaft 588841b46b Merge pull request #2 from KallDrexx/instruction_tostring_fix
Fixed instruction ToString() formatting.
2025-09-08 02:03:51 +02:00
KallDrexx c1e0b17bb3 Fixed instruction ToString() formatting.
`GetOperandFormat` was accidentally using C++ style format strings
instead of C# ones, and thus the values were not being properly
displayed. For example, `LDA #00` was showing as `LDA #$02X`, making it
a bit hard to reason about what value the operand is.
2025-09-07 14:03:35 -04:00
11 changed files with 751 additions and 42 deletions
+291
View File
@@ -0,0 +1,291 @@
name: Build and Release
on:
push:
branches: [ main, master, develop ]
tags:
- 'v*'
pull_request:
branches: [ main, master ]
env:
DOTNET_VERSION: '8.0.x'
PROJECT_NAME: 'NESDecompiler'
jobs:
build-windows:
runs-on: windows-latest
strategy:
matrix:
arch: [x64, x86, arm64]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Restore dependencies
run: dotnet restore
- name: Build CLI
run: |
dotnet publish NESDecompiler.CLI/NESDecompiler.CLI.csproj `
--configuration Release `
--runtime win-${{ matrix.arch }} `
--self-contained true `
--output ./artifacts/cli/win-${{ matrix.arch }} `
-p:PublishSingleFile=true `
-p:PublishTrimmed=true
- name: Build GUI
run: |
dotnet publish NESDecompiler.GUI/NESDecompiler.GUI.csproj `
--configuration Release `
--runtime win-${{ matrix.arch }} `
--self-contained true `
--output ./artifacts/gui/win-${{ matrix.arch }} `
-p:PublishSingleFile=true `
-p:PublishTrimmed=false
- name: Package Windows Release
run: |
$version = if ($env:GITHUB_REF -match 'refs/tags/v(.*)') { $matches[1] } else { (Get-Date -Format "yyyy.MM.dd") + "-" + $env:GITHUB_SHA.Substring(0,7) }
# Package CLI
Compress-Archive -Path ./artifacts/cli/win-${{ matrix.arch }}/* `
-DestinationPath ./NESDecompiler-CLI-win-${{ matrix.arch }}-$version.zip
# Package GUI
Compress-Archive -Path ./artifacts/gui/win-${{ matrix.arch }}/* `
-DestinationPath ./NESDecompiler-GUI-win-${{ matrix.arch }}-$version.zip
# Package Full (CLI + GUI)
New-Item -ItemType Directory -Path ./artifacts/full/win-${{ matrix.arch }} -Force
Copy-Item -Path ./artifacts/cli/win-${{ matrix.arch }}/* -Destination ./artifacts/full/win-${{ matrix.arch }} -Recurse
Copy-Item -Path ./artifacts/gui/win-${{ matrix.arch }}/* -Destination ./artifacts/full/win-${{ matrix.arch }} -Recurse
Copy-Item -Path ./README.md -Destination ./artifacts/full/win-${{ matrix.arch }}
Copy-Item -Path ./LICENSE -Destination ./artifacts/full/win-${{ matrix.arch }}
Compress-Archive -Path ./artifacts/full/win-${{ matrix.arch }}/* `
-DestinationPath ./NESDecompiler-Full-win-${{ matrix.arch }}-$version.zip
shell: pwsh
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: windows-${{ matrix.arch }}
path: |
NESDecompiler-*.zip
retention-days: 7
build-linux:
runs-on: ubuntu-latest
strategy:
matrix:
arch: [x64, arm64]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Restore dependencies (CLI only)
run: |
dotnet restore NESDecompiler.Core/NESDecompiler.Core.csproj
dotnet restore NESDecompiler.CLI/NESDecompiler.CLI.csproj
- name: Build CLI
run: |
dotnet publish NESDecompiler.CLI/NESDecompiler.CLI.csproj \
--configuration Release \
--runtime linux-${{ matrix.arch }} \
--self-contained true \
--output ./artifacts/cli/linux-${{ matrix.arch }} \
-p:PublishSingleFile=true \
-p:PublishTrimmed=true
- name: Set execute permissions
run: chmod +x ./artifacts/cli/linux-${{ matrix.arch }}/NESDecompiler.CLI
- name: Package Linux Release
run: |
if [[ $GITHUB_REF == refs/tags/v* ]]; then
version=${GITHUB_REF#refs/tags/v}
else
version=$(date +%Y.%m.%d)-${GITHUB_SHA:0:7}
fi
# Create release directory
mkdir -p ./artifacts/release/linux-${{ matrix.arch }}
cp -r ./artifacts/cli/linux-${{ matrix.arch }}/* ./artifacts/release/linux-${{ matrix.arch }}/
cp README.md ./artifacts/release/linux-${{ matrix.arch }}/
cp LICENSE ./artifacts/release/linux-${{ matrix.arch }}/
# Create tar.gz
cd ./artifacts/release
tar -czf ../../NESDecompiler-CLI-linux-${{ matrix.arch }}-${version}.tar.gz linux-${{ matrix.arch }}
shell: bash
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: linux-${{ matrix.arch }}
path: |
NESDecompiler-*.tar.gz
retention-days: 7
build-macos:
runs-on: macos-latest
strategy:
matrix:
arch: [x64, arm64]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Restore dependencies (CLI only)
run: |
dotnet restore NESDecompiler.Core/NESDecompiler.Core.csproj
dotnet restore NESDecompiler.CLI/NESDecompiler.CLI.csproj
- name: Build CLI
run: |
dotnet publish NESDecompiler.CLI/NESDecompiler.CLI.csproj \
--configuration Release \
--runtime osx-${{ matrix.arch }} \
--self-contained true \
--output ./artifacts/cli/osx-${{ matrix.arch }} \
-p:PublishSingleFile=true \
-p:PublishTrimmed=true
- name: Set execute permissions
run: chmod +x ./artifacts/cli/osx-${{ matrix.arch }}/NESDecompiler.CLI
- name: Package macOS Release
run: |
if [[ $GITHUB_REF == refs/tags/v* ]]; then
version=${GITHUB_REF#refs/tags/v}
else
version=$(date +%Y.%m.%d)-${GITHUB_SHA:0:7}
fi
# Create release directory
mkdir -p ./artifacts/release/osx-${{ matrix.arch }}
cp -r ./artifacts/cli/osx-${{ matrix.arch }}/* ./artifacts/release/osx-${{ matrix.arch }}/
cp README.md ./artifacts/release/osx-${{ matrix.arch }}/
cp LICENSE ./artifacts/release/osx-${{ matrix.arch }}/
# Create tar.gz
cd ./artifacts/release
tar -czf ../../NESDecompiler-CLI-osx-${{ matrix.arch }}-${version}.tar.gz osx-${{ matrix.arch }}
shell: bash
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: macos-${{ matrix.arch }}
path: |
NESDecompiler-*.tar.gz
retention-days: 7
create-release:
needs: [build-windows, build-linux, build-macos]
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: ./artifacts
- name: Display structure of downloaded files
run: ls -R ./artifacts
- name: Extract version from tag
id: get_version
run: |
VERSION=${GITHUB_REF#refs/tags/v}
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
echo "Version: $VERSION"
- name: Create Release
uses: softprops/action-gh-release@v1
with:
name: NES Decompiler v${{ steps.get_version.outputs.VERSION }}
body: |
# NES Decompiler v${{ steps.get_version.outputs.VERSION }}
## Downloads
### Windows (GUI + CLI)
- **x64**: Full GUI and CLI support
- **x86**: Full GUI and CLI support (32-bit)
- **ARM64**: Full GUI and CLI support (Windows on ARM)
### Linux (CLI only)
- **x64**: Command-line interface
- **ARM64**: Command-line interface
### macOS (CLI only)
- **x64**: Command-line interface (Intel Macs)
- **ARM64**: Command-line interface (Apple Silicon)
## Installation
1. Download the appropriate package for your platform
2. Extract the archive
3. Run the executable (Windows: `NESDecompiler.GUI.exe` or `NESDecompiler.CLI.exe`, Linux/macOS: `./NESDecompiler.CLI`)
## Notes
- Windows packages include both GUI and CLI versions
- Linux and macOS packages include CLI only
- All packages are self-contained and don't require .NET runtime installation
## Changelog
See [CHANGELOG.md](CHANGELOG.md) for details.
files: |
./artifacts/**/*.zip
./artifacts/**/*.tar.gz
draft: false
prerelease: false
token: ${{ secrets.GITHUB_TOKEN }}
build-summary:
needs: [build-windows, build-linux, build-macos]
runs-on: ubuntu-latest
if: always()
steps:
- name: Build Summary
run: |
echo "## Build Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Platform | Architecture | Status |" >> $GITHUB_STEP_SUMMARY
echo "|----------|--------------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Windows | x64 | ${{ needs.build-windows.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Windows | x86 | ${{ needs.build-windows.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Windows | ARM64 | ${{ needs.build-windows.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Linux | x64 | ${{ needs.build-linux.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Linux | ARM64 | ${{ needs.build-linux.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| macOS | x64 | ${{ needs.build-macos.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| macOS | ARM64 | ${{ needs.build-macos.result }} |" >> $GITHUB_STEP_SUMMARY
+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.
+1 -1
View File
@@ -165,7 +165,7 @@ namespace NESDecompiler.CLI
writer.WriteLine(" */");
writer.WriteLine();
string guardName = Path.GetFileNameWithoutExtension(decompiler.ROMInfo.RawData[0].ToString()).ToUpper() + "_H";
string guardName = Path.GetFileNameWithoutExtension(decompiler.ROMInfo.RawData![0].ToString()).ToUpper() + "_H";
writer.WriteLine($"#ifndef {guardName}");
writer.WriteLine($"#define {guardName}");
writer.WriteLine();
+11 -11
View File
@@ -113,17 +113,17 @@ namespace NESDecompiler.Core.CPU
{
AddressingMode.Implied => "",
AddressingMode.Accumulator => "A",
AddressingMode.Immediate => "#$%02X",
AddressingMode.ZeroPage => "$%02X",
AddressingMode.ZeroPageX => "$%02X,X",
AddressingMode.ZeroPageY => "$%02X,Y",
AddressingMode.Relative => "$%02X", // Will be processed specially for branches
AddressingMode.Absolute => "$%04X",
AddressingMode.AbsoluteX => "$%04X,X",
AddressingMode.AbsoluteY => "$%04X,Y",
AddressingMode.Indirect => "($%04X)",
AddressingMode.IndexedIndirect => "($%02X,X)",
AddressingMode.IndirectIndexed => "($%02X),Y",
AddressingMode.Immediate => "#{0:X2}",
AddressingMode.ZeroPage => "${0:X2}",
AddressingMode.ZeroPageX => "${0:X2},X",
AddressingMode.ZeroPageY => "${0:X2},Y",
AddressingMode.Relative => "${0:X2}", // Will be processed specially for branches
AddressingMode.Absolute => "${0:X4}",
AddressingMode.AbsoluteX => "${0:X4},X",
AddressingMode.AbsoluteY => "${0:X4},Y",
AddressingMode.Indirect => "(${0:X4})",
AddressingMode.IndexedIndirect => "(${0:X2},X)",
AddressingMode.IndirectIndexed => "(${0:X2}),Y",
_ => "???"
};
}
@@ -0,0 +1,8 @@
namespace NESDecompiler.Core.Decompilation;
/// <summary>
/// A set of code that may contain executable code
/// </summary>
/// <param name="BaseAddress">Where the first byte of the region can be found from the CPU's memory map</param>
/// <param name="Bytes">The set of data to pull code out of</param>
public record CodeRegion(ushort BaseAddress, ReadOnlyMemory<byte> Bytes);
@@ -0,0 +1,73 @@
using NESDecompiler.Core.Disassembly;
namespace NESDecompiler.Core.Decompilation;
/// <summary>
/// Represents an independently decompiled function
/// </summary>
public class DecompiledFunction
{
/// <summary>
/// The CPU address where the address' first instruction is located
/// </summary>
public ushort Address { get; }
/// <summary>
/// The instructions that make up this function in the correct order in which they should be
/// executed.
/// </summary>
public IReadOnlyList<DisassembledInstruction> OrderedInstructions { get; }
/// <summary>
/// Location and the labels of all jump and branch targets within this function
/// </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,
IReadOnlySet<ushort> jumpTargets)
{
Address = address;
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)
.OrderBy(x => x.CPUAddress)
.ThenBy(x => x.SubAddressOrder);
var trailingInstructions = instructions
.Where(x => x.CPUAddress < address)
.OrderBy(x => x.CPUAddress)
.ThenBy(x => x.SubAddressOrder); // real instructions before virtual ones
// 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();
}
}
+10 -9
View File
@@ -592,6 +592,7 @@ namespace NESDecompiler.Core.Decompilation
if (!disassembler.AddressToInstruction.TryGetValue(address, out var instruction))
{
Console.WriteLine($"Decompilation warning: Instruction {address} (0x{address:X4}) queued to be analyzed but does not exist");
continue;
}
@@ -1026,7 +1027,7 @@ namespace NESDecompiler.Core.Decompilation
{
// Process LDA, LDX, LDY
string register = instruction.Info.Mnemonic.Substring(2);
string variableName = GetVariableName(instruction);
string? variableName = GetVariableName(instruction);
if (variableName != null)
{
@@ -1046,7 +1047,7 @@ namespace NESDecompiler.Core.Decompilation
{
// Process STA, STX, STY
string register = instruction.Info.Mnemonic.Substring(2);
string variableName = GetVariableName(instruction);
string? variableName = GetVariableName(instruction);
if (variableName != null)
{
@@ -1314,7 +1315,7 @@ namespace NESDecompiler.Core.Decompilation
private void GenerateArithmeticCode(DisassembledInstruction instruction, StringBuilder sb)
{
// Handle arithmetic operations: ADC, SBC
string operand = GetOperandString(instruction);
string? operand = GetOperandString(instruction);
if (operand == null)
return;
@@ -1359,7 +1360,7 @@ namespace NESDecompiler.Core.Decompilation
private void GenerateIncrementCode(DisassembledInstruction instruction, StringBuilder sb)
{
// Handle increment operations: INC, INX, INY
string operand = GetOperandString(instruction);
string? operand = GetOperandString(instruction);
switch (instruction.Info.Mnemonic)
{
@@ -1393,7 +1394,7 @@ namespace NESDecompiler.Core.Decompilation
private void GenerateDecrementCode(DisassembledInstruction instruction, StringBuilder sb)
{
// Handle decrement operations: DEC, DEX, DEY
string operand = GetOperandString(instruction);
string? operand = GetOperandString(instruction);
switch (instruction.Info.Mnemonic)
{
@@ -1427,7 +1428,7 @@ namespace NESDecompiler.Core.Decompilation
private void GenerateShiftCode(DisassembledInstruction instruction, StringBuilder sb)
{
// Handle shift operations: ASL, LSR, ROL, ROR
string operand;
string? operand;
if (instruction.Info.AddressingMode == AddressingMode.Accumulator)
{
@@ -1489,7 +1490,7 @@ namespace NESDecompiler.Core.Decompilation
private void GenerateLogicCode(DisassembledInstruction instruction, StringBuilder sb)
{
// Handle logic operations: AND, ORA, EOR, BIT
string operand = GetOperandString(instruction);
string? operand = GetOperandString(instruction);
if (operand == null)
return;
@@ -1533,7 +1534,7 @@ namespace NESDecompiler.Core.Decompilation
private void GenerateCompareCode(DisassembledInstruction instruction, StringBuilder sb)
{
// Handle compare operations: CMP, CPX, CPY
string operand = GetOperandString(instruction);
string? operand = GetOperandString(instruction);
if (operand == null)
return;
@@ -1710,7 +1711,7 @@ namespace NESDecompiler.Core.Decompilation
}
}
private string GetOperandString(DisassembledInstruction instruction)
private string? GetOperandString(DisassembledInstruction instruction)
{
if (instruction.Info.AddressingMode == AddressingMode.Immediate)
{
@@ -0,0 +1,170 @@
using NESDecompiler.Core.CPU;
using NESDecompiler.Core.Disassembly;
namespace NESDecompiler.Core.Decompilation;
public static class FunctionDecompiler
{
/// <summary>
/// Traces and decompiles a single function
/// </summary>
/// <param name="functionAddress">The CPU address of the entry point of the function to decompile</param>
/// <param name="codeRegions">All available regions of bytes that could contain instructions for the function</param>
public static DecompiledFunction Decompile(ushort functionAddress, IReadOnlyList<CodeRegion> codeRegions)
{
var instructions = new List<DisassembledInstruction>();
var jumpAddresses = new HashSet<ushort>();
var seenInstructions = new HashSet<ushort>();
var addressQueue = new Queue<ushort>([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<CodeRegion> 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;
}
}
+72 -12
View File
@@ -25,17 +25,17 @@ namespace NESDecompiler.Core.Disassembly
/// <summary>
/// Information about this instruction's opcode
/// </summary>
public InstructionInfo Info { get; set; }
public required InstructionInfo Info { get; init; }
/// <summary>
/// The raw bytes of this instruction (including operands)
/// </summary>
public byte[] Bytes { get; set; }
public byte[]? Bytes { get; set; }
/// <summary>
/// The operand bytes of this instruction
/// </summary>
public byte[] Operands => Bytes.Length > 1 ? Bytes[1..] : Array.Empty<byte>();
public byte[] Operands => Bytes!.Length > 1 ? Bytes[1..] : Array.Empty<byte>();
/// <summary>
/// The target address for branch and jump instructions
@@ -72,6 +72,14 @@ namespace NESDecompiler.Core.Disassembly
/// </summary>
public bool IsJump => Info.Mnemonic == "JMP" || Info.Mnemonic == "JSR";
/// <summary>
/// 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.
/// </summary>
public sbyte SubAddressOrder { get; set; }
/// <summary>
/// Returns a string representation of this instruction
/// </summary>
@@ -85,7 +93,7 @@ namespace NESDecompiler.Core.Disassembly
}
sb.Append($"{CPUAddress:X4} ");
foreach (var b in Bytes)
foreach (var b in Bytes!)
{
sb.Append($"{b:X2} ");
}
@@ -185,6 +193,19 @@ namespace NESDecompiler.Core.Disassembly
{
entryPoints.Add(romInfo.ResetVector);
}
foreach (var entryPoint in romInfo.EntryPoints)
{
entryPoints.Add(entryPoint);
}
}
public void AddEntyPoint(ushort address)
{
if (address >= 0x8000)
{
entryPoints.Add(address);
}
}
/// <summary>
@@ -196,6 +217,7 @@ namespace NESDecompiler.Core.Disassembly
TraceExecution();
IdentifyFunctions();
GenerateLabels();
EnsureReferencedAddressesAreDisassembled();
}
/// <summary>
@@ -212,20 +234,24 @@ namespace NESDecompiler.Core.Disassembly
/// <summary>
/// Performs a linear disassembly of the code data
/// </summary>
private void LinearDisassembly()
private void LinearDisassembly(int offset = 0)
{
try
{
int offset = 0;
ushort baseAddress = 0x8000;
// 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)
@@ -267,13 +293,18 @@ namespace NESDecompiler.Core.Disassembly
/// <summary>
/// Traces execution from known entry points
/// </summary>
private void TraceExecution()
private void TraceExecution(ushort? additionalTraceAddress = null)
{
try
{
var toTrace = new Queue<ushort>(entryPoints);
var traced = new HashSet<ushort>();
if (additionalTraceAddress != null)
{
toTrace.Enqueue(additionalTraceAddress.Value);
}
while (toTrace.Count > 0)
{
ushort address = toTrace.Dequeue();
@@ -315,6 +346,7 @@ namespace NESDecompiler.Core.Disassembly
if (instruction.Info.Mnemonic == "JMP")
{
continue;
}
}
@@ -398,7 +430,7 @@ namespace NESDecompiler.Core.Disassembly
if (instruction.TargetAddress.HasValue)
{
ushort target = instruction.TargetAddress.Value;
if (labels.TryGetValue(target, out string label))
if (labels.TryGetValue(target, out string? label))
{
instruction.Comment = $"-> {label}";
}
@@ -415,7 +447,7 @@ namespace NESDecompiler.Core.Disassembly
/// Calculates the target address for branch and jump instructions
/// </summary>
/// <param name="instruction">The instruction to process</param>
private void CalculateTargetAddress(DisassembledInstruction instruction)
public static void CalculateTargetAddress(DisassembledInstruction instruction)
{
if (instruction.Info.AddressingMode == AddressingMode.Relative)
{
@@ -437,6 +469,34 @@ namespace NESDecompiler.Core.Disassembly
}
}
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();
}
}
/// <summary>
/// Returns the disassembly as a formatted string
/// </summary>
+11 -1
View File
@@ -73,10 +73,20 @@ namespace NESDecompiler.Core.ROM
/// </summary>
public ushort ResetVector { get; set; }
/// <summary>
/// The NMI handler address
/// </summary>
public ushort NmiVector { get; set; }
/// <summary>
/// The IRQ handler address
/// </summary>
public ushort IrqVector { get; set; }
/// <summary>
/// The raw ROM data for reference
/// </summary>
public byte[] RawData { get; set; }
public byte[]? RawData { get; set; }
/// <summary>
/// List of identified entry points (including reset vector and NMI)
+31 -8
View File
@@ -21,13 +21,13 @@ namespace NESDecompiler.Core.ROM
private const int FLAGS_10_OFFSET = 10;
// ROM data
private byte[] romData;
private ROMInfo romInfo;
private byte[]? romData;
private ROMInfo? romInfo;
/// <summary>
/// Information about the loaded ROM
/// </summary>
public ROMInfo ROMInfo => romInfo;
public ROMInfo? ROMInfo => romInfo;
/// <summary>
/// Loads a NES ROM file from disk
@@ -75,7 +75,7 @@ namespace NESDecompiler.Core.ROM
private ROMInfo ParseROMHeader()
{
// Verify iNES header signature "NES" followed by MS-DOS EOF
if (romData.Length < HEADER_SIZE ||
if (romData!.Length < HEADER_SIZE ||
romData[0] != 0x4E || romData[1] != 0x45 || romData[2] != 0x53 || romData[3] != 0x1A)
{
throw new InvalidROMFormatException("Invalid iNES ROM header");
@@ -107,15 +107,38 @@ namespace NESDecompiler.Core.ROM
romInfo.CHRROMOffset = romInfo.PRGROMOffset + romInfo.PRGROMSize;
// Identify entry points (reset vector)
var end = romInfo.PRGROMOffset + romInfo.PRGROMSize;
var nmiOffset = end - 6;
var resetOffset = end - 4;
var irqOffset = end - 2;
ushort GetVectorAddress(int offset)
{
if (offset >= 0 && offset < romData.Length - 1)
{
return (ushort)(romData[offset] | (romData[offset + 1] << 8));
}
return 0;
}
// Identify entry points (reset vector), NMI handler, and irq handler
if (romInfo.PRGROMSize > 0)
{
// In 6502, reset vector is at 0xFFFC-0xFFFD
// For NES, this is mapped to the end of the first PRG ROM bank
int resetVectorOffset = romInfo.PRGROMOffset + romInfo.PRGROMSize - 4;
if (resetVectorOffset >= 0 && resetVectorOffset < romData.Length - 1)
romInfo.ResetVector = GetVectorAddress(resetOffset);
romInfo.NmiVector = GetVectorAddress(nmiOffset);
romInfo.IrqVector = GetVectorAddress(irqOffset);
if (romInfo.NmiVector > 0)
{
romInfo.ResetVector = (ushort)(romData[resetVectorOffset] | (romData[resetVectorOffset + 1] << 8));
romInfo.EntryPoints.Add(romInfo.NmiVector);
}
if (romInfo.IrqVector > 0)
{
romInfo.EntryPoints.Add(romInfo.IrqVector);
}
}