mirror of
https://github.com/ApfelTeeSaft/NESDecompiler.git
synced 2026-08-27 03:43:30 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26af1f4aa6 | ||
|
|
bc3da6b960 | ||
|
|
c71aa4d6b2 | ||
|
|
bcd9ed4b87 | ||
|
|
c0380bd198 | ||
|
|
c05b7199c9 | ||
|
|
ab9a1fa313 | ||
|
|
bbcd2a6aad | ||
|
|
826747aacb | ||
|
|
8e811e2cbc | ||
|
|
6d3ec6c2c8 | ||
|
|
11b9943c09 | ||
|
|
54b9e3d3df | ||
|
|
a9f323722f | ||
|
|
e8a57399ce | ||
|
|
9068403d56 | ||
|
|
bc76d20895 | ||
|
|
40743c6889 | ||
|
|
32e3d78e6d | ||
|
|
b627bc2a48 | ||
|
|
1e9899b9bc | ||
|
|
e592c31a0c | ||
|
|
b968b151d0 | ||
|
|
588841b46b | ||
|
|
c1e0b17bb3 |
@@ -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
|
||||
@@ -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.
|
||||
@@ -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();
|
||||
|
||||
@@ -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,52 @@
|
||||
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; }
|
||||
|
||||
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)
|
||||
.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 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
|
||||
|
||||
OrderedInstructions = initialInstructions.Concat(trailingInstructions).ToArray();
|
||||
}
|
||||
}
|
||||
@@ -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,169 @@
|
||||
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 = (ushort)(nextAddress - 1),
|
||||
Bytes = [0x4C, (byte)addressLow, (byte)addressHigh],
|
||||
TargetAddress = functionAddress,
|
||||
|
||||
// Make sure they appear after any instruction that already occupies that 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)
|
||||
{
|
||||
if (jumpAddresses.Contains(instruction.CPUAddress))
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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 byte 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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user