A decompiler turns a compiled program back into readable source code, one function at a time. openbinary 26.10.8 decompiles all 25,406 functions in locationd, the background program macOS runs for location services, in 12 seconds, and Ghidra 12.1.4, the standard free decompiler, takes 303 seconds on the same file and laptop.1
At five minutes a file, an analyst decompiles the one file under suspicion. At twelve seconds, every binary in a release can be decompiled before anyone suspects it.
Results
We measured two programs from macOS: locationd, 9.6 MB and 25,406 functions, and syspolicyd, which decides whether software may run, 1.3 MB and 4,174 functions. Ghidra, radare2 and rizin ran on our Mac. Binary Ninja, a paid decompiler, published its own numbers from a much larger machine, which we show but did not measure.2 Analyse is the step before decompiling, finding the functions and the calls between them. radare2 and rizin do not decompile, so their Decompile cells are empty; they do find functions, so they appear in the Analyse and Functions found rows and in the function-finding test.
locationd, 25,406 functions, 9.6 MB
| openbinary26.10.8 | Ghidra12.1.4 | Binary Ninja*6.0 | radare26.2.5 | rizin0.10.0 | |
|---|---|---|---|---|---|
| Decompilea C body for every function | 12.00s25x vs Ghidra | 302.73s | 41.4s | n/a | n/a |
| Analysefinding the functions and the calls, no C | 4.30s29x vs Ghidra | 126.63s | n/a | 79.15s | 44.93s |
| Processor timeall cores added up | 28.9s14x vs Ghidra | 409.3s | n/a | n/a | n/a |
| Memorypeak | 1.82GB2.1x vs Ghidra | 3.83GB | 3.62GB | n/a | n/a |
| Work done by the toolbillions of processor instructions executed | 42112x vs Ghidra | 4,941 | 2,790 | n/a | n/a |
| Functions foundthe four tools on this Mac agree within 0.5% | 25,406 | 25,531 | 28,372 | 25,442 | 25,424 |
syspolicyd, 4,174 functions, 1.3 MB
| openbinary26.10.8 | Ghidra12.1.4 | Binary Ninja*6.0 | radare26.2.5 | rizin0.10.0 | |
|---|---|---|---|---|---|
| Decompilea C body for every function | 1.73s78x vs Ghidra | 134.97s | 7.0s | n/a | n/a |
| Analysefinding the functions and the calls, no C | 0.75s58x vs Ghidra | 43.78s | n/a | 7.18s | 3.18s |
| Functions foundthe four tools on this Mac agree within 0.5% | 4,174 | 4,227 | 4,215 | 4,180 | 4,174 |
The smaller file widens the gap to 78x, since Ghidra is written in Java and sets up a project database before it reads the file, a fixed cost that dominates on a small file. Both binaries are public: locationd, syspolicyd.
Flutter engine, 40,726 functions, 21 MB
The third file is a Linux library, libflutter_linux_gtk.so, the engine behind Flutter desktop apps, written in C++, with its debug information removed and its symbol table kept.
| openbinary26.10.8 | Ghidra12.1.4 | radare26.2.5 | rizin0.10.0 | |
|---|---|---|---|---|
| Decompilea C body for every function | 41.7s37x vs Ghidra | 1547.7s | n/a | n/a |
| Analysefinding the functions and the calls, no C | 16.4s31x vs Ghidra | 510.7s | 115.1s | 204.4s |
| Processor timeall cores added up | 63s36x vs Ghidra | 2288s | n/a | n/a |
| Memorypeak | 3.79GB2.1x vs Ghidra | 7.78GB | n/a | n/a |
| Work done by the toolbillions of processor instructions executed | 87713x vs Ghidra | 11,030 | n/a | n/a |
| Functions foundthe four tools agree within 2% | 40,726 | 41,387 | 41,119 | 41,594 |
searchpartyd, 56,718 functions, 22 MB
The largest macOS file we timed is searchpartyd, the background program behind Find My, with twice the code of locationd.
| openbinary26.10.8 | Ghidra12.1.4 | radare26.2.5 | rizin0.10.0 | |
|---|---|---|---|---|
| Decompilea C body for every function | 36.8s311x vs Ghidra | 11434.2s | n/a | n/a |
| Analysefinding the functions and the calls, no C | 17.4s129x vs Ghidra | 2236.3s | 160.4s | 66.7s |
| Processor timeall cores added up | 77s227x vs Ghidra | 17554s | n/a | n/a |
| Memorypeak | 3.60GB1.3x vs Ghidra | 4.84GB | n/a | n/a |
| Work done by the toolbillions of processor instructions executed | 1,09912x vs Ghidra | 13,038 | n/a | n/a |
| Functions foundthe four tools agree within 0.6% | 56,718 | 56,825 | 57,008 | 57,040 |
dsymutil, 43,056 functions, 50 MB
The fifth file is dsymutil, the tool Xcode uses to build debug-symbol bundles, 50 MB of C++ from the LLVM project. It is the one file where openbinary uses more memory than Ghidra, 8.9 GB against 6.4 GB at peak, and one of its functions, with 2,401 basic blocks and 27 switch tables, takes 18 of openbinary's analysis seconds on its own.
| openbinary26.10.8 | Ghidra12.1.4 | radare26.2.5 | rizin0.10.0 | |
|---|---|---|---|---|
| Decompilea C body for every function | 256.3s18x vs Ghidra | 4548.6s | n/a | n/a |
| Analysefinding the functions and the calls, no C | 47.1s186x vs Ghidra | 8755.2s | 444.6s | 479.1s |
| Processor timeall cores added up | 385s20x vs Ghidra | 7604s | n/a | n/a |
| Memorypeak | 8.93GB1.4x more | 6.41GB | n/a | n/a |
| Work done by the toolbillions of processor instructions executed | 5,8916.9x vs Ghidra | 40,544 | n/a | n/a |
| Functions foundthree agree within 2%, rizin 9% more | 43,056 | 42,993 | 43,755 | 47,082 |
Where the time goes
Two effects account for the gap. openbinary executes 12x fewer instructions than Ghidra on the same file, so it spends 14x less processor time, and it keeps on average 2.4 cores busy against Ghidra's 1.4. Instruction count is the most stable measure: it moved under 0.1% between runs, while Ghidra's wall time moved by 40% because the laptop was busy. Memory follows the same shape, 1.8 GB against 3.8 GB at peak.
Every function becomes an independent job that any core can take, and producing the C accounts for two thirds of the time, finding the functions for the rest.
Finding the functions
A tool can be faster because it finds fewer functions, so we compared the counts: the four tools on this Mac agree within 0.5% on each file. Ghidra hit its 60-second limit on five functions, four in locationd and one in syspolicyd, and produced no C for those, which openbinary did.
macOS programs list their function addresses inside the file. A Linux program leaves the compiler with a symbol table, a list of every function's name and address, and stripping removes that list before the program ships, so a decompiler has to find the function starts on its own. We stripped three Linux files and measured the share of the table's functions each tool found, and how many it reported where the table has none.3
| openbinary26.10.8 | Ghidra12.1.4 | radare26.2.5 | rizin0.10.0 | |
|---|---|---|---|---|
| Ghidra's own decompilera Linux program, C++, 8,251 functions | 100%2 false positives | 100%2,260 false positives | 75% | 38% |
| localsend-clia Linux program, Rust, 14,547 functions | 100%3 false positives | 95%892 false positives | 56%145 false positives | 59%1,251 false positives |
| Flutter enginea Linux library, C++, 39,889 functions | 100%5 false positives | 100%14 false positives | 47%385 false positives | 46%539 false positives |
openbinary found every function in all three files, with ten false positives across 62,687 functions. Ghidra matched it on the two C++ files and found 95% on the Rust one, with far more false positives. The stripped files keep the unwind tables, a crash-handling structure that marks function starts, and openbinary relies on them: with those removed as well, which no normal build does, it finds only a third in the first file.
Side by side
A small function from syspolicyd, picked by rule rather than by hand.4
struct self_s { uint64_t field_8; uint64_t field_10; uint64_t field_18; uint64_t field_20; uint64_t field_28; }; void -[SPSamplingTasking encodeWithCoder:](struct self_s * self, id a1){ uint64_t r12_1; uint64_t r13_1; uint64_t r14_1; uint64_t rax_7; r12_1 = self->uuid; r14_1 = *(uint64_t *)0x1001204e8; (*(uint64_t *)0x1000fef68)(a1); r13_1 = *(uint64_t *)0x1000fef20; (r13_1)(a1, r14_1, r12_1, 0x1001037c8); (r13_1)(a1, r14_1, self->name, 0x1001037e8); (r13_1)(a1, r14_1, self->expiration, 0x100103808); (r13_1)(a1, r14_1, self->samplingRate, 0x100103828); (r13_1)(a1, r14_1, self->publicKey, 0x100103848); rax_7 = (*(uint64_t *)0x1000fef60)(a1); return;}
/* Function Stack Size: 0x18 bytes */ void SPSamplingTasking::encodeWithCoder_(ID param_1,SEL param_2,ID param_3) { undefined8 uVar1; undefined *puVar2; undefined *puVar3; puVar3 = PTR_s_encodeObject_forKey__1001204e8; uVar1 = *(undefined8 *)(param_1 + 8); (*(code *)PTR__objc_retain_1000fef68)(param_3); puVar2 = PTR__objc_msgSend_1000fef20; (*(code *)PTR__objc_msgSend_1000fef20)(param_3,puVar3,uVar1,&cf__uuid); (*(code *)puVar2)(param_3,puVar3,*(undefined8 *)(param_1 + 0x10),&cf__name); (*(code *)puVar2)(param_3,puVar3,*(undefined8 *)(param_1 + 0x18),&cf__expiration); (*(code *)puVar2)(param_3,puVar3,*(undefined8 *)(param_1 + 0x20),&cf__samplingRate); (*(code *)puVar2)(param_3,puVar3,*(undefined8 *)(param_1 + 0x28),&cf__publicKey); (*(code *)PTR__objc_release_1000fef60)(param_3); return;}
- Both tools: produce the same five encode calls in the same order.
- openbinary: keeps the method's real name and reads its fields by name,
self->uuidandself->name, while the struct declaration above the body still names themfield_8tofield_28, which is a bug on our side. - Ghidra: keeps the names of the called helpers,
objc_msgSendamong them, and the text keys such asuuid, where openbinary shows numeric addresses.
What it changes
At this speed the unit of work is a release, not a file.
- Coverage: every finding and every attack path is computed over all 25,406 functions, rather than the handful an analyst opened.
- Scale: a firmware image of hundreds of binaries takes minutes, so every release is a batch job rather than a project.
- Automation: an AI agent or a CI check can wait twelve seconds inside one step, and neither can wait five minutes.
What we don't know
Binary Ninja reports 28,372 functions on locationd, 12% more than we find, and their input is not published, so we cannot tell counting differences from functions we miss. Produced is not the same as correct: we never checked the C against debug information or recompiled it, and the field_8 names above are the defect such a check would catch. Five files is still a small sample: three macOS programs and two Linux files, all x86_64, no Windows binary, and nothing the size of a browser. Ghidra ran once on the three larger files, so those multipliers carry one run's noise and, on searchpartyd, a busy machine.
What's next
The C is produced but not verified. Next are a Windows binary, a file the size of a browser, and a correctness check of the C. You can upload your own binary.
References
-
Run of 2026-10-19 on an Apple M4 Pro, 12 cores, 24 GB, macOS 26.5, with the laptop busy with other work: 1-minute load between 4 and 16 throughout. Each cell is three timed runs after one unmeasured warm-up,
/usr/bin/time -laround the tool process, Ghidra's decompiler child processes sampled every second and added to its CPU, memory and instruction figures. We report each tool's fastest run: load only adds time, so the fastest run is the closest to a tool's true cost; the slowest run stands under each table. Tools: openbinary 26.10.8 release build, CLI sha256bf6a9415d2acb7a6a80541d611f6f37f649abea895140828d94a81ca42492b0f; Ghidra 12.1.4 PUBLIC on Temurin JDK 21.0.12.1+1 aarch64 with the native decompiler built,GHIDRA_MAXMEM=8G, default analysers, a 60 s per-function decompile timeout; radare2 6.2.5 and rizin 0.10.0 built from source,aaaanalysis, no decompiler plugin; Binary Ninja 6.0 from their published figures only. Function counts are openbinary's own, Ghidra's non-external functions, andaflcfor radare2 and rizin. Files: locationd x86_64 slice sha256285b4cc7997eb5e46d289ac01aebd05bf82d5c8eb178a2e4540f77f63e893924(9,598,000 bytes) of universal8440da928be4b9e62fdd25294bdebff234ac49d377f7676ca9707636ef79e7bb; syspolicyd x86_64 slice216cd07e5eaec14046b45574f7d8824da2013da2b836fda81869d40c5f368d85(1,344,688 bytes) of universal68718642e44a4eca3e1df85e7501f04167ac555632174a296cc48abad845bf51. ↩ -
Vector 35, Binary Ninja 6.0 release post: an AMD Ryzen 9 9950X with 96 GB, Ubuntu 26.04, idle, default headless analysis with full decompilation. Their input hashes and macOS build are not published. ↩
-
Ground truth is every FUNC symbol in the
.symtabthat lies in.text, read withreadelf, from the unstripped file; the test copies were made withobjcopy --strip-all, which keeps.eh_frame. A start counts as found only at the exact address. Function lists came from openbinary's JSON, a Ghidra post-script printing each function's entry point (rebased for position-independent files), andafljafteraaain radare2 and rizin. The files: Ghidra 12.1.4's owndecompilebinary (GCC 8.5, 8,251 functions), localsend-cli (Rust 1.97, 14,547) and libflutter_linux_gtk.so from the LocalSend Linux build (39,889, of which 216 remain exported after stripping). ↩ -
Selection rule: walking Ghidra's decompile of the syspolicyd x86_64 slice in address order, take the first function whose name is not auto-generated (no
FUN_,thunk_,_INIT_,entryor__prefix), whose Ghidra C block ({to}) is 14 to 40 lines, and which openbinary also decompiled at the same address. That is-[SPSamplingTasking encodeWithCoder:]at0x1000025cf. ↩