Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,46 @@
# splat Release Notes

### Unreleased

* Add support for Win32 PE binaries (x86 PE32 and x86_64 PE32+).
* New `platform: win32` option backed by a self-contained PE parser
(every populated data directory + COFF symtab + x64 SEH unwind
info + the .NET CLR header) and a Capstone-based x86 / x86_64
disassembler. Optional dependency group `win32` pulls in
`capstone>=5.0.0`.
* New segtypes under `segtypes/win32/`: `header` (structured PE
header byte-by-byte dump + human-readable summary block),
`text` / `asm` (Capstone disasm with GAS-compatible operand
rewrites), `data` / `rodata` (heuristic string + pointer
detection, NUL-run collapse), `bss` (NOLOAD reservation), `bin`
(opaque blob for `.reloc` / `.rsrc` / signature / COFF symtab),
`pdata` (PE32+ RUNTIME_FUNCTION rows with optional decoded
UNWIND_INFO opcode lists).
* New compiler tags: `MSVC2..14`, `MINGW`, `CLANG_LLD`. All share
the same MASM-style asm conventions; distinct names preserve
provenance of generated configs.
* `create_config` auto-detects PE files (MZ + PE magic), generates
a YAML + symbol_addrs.txt with named symbols for the entrypoint,
exports (incl. forwarders as comments), eager + delay imports,
TLS callbacks, SafeSEH handlers, /guard:cf targets, /GS security
cookie, .NET CLR metadata pointers, and unwind RVAs.
* `auto_link_sections` default is `[]` for `platform: win32` (PE
sections are independent subsegments — implicit MIPS-style
sibling generation produces phantom linker entries otherwise).
* New `python -m splat.scripts.win32_reassemble <yaml>` script:
runs `as` + `ld` + `objcopy` against the splat-generated
layout to reconstruct a PE. With `exact_encoding: true` on
text/data/pdata subsegments, the reassembled PE is
byte-identical to the original. Verified end-to-end on
5 real-world binaries: Sysinternals PsExec (PE32) + PsExec64
(PE32+), PuTTY 0.60 (vintage MSVC6), PuTTY 0.70 32-bit (MSVC14
with `.00cfg` CFG section), PuTTY 0.83 64-bit (MSVC14 PE32+
with 2410 RUNTIME_FUNCTION entries).
* Tests: 199 unit tests covering the PE parser, label generation
helpers, segtype emission, header rendering, and string
detectors; 10 end-to-end tests covering split + reassemble +
GAS-clean assembly on both PE32 and PE32+ synthetic fixtures.

### 0.40.1

* Always write the link dependency file.
Expand Down
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

A binary splitting tool to assist with decompilation and modding projects

Currently, only N64, PSX, PS2 and PSP binaries are supported. More platforms may come in the future.
Currently N64, PSX, PS2, PSP and Win32 PE (x86 / x86_64) binaries are supported. More platforms may come in the future.

Please check out the [wiki](https://github.com/ethteck/splat/wiki) for more information including [examples](https://github.com/ethteck/splat/wiki/Examples) of projects that use splat.

Expand All @@ -27,8 +27,23 @@ splat64[mips]>=0.40.1,<1.0.0
### Optional dependencies

- `mips`: Required when using the N64, PSX, PS2 or PSP platforms.
- `win32`: Required when using the Win32 PE platform (pulls in Capstone for x86 / x86_64 disassembly).
- `dev`: Installs all the available dependencies groups and other packages for development.

### Gamecube / Wii

For Gamecube / Wii projects, see [decomp-toolkit](https://github.com/encounter/decomp-toolkit)!

### Win32 PE support

The `win32` platform handles PE32 (x86) and PE32+ (x86_64) binaries built by MSVC 4.x-14.x, MinGW (libgcc-linked), and Clang-LLD. Decoded directories include exports, imports, delay imports, bound imports, resources, exception/SEH tables (with unwind-info opcode lists), TLS, /GS + /SAFESEH + /guard:cf load-config, base relocations, debug (CodeView PDB GUID/age extraction), the CLR runtime header (.NET assemblies), and the deprecated COFF symbol table.

Workflow:

```bash
python -m splat.scripts.create_config my.exe # auto-generate YAML + symbol_addrs.txt
python -m splat split my.exe.yaml # produce GAS-clean .s + linker script
python -m splat.scripts.win32_reassemble my.exe.yaml # link bytes back into a PE
```

With `exact_encoding: true` on the text/data/pdata subsegments the reassembled PE is byte-identical to the original.
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,12 @@ mips = [
"n64img>=0.3.3",
"crunch64>=0.5.1,<1.0.0",
]
win32 = [
"capstone>=5.0.0",
]
dev = [
"splat64[mips]",
"splat64[win32]",
"ruff",
"mypy",
"types-PyYAML",
Expand Down
109 changes: 109 additions & 0 deletions src/splat/disassembler/capstone_disassembler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"""Capstone-backed disassembler used by the win32 platform.

The MIPS disassembler stack (spimdisasm/rabbitizer) is incompatible with x86,
so win32 segments do not flow through `CommonSegCodeSubsegment`. This module
exposes a tiny façade: configure a Capstone engine once, hand it out to
segtypes for them to decode byte ranges, and surface known section names.
"""

from typing import Optional, Set

from . import disassembler
from ..util import log


class CapstoneDisassembler(disassembler.Disassembler):
CAPSTONE_MIN = (5, 0, 0)

def __init__(self):
self._md = None

def configure(self):
# Defer engine creation to `get_engine()` — at this point in startup
# the target hasn't been parsed yet, so we don't yet know whether
# it's PE32 (CS_MODE_32) or PE32+ (CS_MODE_64).
try:
import capstone # noqa: F401 — just verify availability
except ImportError:
log.error(
"The win32 platform requires the optional 'capstone' dependency. "
"Install it with: pip install 'splat64[win32]'"
)

def check_version(self, skip_version_check: bool, splat_version: str):
try:
import capstone
except ImportError:
log.error(
"The win32 platform requires the optional 'capstone' dependency. "
"Install it with: pip install 'splat64[win32]'"
)

if not skip_version_check:
cs_version = getattr(capstone, "__version__", None)
if cs_version is not None:
parts = []
for chunk in cs_version.split("."):
digits = "".join(c for c in chunk if c.isdigit())
parts.append(int(digits) if digits else 0)
while len(parts) < 3:
parts.append(0)
if tuple(parts[:3]) < self.CAPSTONE_MIN:
log.error(
f"splat {splat_version} requires at least capstone "
f"{self.CAPSTONE_MIN}, but {cs_version} is installed"
)
log.write(
f"splat {splat_version} (powered by capstone {cs_version or '?'})"
)

def get_engine(self):
if self._md is not None:
return self._md

import capstone
from ..platforms import win32 as win32_platform

arch = capstone.CS_ARCH_X86
# Honour the parsed PE's bitness when the platform module has been
# initialized; otherwise default to PE32 (32-bit).
if win32_platform.info.is_pe32_plus:
mode = capstone.CS_MODE_64
else:
mode = capstone.CS_MODE_32

md = capstone.Cs(arch, mode)
md.detail = True
md.syntax = capstone.CS_OPT_SYNTAX_INTEL
self._md = md
return md

def known_types(self) -> Set[str]:
# Mirror the standard primitive type names that the spimdisasm
# backend exposes so symbol_addrs files written for win32 binaries
# can use the same `type:u32` / `type:asciz` vocabulary.
return {
"u8",
"u16",
"u32",
"u64",
"s8",
"s16",
"s32",
"s64",
"f32",
"f64",
"char",
"char*",
"asciz",
}


def get_capstone_disassembler() -> Optional["CapstoneDisassembler"]:
"""Return the active CapstoneDisassembler if one is wired up, else None."""
from . import disassembler_instance

inst = disassembler_instance.get_instance()
if isinstance(inst, CapstoneDisassembler):
return inst
return None
9 changes: 9 additions & 0 deletions src/splat/disassembler/disassembler_instance.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from .disassembler import Disassembler
from .spimdisasm_disassembler import SpimdisasmDisassembler
from .capstone_disassembler import CapstoneDisassembler
from .null_disassembler import NullDisassembler

from ..util import options
Expand All @@ -19,6 +20,14 @@ def create_disassembler_instance(skip_version_check: bool, splat_version: str):
__instance.configure()
return

if options.opts.platform == "win32":
__instance = CapstoneDisassembler()
__initialized = True

__instance.check_version(skip_version_check, splat_version)
__instance.configure()
return

raise NotImplementedError("No disassembler for requested platform")


Expand Down
1 change: 1 addition & 0 deletions src/splat/platforms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
from . import ps2 as ps2
from . import psx as psx
from . import psp as psp
from . import win32 as win32
Loading