Inspiration

Reverse engineering iOS and macOS applications still involves a large amount of manual reconstruction. Existing decompilers (which are very expensive) can process ARM64 binaries, but on Objective-C and Swift applications they often expose compiler and runtime machinery instead of the application logic a developer originally wrote.

Common examples include raw objc_msgSend calls, Objective-C fast-enumeration scaffolding, Swift metadata operations, retain/release noise, block descriptors, indirect callback invocations, and control flow expressed through labels and gotos.

I wanted a decompiler built specifically for this environment: ARM64 Mach-O binaries containing Objective-C, Swift, Foundation, UIKit, blocks, and Apple-specific ABI patterns. The goal was not to support every architecture or every programming language. The goal was to go much deeper on the binaries iOS and macOS researchers encounter every day.

What it does

The project converts ARM64 Mach-O machine code into source-like C, Objective-C, and Swift-oriented pseudocode.

It can recover constructs such as:

  • Objective-C message-send syntax
  • classes, selectors, methods, ivars, and protocols
  • Objective-C fast enumeration as for (... in ...)
  • block literals, captured variables, and nested block bodies
  • Swift metadata and value-witness operations
  • Swift strings, optionals, tuples, dynamic casts, and bridge operations
  • structured if, loop, switch, guard, break, and continue regions
  • Foundation collection and string literals
  • UIKit and AppKit API calls
  • function signatures and types derived from SDK headers, metadata, ABI behavior, and interprocedural evidence

How I built it

The decompiler is written in Python and uses a multi-stage compiler-style pipeline.

It begins by parsing Mach-O metadata, symbols, relocations, Objective-C runtime data, and Swift metadata. ARM64 instructions are then lifted into an intermediate representation with explicit registers, memory accesses, flags, branches, calls, and side effects.

From there, the project performs:

  • control-flow graph construction
  • static single-assignment and dataflow analysis
  • stack and register variable recovery
  • type inference
  • interprocedural analysis
  • Objective-C and Swift semantic recovery
  • block and closure reconstruction
  • source-level control-flow structuring
  • expression simplification and final C/Objective-C rendering

A major design priority is that readability transformations must remain faithful to the binary. The project tracks instruction provenance and observable effects through transformations, compares effect paths, and rejects structurally attractive output when equivalence cannot be established.

For example, it does not silently replace repeated Objective-C calls with cached values, and it avoids inventing helper calls such as fabs() when the binary implemented the behavior directly through comparisons and negation.

How Codex and GPT-5.6 helped

Codex with GPT-5.6 contributed substantially to the core decompiler, not only the hosted interface.

Its largest contributions were architectural rather than isolated code generation. Codex helped implement and refine:

  • shared IR and C-AST traversal infrastructure
  • SSA and dataflow convergence
  • provenance-preserving rewrites
  • effect-safe control-flow structuring
  • Objective-C block and byref recovery
  • Swift aggregate, optional, tuple, and callback recovery
  • interprocedural type propagation
  • SDK and runtime type harvesting
  • corpus-wide semantic, structural, and type regression gates

Codex also helped investigate difficult correctness failures. These included register lifetime collisions, multi-register Swift values, ARM64 writeback addressing, stack-object interior pointers, floating-point aggregate arguments, and transformations that improved one function while silently damaging another.

The most valuable outcome was not simply faster implementation. Codex helped turn the project into a more systematic and testable decompiler rather than a collection of special-case output rewrites.

Testing and validation

Decompiler output can look convincing while still being wrong, so validation became a major part of the project.

The current test inventory includes:

  • 8,480 tests
  • 7,160 unit tests
  • 1,320 integration tests
  • 95% line coverage across the core decompiler package
  • 379 compiled fixture binaries
  • fixtures written in C, C++, Objective-C, Objective-C++, Swift, and ARM64 assembly
  • 15 real-world corpus binaries
  • more than 91,000 lines of golden decompiler output
  • more than 4,600 functions tracked through corpus fingerprints

Corpus changes are checked against:

  • byte-for-byte golden output
  • semantic fingerprints
  • structure fingerprints
  • type fingerprints
  • quantitative metrics baselines

This allows the project to make aggressive source-recovery improvements while detecting lost calls, stores, constants, return values, type facts, or control-flow behavior.

Challenges

The hardest part was not translating individual ARM64 instructions into C syntax. The difficult part was reconstructing high-level meaning from optimized machine code without inventing behavior.

Some of the most challenging areas included:

Value identity

ARM64 registers are reused constantly. One physical register may represent several unrelated source-level variables during a function. SSA construction, lifetime splitting, copy propagation, and coalescing were necessary to avoid producing misleading reused locals.

Swift ABI recovery

Swift values frequently span several registers, use indirect result buffers, contain metadata and witness-table arguments, or rely on runtime calls that have little resemblance to the original source. Strings, tuples, optionals, existentials, async contexts, and value-witness operations all required dedicated modeling.

Objective-C blocks

A compiled block contains an ABI layout, invoke function, descriptor, capture fields, copy/dispose helpers, and sometimes nested Swift closure objects. Reconstructing an actual source-like block body requires joining information across several functions and data structures.

Control-flow structuring

Optimized code does not naturally preserve clean source-level if, loop, and switch structures. Shared tails, partial joins, cleanup regions, irreducible graphs, and side-effect ordering make aggressive goto elimination unsafe unless transformations are validated.

Correctness versus readability

A decompiler can always make output look nicer by assuming more. This project instead tries to preserve uncertainty. When a transformation cannot be proven safe, the decompiler should retain a lower-level expression or justified goto rather than emit attractive but false source.

What I learned

The project taught me that a production-quality decompiler is less like a disassembler with a C printer and more like a full compiler middle-end operating in reverse.

The most important lessons were:

  • analysis infrastructure matters more than individual pattern rewrites
  • provenance must survive every transformation
  • type inference needs confidence and evidence, not only heuristics
  • local improvements must be validated across a large corpus
  • output quality and correctness need separate measurements
  • specialized domain knowledge can outperform broader tools on a narrow corpus
  • tests for real compiler output are as important as unit tests for algorithms

I also learned that specialization is a legitimate advantage. By focusing narrowly on ARM64 Mach-O Objective-C and Swift applications, the project can recover structures that general-purpose decompilers often leave buried beneath runtime machinery.

Accomplishments

The project has reached a point where, on many real iOS and macOS application functions, it produces output that is more source-like and easier to understand than established decompilers.

Examples include:

  • converting Objective-C enumeration machinery into normal for-in loops
  • reconstructing nested asynchronous block bodies
  • recovering UIKit view-controller and animation logic
  • resolving Objective-C selectors and ivars
  • reconstructing Swift metadata and value-witness operations
  • recovering large real-world initializers into understandable application phases
  • preserving repeated calls and explicit control flow rather than over-simplifying the binary

The project is still evolving, particularly around difficult Swift ABI cases, C++ cleanup regions, complex aggregate values, and unusual floating-point calling conventions. However, it is already a practical specialized decompiler rather than a toy or syntax-only prototype.

What is next

The next steps are focused on making the project easier to evaluate and use:

  • a hosted comparison interface with curated real-world examples
  • constrained live analysis of uploaded ARM64 Mach-O files
  • synchronized highlighting between competing decompiler outputs
  • confidence and provenance information for recovered constructs
  • broader corpus measurement against IDA, Hopper, Ghidra, and Binary Ninja
  • improved Swift existential and async recovery
  • stronger C++ cleanup and RAII structuring
  • dyld shared-cache and framework-resolution support

The long-term goal is a practical reverse-engineering tool that lets researchers begin with recognizable application logic instead of first spending hours manually removing compiler and runtime scaffolding.

Built With

Share this project:

Updates