ARM64 → C-like decompiler for Mach-O (iOS / macOS) and ELF (Android .so / JNI).
Docs: pipeline · roadmap · parity plan · ELF structuring tests · Swift/Ghidra
Uses the sibling apple-re crates (macho_core,
ipa_vfs, apple_metadata) — same load path as apple-re decompile.
cargo run --example decompile_macho --
cargo run --example decompile_macho -- _while_sum
cargo run --example decompile_macho -- _cd_smoke_call # ObjC [self hello:]
cargo run --example decompile_macho -- --list
cargo run --example decompile_macho -- --engine micro ./MyApp _main
cargo run --example decompile_macho -- --engine micro ./App.ipa _main
cargo run --example decompile_macho -- --framework Foo ./App.ipa -[Bar hello:]Engine compare on the Mach-O fixture:
cargo run --example cmp_engines -- _add1AArch64 Linux ELF built from the same C sources (testdata/decompiler_fixtures/ decompiler_fixtures.elf). Needs sibling [elf_core](https://github.com/androguard)
beside this repo.
# Default: fixture ELF + symbol add1 (Legacy + Micro)
cargo run --example decompile_elf --
# Symbol only → uses the in-repo fixture ELF
cargo run --example decompile_elf -- while_sum
cargo run --example decompile_elf -- switch_sparse
cargo run --example decompile_elf -- --engine micro if_else
# Explicit ELF path + symbol (Android .so, your own binary, …)
cargo run --example decompile_elf -- \
testdata/decompiler_fixtures/decompiler_fixtures.elf for_sum
cargo run --example decompile_elf -- \
path/to/libdatastore_shared_counter.so \
Java_androidx_datastore_core_NativeSharedCounter_nativeTruncateFile
# List exported / function symbols
cargo run --example decompile_elf -- --listRebuild the ELF fixture (needs zig):
testdata/decompiler_fixtures/build_elf.shEngine compare helper (same args: ELF SYMBOL):
cargo run --example cmp_elf_engines -- \
testdata/decompiler_fixtures/decompiler_fixtures.elf do_while_countcargo run --example decompile_elf -- --engine micro add1int add1(int param_1) {
int local_4;
local_4 = param_1;
return (local_4 + 1);
}cargo run --example decompile_elf -- --engine micro while_sumint while_sum(undefined8 param_1) {
…
while (local_8 < local_4) {
local_c = (local_c + local_8);
local_8 = (local_8 + 1);
}
return local_c;
}cargo run --example decompile_elf -- --engine micro switch_sparseint switch_sparse(undefined8 param_1) {
…
switch (local_8) {
case 1: local_4 = 0xb; break;
case 0xa: local_4 = 0x16; break;
case 0x64: local_4 = 0x21; break;
default: local_4 = 0; break;
}
return local_4;
}int _add1(int param_1) {
int local_4;
local_4 = param_1;
return (local_4 + 1);
}int _while_sum(undefined8 param_1) {
int local_4;
int local_8;
int local_c;
local_4 = param_1;
local_8 = 0;
local_c = 0;
while (local_8 < local_4) {
local_c = (local_c + local_8);
local_8 = (local_8 + 1);
}
return local_c;
}undefined8 _cd_smoke_call(id self) {
…
x0 = [self hello:local_1c];
local_20 = x0;
return local_20;
}From androidx libdatastore_shared_counter.so (Micro engine):
jlong Java_androidx_datastore_core_NativeSharedCounter_nativeTruncateFile(
JNIEnv * env, jclass clazz, jint param_3)
{
/* JNINativeInterface — see jni.h (*env)->FindClass / ThrowNew / … */
x0 = _ZN9datastore12TruncateFileEi(param_3);
if (w0 != 0) {
x0 = _strerror();
x0 = (*env)->FindClass(env, "java/io/IOException");
if (x0 != 0) {
x0 = (*env)->ThrowNew(env, x0, x20);
}
}
return x0;
}nativeCreateSharedCounter — same JNI fold, errno in x19, stack cookie:
jlong Java_androidx_datastore_core_NativeSharedCounter_nativeCreateSharedCounter(
JNIEnv * env, jclass clazz, jint param_3)
{
/* JNINativeInterface — see jni.h (*env)->FindClass / ThrowNew / … */
undefined4 local_10;
int local_38;
int local_40;
local_38 = local_10;
local_40 = 0;
x0 = _ZN9datastore19CreateSharedCounterEiPPv(param_3, sp);
if (w0 != 0) {
x0 = _strerror(x19);
x0 = (*env)->FindClass(env, "java/io/IOException");
if (x0 != 0) {
x0 = (*env)->ThrowNew(env, x0, x21);
}
}
x10 = *((x22 + 0x28));
if (x10 != local_38) {
x0 = __stack_chk_fail();
} else {
return x0;
}
}use arm_decompiler::{decompile_macho_symbol, DecompilerOptions};
let out = decompile_macho_symbol(
&std::fs::read("MyApp")?,
"_main",
&DecompilerOptions::default(),
)?;
println!("{}", out.source);IPA or fat Mach-O (main executable / ARM64 slice). Optional framework inside the IPA:
use arm_decompiler::{decompile_apple_symbol, AppleLoadOptions, DecompilerOptions};
let ipa = std::fs::read("App.ipa")?;
let load = AppleLoadOptions {
framework: Some("Foo".into()), // None = Payload/*.app main binary
arch: None, // None = prefer arm64
};
let out = decompile_apple_symbol(&ipa, "_main", &load, &DecompilerOptions::default())?;
println!("{}", out.source);ELF (you supply the function bytes + symbol map — see examples/cmp_elf_engines.rs):
use arm_decompiler::{collect_cstrings, decompile_elf_symbol, DecompilerOptions, Engine};
let mut opts = DecompilerOptions::default();
opts.engine = Engine::Micro;
opts.string_lits = /* VA → C string from PT_LOAD */;
let out = decompile_elf_symbol(code, base_va, "Java_…_nativeFoo", &symbols, &opts)?;Apple-re CLI (full Mach-O / IPA / ObjC / Swift — same crates, extra commands):
cargo run -p apple_re_cli --manifest-path ../apple-re/Cargo.toml -- \
decompile ./MyBinary -n _main[arm_decompiler-py/](arm_decompiler-py/) — PyO3 / maturin.
cd arm_decompiler-py
pip install maturin lief
maturin develop --releasefrom arm_decompiler import decompile
result = decompile(
code, # raw ARM64 bytes of one function
base_vaddr=0x1000, # VA of first instruction
name="sub_1000",
symbols={0x2000: "malloc"}, # optional call targets
)
print(result["source"])import lief
from arm_decompiler import decompile
elf = lief.parse("libdatastore_shared_counter.so")
name = "Java_androidx_datastore_core_NativeSharedCounter_nativeTruncateFile"
sym = elf.get_symbol(name)
size = sym.size or 0x200
code = bytes(elf.get_content_from_virtual_address(sym.value, size))
symbols = {s.value: s.name for s in elf.symbols if s.value and s.name}
print(decompile(code, sym.value, name, symbols)["source"])Batch every exported Java_*:
for sym in elf.exported_symbols:
if not sym.name.startswith("Java_") or not sym.value:
continue
code = bytes(elf.get_content_from_virtual_address(sym.value, sym.size or 0x200))
try:
src = decompile(code, sym.value, sym.name, symbols)["source"]
except ValueError as e:
print(f"# skip {sym.name}: {e}")
continue
print(f"\n/* ==== {sym.name} @ {sym.value:#x} ==== */\n{src}")from arm_decompiler import decompile_macho, list_macho_functions
image = open("MyApp", "rb").read() # or App.ipa
print(list_macho_functions(image)[:8])
print(decompile_macho(image, "_main")["source"])
print(decompile_macho(image, "_main", framework="Foo")["source"])import lief
from arm_decompiler import decompile
macho = lief.parse("MyApp")
sym = next(s for s in macho.symbols if s.name in ("_main", "main") or s.name.endswith("_main"))
va = sym.value
sect = next(s for s in macho.sections if s.virtual_address <= va < s.virtual_address + s.size)
off = va - sect.virtual_address
nxt = sorted(
s.value for s in macho.symbols
if s.value > va and sect.virtual_address <= s.value < sect.virtual_address + s.size
)
end = nxt[0] if nxt else sect.virtual_address + sect.size
code = bytes(sect.content[off : off + (end - va)])
symbols = {s.value: s.name for s in macho.symbols if s.value and s.name}
print(decompile(code, va, sym.name, symbols)["source"])| Crate | Source |
|---|---|
macho_core, apple_metadata, ipa_vfs |
sibling ../apple-re (path dep) |
arm_disassembler |
androguard/arm_disassembler |
elf_core (dev / ELF examples) |
local sibling until published |
cargo test
cargo test --test apple_image # IPA wrap + Mach-O via apple-re
cargo test --test elf_structuring # ELF control-flow / switch / calls
cargo test --test decbench_scoreboard -- --nocapture # DecBench-style Union/S/T/R rates
cargo test --test decompiler_tests scoreboard -- --nocapture
cargo test --test decompiler_tests performance -- --nocaptureRebuild fixtures (macOS + clang / swiftc):
cd testdata/decompiler_fixtures && ./build.sh
cd testdata/swift_fixtures && ./build.sh