Architectures and platforms
A vendor firmware image for a Zyxel VMG8825-T50K VDSL2 gateway is a 26 MiB zip. Inside it is a proprietary "2RDH" container, inside that a compressed kernel and a SquashFS root filesystem, and unpacking the lot yields 1,683 files. 538 of them are ELF executables, and every one looks like this:
$ xxd -l 32 bin/busybox
00000000: 7f45 4c46 0102 0100 0000 0000 0000 0000 .ELF............
00000010: 0002 0008 0000 0001 0040 3250 0000 0034 .........@2P...4
^^^^ ^^^^ ^^^^^^^^^
| e_machine 8 e_entry = 0x00403250
e_type = 2 (EXEC) (EM_MIPS)
Byte 5 is 02: big-endian. Byte 4 is 01: 32-bit. file adds "no section header" — the link view was stripped. Nothing in this file is x86, nothing is little-endian, and nothing about it resembles the desktop binaries most tooling is written for. 537 of the 538 are in the fixture corpus, and all 537 decode clean: 9,963,208 instructions across the image, zero unknown opcodes.
The 538th, bin/cfg, is a 53,804-byte MIPS32 big-endian executable that never reaches a decoder. The corpus harvester filters by file extension before it looks at the magic, and takes the last dot-separated component of a name as that extension — for a file with no dot, the whole name. cfg is on its non-binary list, alongside sh, conf, key, out, py, js, lua, ini and md, so bin/cfg and bin/sh — the canonical extensionless binary in any firmware rootfs — are discarded as config files. That absence is a measurement bug, not a decoder limit.
That is the job. A binary announces a format and a machine type, and every analysis downstream — disassembly, control flow, taint, syscall naming, calling convention, argument recovery — is built on getting those two fields right and then having a decoder that actually covers the instruction set behind them.
| Question | Section |
|---|---|
| What happens when the format or machine type is resolved wrongly? | Format and architecture misrouting |
| Which file formats are recognized, and what does reading one yield? | Executable formats |
| Which instruction sets decode, and which of them reach IL? | Architectures |
| Which decoders have been run against real binaries, and how clean? | Decode results on real binaries |
| What validates a decoder that has no fixture binary? | Decoder validation |
| How are arguments and syscall numbers resolved per target? | Calling conventions · Platforms and syscall tables |
| Which containers open, and which are identified only? | Containers and unpackers |
| What is claimed but unproven, or known wrong? | Limits |
Format and architecture misrouting
Format and architecture resolution fails quietly: a wrong answer does not throw, it produces plausible garbage.
Routing on bitness. PE routing was once decided on bitness, so every 64-bit PE went through the x86_64 lifter — including ARM64 PEs, which produced a full, confident, entirely fictional call graph. The fix routes on the COFF machine field and returns nothing for machines the pipeline does not fully support, so an unsupported PE yields an honest empty call graph instead of x86-misdecoded output. Honest-empty versus plausible-wrong is the distinction every per-architecture gate exists to enforce.
ARM64X hybrid images. A hybrid ARM64X PE carries ARM64 code and x64 code in one image, described by an IMAGE_ARM64EC_METADATA code map, so there is no single answer to "what architecture is this file" — the same problem made structural. The lifter builds a per-range plan, unions the ARM64 exception directory with the x64 ExtraRFETable, and selects a decoder per address. This is proven against a hand-built hybrid PE only — no real ARM64X binary is in the corpus.
Compile-time format sets. Format and architecture support is a compile-time set, not a runtime one, so a format omitted from a build turns into a refusal at analysis time rather than a link error — the one failure mode that does not even involve a binary. A fleet worker built without the PE option compiled cleanly, passed every lint, and then answered every PE with "the pe Cargo feature is disabled" and every ELF with the equivalent — analysing Mach-O and nothing else. Nothing observable went wrong until something was fed to it. The guard that now exists cannot be a compile-time assertion for the same reason: it executes the built binary once per format, because asking whether a build requested a feature is a different question from whether the thing it runs can read the file.
What a stock build actually contains. Every layer below is a compile-time set except identification, which is never gated.
| Layer | On in a shipped command-line or worker build | Off unless the build names it |
|---|---|---|
| Loadable formats (58) | 4 — Mach-O unconditionally, plus PE, ELF and UEFI Terse Executable | DEX, WASM, COFF, standalone MZ, NE, PEF, BFLT, QNX LMF, Qualcomm MBN, Apple SEP, the Android boot image, the Switch homebrew pair, luac, pyc, and the console, retro and legacy-desktop families |
| Lifted architectures (39) | 32 — arm64, x86_64, x86, ARM32, MIPS, PowerPC, RISC-V, eBPF and C166 come from the decoder library alone; the shipped builds add 21 more — SuperH, SPARC, m68k, s390x, Xtensa, AVR, MSP430, Z80, H8/300, V850, TriCore, Hexagon, NDS32, LoongArch, 8051, 6502, 8085, 8048, 6800, PIC and HC08 | 7 — SH-4, V810, LM32, OpenRISC 1000, CR16, SM83, PA-RISC |
| Bytecode ISAs (9) | CIL | the other 8, including the DEX loader |
| Format identities (170) | all 170, by magic | — |
A binary for an omitted architecture gets a refusal, not a misdecode, and a stock build can name a .dex it cannot open.
Executable formats
58 formats are recognized as loadable images. Six carry a manifest deep enough to describe the structure rather than sample it; 20 more carry a smaller one, several of them bundled so that a single manifest covers a whole console or retro family; the rest are recognized by magic and header validation with no item list behind them. Three have been walked over real binaries — ELF across 584, Mach-O across 9, WASM across 1; the other three are checked against their published specification only. Deprecated items stay in the counts.
| Format | Manifest items | What reading it yields |
|---|---|---|
| ELF | 895 across 22 dimensions, 42 deprecated | program/section headers, .dynamic, GOT/PLT, build-id, RELRO and PIE posture, PT_INTERP libc flavour |
| Mach-O | 632 across 51 dimensions, 32 deprecated | load commands, chained fixups, code-signature SuperBlob and entitlements, compact unwind, ObjC/Swift metadata, fat-slice split |
| PE32+ | 249 across 16 dimensions, 5 deprecated | data directories, IAT, .rsrc tree, load config (CFG, CHPE), Authenticode certificate table, .pdata function ranges |
| DEX | 31 across 3 dimensions | 23 header fields, 3 validation constants, 5 format versions — the table offsets that a separate const-pool walker turns into the naming layer for Dalvik bytecode |
| WASM | 246 across 4 dimensions | section table, 5 malformed-module error classes, and 224 opcodes — the container and its instruction set in one manifest |
| SEP64 | 29 across 4 dimensions | Apple Secure Enclave firmware split into boot / kernel / app slices |
Having a manifest is not the same as matching it. Four format targets fall short:
| Format target | Items dispatched | Rate |
|---|---|---|
| Mac OS Classic PEF | 16 of 19 | 84.2% |
| Mach-O | 439 of 632 | 69.5% |
| COFF | 9 of 16 | 56.2% |
| UEFI TE | 14 of 25 | 56.0% |
| ELF, PE, DEX, WASM, SEP64 | all | 100% |
Mach-O is the one to distrust. Lower rates exist among the architecture targets — mcs96, spc700 and w65c816 sit at 0% — but its 193 unmatched items are the largest count anywhere, a quarter of the 789 unmatched across all 104 targets. They cluster:
| Item class | Unmatched | Of |
|---|---|---|
nlist symbol types | 37 | 41 |
| Header flags | 27 | 30 |
| arm64 and x86_64 relocation types | 22 | 22 |
| Section types | 20 | 23 |
| CPU types and arm64/x86_64 subtypes | 20 | 26 |
| Bind and rebase opcodes | 19 | 23 |
| Chained-fixup pointer formats | 14 | 14 |
| Load commands | 13 | 58 |
| Section attributes | 9 | 10 |
| Export-trie flags | 8 | 8 |
| File types | 4 | 14 |
The figure is a name-match, so it cannot separate an unwired arm from a name spelled differently on the two sides; entitlements and the code-signature SuperBlob read 100% and are demonstrably parsed, while the relocation tables a linked Mach-O never carries read zero. The export trie has no such excuse: it is how a modern Mach-O enumerates what it exports, and none of its 8 flags matches.
Beyond those six, the router recognizes:
| Domain | Formats recognized |
|---|---|
| Android and Java | Android boot images, ART/OAT, Android binary XML (AndroidManifest.xml), Java class files |
| Windows and DOS lineage | DOS MZ, 16-bit Windows NE, DOS LE/LX, Xbox XBE, UEFI Terse Executable, Windows user-mode minidumps, DMP64 kernel pagedumps |
| Unix and other desktop | standalone COFF objects, uClinux BFLT, Mac OS Classic PEF, ECOFF, Plan 9 a.out, Amiga HUNK, MenuetOS, QNX 4 LMF, Apple .symbols, DARPA CGC |
| Bootloaders and firmware | Qualcomm MBN bootloaders and MDT, PC BIOS and option ROMs, Apple GNS1 baseband, ARM Linux self-decompressing zImage wrapper, Atmel AVR firmware as checksum-validated Intel HEX text, Infineon C166, Intel OMF and OMF166, Pebble |
| Consoles and retro | Nintendo GameCube/Wii DOL, Switch NRO/NSO, NES, Mega Drive, Master System, SNES ROMs, SNES SPC700 audio dumps, PSX-EXE, N64, Game Boy, GBA, Nintendo DS, 3DS FIRM, C64 PRG, Atari 2600, VICE snapshots |
Three cases sit across the boundary between a format and an instruction set:
- Recognized as images and lifted as ISAs — Lua
luac, Pythonpyc, Java class files. - Never a top-level format — CIL rides inside a PE, EBC inside a PE or TE.
- Split before analysis — Mach-O universal binaries are sliced, and each slice is a separate image.
Architectures
72 instruction sets are claimed, in exactly three tiers: 39 lifted, 9 bytecode VMs that also reach IL, and 24 decode-only. A test reads the specification registry and asserts every architecture in it appears in exactly one tier, failing on either an architecture with no tier or a tier naming an architecture with no specification — so a new architecture cannot land unmeasured and the gap list cannot go stale.
Lifted architectures (39) — full IL analysis
A lifted architecture reaches intermediate language, so taint, CWE detectors, the interval domain, call-graph construction and argument recovery all run on it. Manifest items are decoder breadth — for most architectures that is opcodes; for m68k it also counts addressing modes.
| Architecture | Items | Architecture | Items | Architecture | Items |
|---|---|---|---|---|---|
| x86_64 | 1186 | tricore | 250 | mc6800 | 118 |
| arm64 | 1008 | riscv | 198 | v850 | 111 |
| x86 | 756 | xtensa | 197 | ebpf | 94 |
| s390x | 490 | hexagon | 196 | hcs08 | 92 |
| powerpc | 390 | m68k | 174 | c166 | 89 |
| arm | 310 | sh4 | 127 | nds32 | 88 |
| sparc | 297 | avr | 119 | 8085 | 80 |
| mips32 | 281 | msp430 | 79 | pic | 76 |
| loongarch | 267 | superh | 74 | mips64 | 72 |
| z80 | 71 | v810 | 65 | 6502 | 64 |
| lm32 | 64 | cr16 | 61 | 8048 | 56 |
| arm64e | 45 | sm83 | 44 | 8051 | 44 |
| h8300 | 42 | pa-risc | 37 | or1k | 36 |
Two further properties — that arithmetic lifts at the right operand width, and that the syscall instruction reaches the intrinsic the resolver reads — are asserted per-architecture for only eight of the 39:
| Group | Architectures | Width and syscall probes |
|---|---|---|
| Probed | x86_64, x86, arm64, arm, powerpc, mips32, mips64, riscv | both asserted |
| Rides another row | arm64e | not probed separately; covered by the arm64 row |
| Hosted Linux, deferred | sparc, m68k, s390x, superh, loongarch, nds32 | deliberately deferred to real-corpus validation rather than asserted on a synthetic sample |
| Bare-metal microcontrollers | most of the remaining 30 | no syscall ABI at all, so the property does not apply |
| Unmeasured | ebpf | neither the width probe nor the constant-load interval probe is wired; the test records both as unset, but the rendered matrix prints them as not-applicable — the glyph a bare-metal chip with no syscall ABI gets |
Every lifted architecture has a textual disassembler. x86, x86_64, arm64 and arm64e use an edge decoder (iced for the x86 family, bad64 for arm64); the other 35 use a hand-rolled renderer.
Bytecode VMs (9) — decode plus IL
All nine emit real IL, so the standard analyses consume them like native code.
| ISA | Items | Where it lives |
|---|---|---|
| wasm | 246 | .wasm modules; the only bytecode VM with any corpus fixture at all — one module, 4,182 instructions |
| dalvik | 195 | DEX inside every APK |
| jvm | 165 | Java class files |
| cil | 66 | .NET assemblies inside a PE |
| ebc | 56 | UEFI option ROMs and add-in card drivers |
| cbpf | 28 | seccomp filters |
| bpf | 27 | kernel eBPF programs |
| pyc | 19 | Python bytecode caches |
| luac | 12 | compiled Lua, including TP-Link's 5.1 variant |
The DEX loader is one of the eight that a stock build leaves out, so a stock analyzer never opens a .dex at all — the Dalvik lifter and its bridge are present, but nothing routes a file to them.
The Dalvik path shows why the container matters as much as the ISA: the register-VM lifter has no DEX context, so an invoke lifts to the opaque token dalvik.method#N and a const-string to dalvik.const_string#N. Every name-keyed detector matches on resolved symbols, so those tokens are invisible to all of them. A separate bridge parses the DEX, walks each class to its method bodies, and rewrites resolvable tokens to their smali names — Ljava/lang/Runtime;->exec(...) — which is what makes the command-execution sink fire. An index that does not resolve, because it is out of range or the app is multidex, keeps its raw token rather than being guessed at.
Decode-only architectures (24) — no IL
These 24 are recognized and disassembled, and nothing else. No taint, no CWE detection, no value-range reasoning, no call graph. Promoting one means writing a lifter.
| Architecture | Items | Architecture | Items | Architecture | Items |
|---|---|---|---|---|---|
| hcs12 | 171 | cp1600 | 55 | rx | 33 |
| rsp | 119 | alpha | 51 | amd29k | 32 |
| mcs96 | 101 | i4004 | 45 | xcore | 32 |
| w65c816 | 92 | rl78 | 44 | chip8 | 26 |
| h8500 | 84 | m8c | 41 | mcore | 25 |
| m16c | 81 | dcpu16 | 36 | xap | 16 |
| spc700 | 66 | tms320 | 36 | brainfuck | 8 |
| propeller | 64 | lh5801 | 35 | malbolge | 8 |
Five of those numbers are aspiration rather than decoder breadth. For these five, "disassembled" describes an intent, not an output:
| Architecture | Manifest items | Items with a dispatch arm |
|---|---|---|
| mcs96 | 101 | 0 |
| w65c816 | 92 | 0 |
| h8500 | 84 | 7 |
| spc700 | 66 | 0 |
| rsp (N64) | 119 | 51 |
Decode results on real binaries
14 of the 104 coverage targets are exercised against fixture binaries; the other 90 have never had a real file fed through them. Fixture counts are not comparable across rows either: the eBPF row is five programs totalling 67 instructions, which is enough to prove the decoder runs and nothing more, and six further rows rest on three fixtures or fewer.
| Target | Fixtures | Instructions decoded | Unknown |
|---|---|---|---|
| ELF (format walk) | 584 | 18,866,477 | 1,845 |
| mips32 | 541 | 10,198,938 | 0 |
| x86_64 | 15 | 6,468,178 | 0 |
| arm64 | 14 | 4,381,364 | 0 |
| Mach-O (format walk) | 9 | 3,418,898 | 0 |
| arm64e | 2 | 335,229 | 0 |
| arm | 4 | 287,914 | 1,755 |
| riscv | 5 | 253,300 | 90 |
| powerpc | 3 | 173,652 | 0 |
| mips64 | 2 | 102,687 | 0 |
| sparc | 1 | 45,674 | 0 |
| s390x | 1 | 38,372 | 0 |
| wasm | 1 | 4,182 | 0 |
| ebpf | 5 | 67 | 0 |
Two architectures are not clean, and the ELF row's 1,845 unknowns are exactly those two summed — 1,755 ARM32 plus 90 RISC-V. Every other architecture in the 584-binary ELF walk decodes completely.
ARM32 is the larger: a glibc-dynamic ARM32 fixture reports 903 unknown slots in 129,878 instructions (0.695%), and the report names the offenders — 764 unnamed, plus 102 b, 31 ldr pc, 6 bx. Branches and PC-relative loads appearing in that list is the ARM/Thumb interworking frontier: the ARM decoder targets the integer core that firmware is built from, and NEON, VFP, coprocessor and Thumb-IT encodings fall back to a raw word rather than a mnemonic.
RISC-V's 90 unknowns across 5 fixtures are a different shape, and a less legible one: they come back as bare riscv.unknown with no mnemonic attached, so unlike ARM32 the report cannot name what failed.
| RISC-V fixture | Instructions | Unknown | Rate |
|---|---|---|---|
| rv32 musl-static | 99,166 | 22 | 0.022% |
| rv64 glibc-dynamic | 81,709 | 29 | 0.035% |
| rv64 | 36,643 | 19 | 0.052% |
| rv64 | 35,711 | 19 | 0.053% |
| fifth fixture | 71 | 1 | 1.41% |
The last row clears the 1%-or-50-unknowns threshold that flags a fixture for investigation, and is a denominator artefact rather than a decoder signal. The manifest is no help in locating any of them either: it is complete against the dispatch source, yet 25 bit-manipulation instructions (rev8, bexti, orc.b, the sh1add.uw family, rori and friends) are dispatched without appearing in it at all, so specification and code have drifted in opposite directions on the same architecture.
537 of the 541 MIPS fixtures are the Zyxel router binaries from the opening; the four remaining are cross-compiled fixtures covering both endiannesses. That one firmware image contributes 9,963,208 of the row's 10,198,938 instructions — more than any other architecture target's whole fixture set, and second overall only to the 584-binary ELF format walk, which contains those same 537 files.
Decoder validation
Decoders are checked three ways: replayed golden vectors from rizin and radare2, a capability matrix driven through the production lift seam, and name inventories cross-referenced against rizin and Ghidra. Across all 104 targets, 12,977 of 13,766 manifest items have a dispatch arm — 94.3%.
Golden vectors
Twelve architectures — sparc, xtensa, avr, h8300, h8500, msp430, superh, v850, z80, tricore, nds32, m68k — replay rizin's assembly corpus, and loongarch replays radare2's ESIL corpus. Each vector is an assembly string and its encoding:
d "movea 0xff, r0, r20" 20a6ff00
d "mov 0xffff, r21" 3506ffff0000
d "mov 0x200000, sp" 230600002000
Two things are asserted per vector: the instruction decodes, and the summed per-instruction lengths equal the hex payload length. Length is the paramount axis — a decoder that gets a mnemonic wrong produces one bad row, but a decoder that gets a length wrong desynchronises the stream and corrupts every basic block after it.
| Outcome | Result |
|---|---|
| Length mismatch | test failure |
| Decode failure | test failure |
| Placeholder mnemonic where rizin has a real name | test failure |
Both sides emit real names but disagree — z80's register-fused ld a,b, sparc's synthetic clr for or %g0,%g0,%rd | counted and reported, does not fail: cannot be separated from harness normalisation without per-architecture equivalence tables |
Rizin row marked asm="invalid" | passes either way — it asserts that something sane happens on undecodable bytes, not a specific mnemonic |
| Corpus file missing or holding no parseable vectors | printed as a skip, run stays green — corpus availability is treated as upstream-vendor state, not a decoder property |
Coverage of the corpus is uneven against the manifest in both directions: h8500 replays golden vectors while only 7 of its 84 manifest items have a dispatch arm, so a green h8500 run says the implemented tenth is right, not that the architecture is covered.
The capability matrix
One table-driven test drives the real production lift seam per architecture and asserts seven properties per chipset. Rows left unset are recorded as unmeasured, never as passing.
| Probe | What it asserts | Where it runs |
|---|---|---|
| CFG | a minimal function lifts to a non-empty CFG with a terminator | asserted per chipset |
| Asm | the architecture's claimed edge decoder and textual backend match what is actually wired | asserted per chipset |
| Width | a width-sensitive arithmetic instruction produces IL at the right operand width | hosted set only |
| Syscall | the architecture's syscall instruction reaches the uniform intrinsic the resolver consumes | hosted set only |
| Interval | a const-load folds to a bounded interval | 8 hosted architectures plus 23 embedded ones, down to lda #5 on a 6502 |
| Convention | the calling convention selected for each (architecture, format) pair is the right one | 20 (architecture, format) pairs |
| Args | the selected convention declares integer parameter registers, or is a declared stack ABI | 35 (architecture, format) pairs |
A set row is not the same as an executed one: the 25 embedded-ISA lift probes and 23 embedded interval probes compile out unless the build names that architecture, and none of those features is in the lifter crate's own default set, so testing that crate alone runs none of them.
The rendered matrix is a hand-maintained copy of these tables rather than a projection of them, and twelve of its ticks have no asserting row behind them: interval for MSP430, Hexagon, s390x, LoongArch, C166 and V810, and argument recovery for arm64e, eBPF, C166, V810, LM32 and OpenRISC 1000.
Each column exists because it caught something:
- Width caught a 32-bit i386 operation modelled as 64-bit and hidden behind a plausible CFG.
- Syscall caught i386's
int 0x80and RISC-V'secalldecoding as unknown, which made both architectures blind to their entire syscall surface. - Interval caught MIPS reading
$zeroas an opaque variable, which left every$zero-derived value at Top and silently disabled value-range analysis for MIPS's canonical constant load.
Cross-tool inventories
Filenames under vendors/rizin/librz/{arch,bin}/p/ and vendors/ghidra/Ghidra/Processors/ are cross-referenced against the manifest names, and one entry in 157 is unshared.
| Inventory | Shared | Of |
|---|---|---|
| rizin architectures | 57 | 57 |
| rizin formats | 63 | 63 |
| Ghidra processors | 36 | 37 |
The single Ghidra-only entry is its 68000 tree — m68k is covered, under that name, with a 162-item opcode dimension at full dispatch, but the parity map is keyed on directory name and does not alias the two. The real m68k gap is elsewhere: 2 of 12 addressing modes, IdxAn and IdxPc, are in the manifest with no dispatch arm, so index-register and PC-indexed operands are the frontier.
Calling conventions
48 conventions, selected automatically from the pair (architecture, image format), not from architecture alone. The same x86_64 instruction stream is SysV on ELF and Mach-O and MS_X64 on PE: different argument registers, and 32 bytes of caller-reserved shadow space on Windows against zero on the others. Getting this wrong recovers the wrong arguments for every call in the binary rather than failing outright.
The format half of the pair only bites where more than one convention is reachable. 32-bit x86 resolves to MsX86Stdcall on every format, ELF and Mach-O included, so the other three MS x86 conventions below are never the automatic answer — they are names an analyst or an annotation can select, not routing outcomes.
| Target | Conventions |
|---|---|
| x86_64 | SysVAmd64, MS_X64 |
| x86 (32-bit) | MsX86Stdcall, MsX86Cdecl, MsX86Fastcall, MsX86Thiscall |
| arm64 | AAPCS64-Apple, AAPCS64-Objc, AAPCS64-SysV |
| ARM32 | AAPCS, AAPCS-VFP |
| MIPS | MIPS_O32, MIPS_N64 |
| PowerPC | SysV_PPC32, SysV_PPC64, Darwin_PPC |
| Other general-purpose | RISC-V, SPARC v8, s390x (Linux ELF), LoongArch LP64D, Hppa (PA-RISC SysV), m68k (GCC), Nds32 (Andes ELF), OpenRISC 1000, LM32 (LatticeMico32) |
| Microcontroller and DSP | Xtensa CALL0, Hexagon QDSP6, AVR (avr-gcc), MSP430 EABI, SuperH (SH-1/2), TriCore EABI, H8/300 (GCC), V850 EABI, Z80 (SDCC), MCS-51 (8051, SDCC), MOS 6502 (cc65), 8080/8085 (SDCC), MCS-48 (8048), 6800 (Motorola), PIC18 (Microchip), HC08/HCS08 (NXP), CR16 (CR16C stdcall), C166 (Infineon/Keil) |
| Language and VM ABIs | SwiftCC, SwiftMethod, GoRegabi, BPF_Helpers, cBPF_Seccomp |
Every register-argument convention the gate names must declare its integer parameter registers, so an architecture that would silently recover zero arguments fails per-chipset. Stack-argument ABIs — 32-bit x86, m68k — are exempt by construction. The gate is 35 (architecture, format) rows and does not reach the whole table: the six architectures missing an argument-recovery row are named under the capability matrix, and the Swift, Go, BPF-helper and seccomp ABIs are asserted for no architecture at all.
Platforms and syscall tables
A syscall is a number in a register, and the number means nothing without knowing which table to read. Five platform families carry tables here — Linux, Apple, Windows, BSD and Solaris/illumos. Number 59:
x86_64 59 = execve arm64 59 = pipe2
sparc 59 = execve ppc 59 = oldolduname
x86_32 59 = olduname arm32 59 = unassigned
And execve itself is 59 on x86_64 and SPARC, 11 on x86_32, ARM32, PowerPC and s390x, 221 on arm64, 4011 on MIPS o32, and 5057 on MIPS n64. Resolving a direct syscall requires the architecture before it requires the operating system.
Behavioural output uses one 12-term vocabulary across all three platforms — FileIo, Network, ProcessControl, Ipc, Memory, Crypto, Time, Signals, Threading, DynamicCode, SystemInfo, Hardware — so an ELF profile and a PE profile can be compared directly rather than each speaking its own dialect.
Linux
9 tables, 3,619 entries.
| Table | Entries | Notes |
|---|---|---|
| x86_64 | 385 | |
| x86_32 | 423 | |
| arm64 | 328 | also resolves RISC-V rv32 and rv64 and LoongArch64 — the same asm-generic numbers from include/uapi/asm-generic/unistd.h, not a separate table |
| ARM32 EABI | 424 | |
| MIPS32 o32 | 445 | predates asm-generic and cannot share it |
| MIPS64 | 374 | predates asm-generic and cannot share it |
| PowerPC | 425 | |
| SPARC | 434 | predates asm-generic and cannot share it |
| s390x | 381 |
Above the syscall layer sit 10 capability domains — Network, Process, Filesystem, Memory, IPC, Crypto, DynamicCode, Time, SystemInfo, Threading — and 57 library families, which is how a statically linked binary with no imports still gets a behavioural reading:
| Family group | Members |
|---|---|
| libc | glibc, musl, uClibc, bionic |
| Crypto | OpenSSL, BoringSSL, mbedTLS, wolfSSL, libsodium, gcrypt, nettle |
| Compression | zlib, bzip2, LZMA, zstd, lz4, brotli |
| Databases | SQLite, LMDB, Berkeley DB, Postgres, MariaDB |
| Network | curl, libsoup, nghttp2, libssh and libssh2, c-ares, libevent, libuv |
| Parsers | expat, libxml2, libyaml, jansson, json-c, cJSON |
| Media | libpng, libjpeg, libtiff, and the rest |
Apple
457 BSD syscalls and 62 Mach traps. Above them sit 1,053 framework and runtime symbols across 52 dimensions — the coverage report's 1,209-across-54 figure counts the same two syscall surfaces again as dimensions of its own, at 101 and 55 entries.
| Group | Frameworks and runtimes |
|---|---|
| Language and object runtimes | Foundation, GCD, the ObjC runtime, ARC barriers, the Swift runtime, Combine |
| Security and authentication | CryptoKit, SecurityFramework, AuthenticationServices, LocalAuthentication, PassKit, StoreKit |
| Networking | NetworkExtension, Network, CFNetwork |
| System, process and device | IOKit, EndpointSecurity, DriverKit, XPC, os_log, SystemConfiguration, SystemExtensions, ServiceManagement, ContainerizationKit, Virtualization, MetricKit |
| Capture and media | Photos, PhotosUI, ScreenCaptureKit, AVFoundation |
| Sensors and accessories | CoreLocation, CoreBluetooth, AccessorySetupKit |
| Intelligence and ML | Apple Intelligence and its runtime, NaturalLanguage, Speech, Vision, CoreML |
| User data and app integration | AppIntents, AlarmKit, EventKit, UserNotifications, ExtensionKit, CoreData, CloudKit, CoreSpotlight, Contacts, GameSave, OSAKit |
| Interface | SwiftUI, WebKit |
1,105 of the 1,209 are dispatched, and all 104 shortfalls are in three of the framework and runtime dimensions:
| Dimension | Dispatched | Of |
|---|---|---|
| Swift runtime | 2 | 51 |
| ObjC runtime | 33 | 72 |
| Foundation | 48 | 64 |
Apple hardening posture is thinner than the framework inventory suggests: 3 of 14 keys have a reader, against 9 of 9 each for Linux and Windows. Nothing dispatches PIE, non-executable heap, RWX segments, stack canaries, FORTIFY, pointer authentication, __DATA_CONST, ad-hoc signing, hardened runtime, library validation or FairPlay encryption. This is the same name-match measurement that reads Mach-O relocations as zero, so some may be read under another name — but it is the weakest measured surface on the platform.
Windows
142 NT SSDT entries and 21 win32k.sys entries in the modern table, plus separate Win7-era tables, because the Vista/7/Server 2008 numbering is a different map, not an older prefix of the same one.
| Table | NT SSDT | win32k |
|---|---|---|
| Modern | 142 | 21 |
| Win7-era, x86 | 401 | 818 |
| Win7-era, x64 | 401 | 827 |
On top of those, 57 syscalls carry per-release numbers across six builds. Most are stable, but the ones that move are exactly the ones worth watching:
win10 win10 win11 win11 win11 win11
1809 22H2 21H2 22H2 23H2 24H2
NtCreateThreadEx 0x00BC 0x00C2 0x00C6 0x00C7 0x00C7 0x00C9
NtCreateUserProcess 0x00C3 0x00C9 0x00CE 0x00CF 0x00CF 0x00D1
NtSuspendProcess 0x01B4 0x01BD 0x01C7 0x01CB 0x01CB 0x01CE
NtSystemDebugControl 0x01B6 0x01BF 0x01C9 0x01CD 0x01CD 0x01D0
NtWriteVirtualMemory 0x003A 0x003A 0x003A 0x003A 0x003A 0x003A
A direct-syscall stub that loads 0xC9 into eax is calling NtCreateThreadEx on Windows 11 24H2 and NtCreateUserProcess on Windows 10 22H2. Low numbers like NtWriteVirtualMemory at 0x3A have not moved across any of the six, which is why the shifting high numbers are where the ambiguity lives.
The stub walk that feeds those tables runs on x86_64 PEs only: an ARM64 or 32-bit PE returns before it starts, so a direct-syscall stub there resolves to nothing rather than to a wrong name.
Which of the six a sample targets is the part a PE cannot answer. Both available signals collapse: any MajorOperatingSystemVersion of 6 or higher resolves to a single baseline, because the MSVC linker default is sticky and 2024-built binaries still declare (10, 0); and the manifest supportedOS GUID {8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a} covers Windows 10 and 11 together, since the schema never split them. So 0xC9 resolves to one name, not two candidates, and the version field is a floor rather than a target.
The rest of the Windows surface:
| Surface | Contents |
|---|---|
| COM | 45 classes |
| API sets | 137 api-ms-win-* api-set names mapped to the host module they forward to, so an import of api-ms-win-core-com reads as combase.dll |
| TEB/PEB | 4 field classes |
| Curated imports | kernel32, ws2_32, bcrypt, crypt32, ole32 |
| DLL-to-domain map | five behavioural domains — process control, network, crypto, host interaction, anti-analysis — plus a catch-all |
BSD family
4 tables, 1,423 entries.
| Table | Entries |
|---|---|
| FreeBSD | 499 |
| OpenBSD | 231 |
| NetBSD | 377 |
| DragonFly | 316 |
Family-specific surfaces matter more than the totals: OpenBSD's pledge and unveil declare a process's own restrictions, and FreeBSD's Capsicum cap_* and jail-control calls do the same.
Solaris and illumos
246 entries, bridged one-to-one into the Linux capability taxonomy. What justifies a separate table are the illumos primitives with no Linux equivalent — door, port, zone, pset, modctl, processor_bind and processor_info. Their userspace wrappers (door_call, port_create, zone_create, pset_bind and siblings) are not in the syscall table; they resolve through the library-symbol catalog, which is what a dynamically linked Solaris binary actually references.
libc flavour
libc flavour is read from the ELF PT_INTERP path, falling back to the DT_NEEDED soname for shared objects, which have no PT_INTERP.
| Evidence | Reads as |
|---|---|
PT_INTERP /lib/ld-musl-* | musl |
PT_INTERP /lib64/ld-linux-*, /lib/ld-linux* | glibc |
PT_INTERP /system/bin/linker* | Android bionic |
DT_NEEDED libc.so.6 | glibc |
DT_NEEDED libc.musl-* | musl |
| Statically linked — no interpreter, no soname | unknown, which is the honest answer |
PT_INTERP /lib/ld-uClibc.so.0 | unknown — matches no prefix in the classifier |
The uClibc row is the weak one: the Zyxel binaries from the opening name /lib/ld-uClibc.so.0 as their interpreter and return unknown, even though the library-identification layer recognizes uClibc by soname and by the __uClibc_main symbol. Embedded Linux is the case this classifier is weakest on, and it supplies 537 of the 584 binaries in the ELF corpus walk.
Containers and unpackers
The identify authority carries 170 format identities, 161 of them backed by a signature-validated magic, and 110 route to a working extractor.
| Count | What it counts | Why it differs from the row above |
|---|---|---|
| 170 | format identities in the identify authority | — |
| 161 | identities backed by a signature-validated magic | 9 are identified without a validated magic |
| 110 | identities that route to a working extractor | the rest are leaves, constants, or executables handed to the analysis engine rather than the unpacker |
Most of the 60 that do not extract are categories with nothing to descend into.
| Category | Contents | Why nothing is extracted |
|---|---|---|
| Crypto markers | 12: the AES forward, reverse and acceleration tables, the AES S-box and round constants, CRC32, MD5 and SHA-256 constants, PKCS DER hash prefixes, RSA structure, LUKS and DPAPI headers | constants rather than containers |
| Media and documents | leaf formats | no nested payload |
| Executables | ELF, PE, UEFI TE | routed to the analysis engine instead of the unpacker |
The four crypto rows that do carve — PEM certificates, private keys, public keys, and OpenSSL blobs — extract because there is an encoded payload to pull out.
Six containers are identified and not extracted. In every case the reason is the same: no extractor has been written, not that the container defeats extraction.
| Format | Kind | State |
|---|---|---|
| btrfs | filesystem | identified, not extracted |
| logfs | filesystem | identified, not extracted |
| Seama | carrier | identified, not extracted |
| eCos | carrier | identified, not extracted |
| Wind River kernel | image | identified, not extracted |
| Qualcomm MBN | bootloader | identified, with the code, signature and certificate sub-regions left unsplit |
Four compression formats are deliberately excluded from automatic routing — compress (.Z), raw zlib, raw LZMA1 and raw xz streams. Their magics are short enough that scanning for them across a firmware image produces more false positives than payloads: raw LZMA1's "magic" is its one-byte properties value, which matches roughly 24% of random bytes. All four extract by name only, from carrier-aware paths that already know what they are looking at. A stripped-OOB YAFFS2 image takes the same posture for the opposite reason — it cannot reach header-valid confidence, so it is never auto-promoted.
The extractor catalog:
| Category | Formats |
|---|---|
| Filesystems | cramfs, EROFS, ext2, FAT, ISO 9660, JFFS2, NTFS, qcow, romfs, squashfs, UBI, UBIFS, UFS, YAFFS2, APFS |
| Archives | 7-zip, ar, ARJ, CAB, cpio, CRX, Dahua, RPM, tar, xar, zip, GPG-signed containers |
| Compression | bzip2, gzip, lz4, lzfse, LZMA, lzop, xz, zstd |
| Firmware carriers | U-Boot, uImage, TRX, Android boot and sparse images, device tree (DTB), FIT, UEFI FFS and capsules, Zyxel 2RDH and sig, CFE, CHK, packimg, RTK, TP-Link and TP-Link RTOS, JBOOT arm / sch2 / stag, Arcadyan, csman, dkbs, D-Link TLV, DLKE, DMS, shrs, QNX IFS, WinCE, pchrom, Autel, DLOB, Matter OTA, SEP64, NSIS, EncFw, encrpted_img, MH01, Motorola S-record, VxWorks symtab, UF2, Apple ftab, XALZ (Xamarin) |
| Kernel and disk images | ARM32 zImage, ARM64 boot images, x86 boot images, raw Linux kernels validated by banner string, MBR, EFI-GPT, DMG |
| Apple | dyld shared cache, pbzx, pbze, AppleArchive, AEA (key-gated), Img4, Trustcache, BOM, BuildManifest, APFS |
| Other containers | OCI and docker save images, OLE (MSI), RAR, .NET single-file apphost bundles, UPX |
DMG, the dyld cache, MH_FILESET kernel collections and Mach-O universal binaries bypass magic scanning entirely and go to dedicated loaders. UPX is a format-aware static unpacker that reconstructs the original Mach-O, ELF or PE image rather than stripping a header. The recursion mechanics, budgets, and the packed-payload verdict handoff are in Unpacking.
Measured against vendored binwalk (111 formats identified, 72 extracted), every extraction difference runs the same way:
| Relation | Count | Formats |
|---|---|---|
| Extracted here, no binwalk entry at all | 23 | ar, xar, AppleArchive, raw LZMA1 and xz streams, Zyxel 2RDH and sig, AEA, BOM, BuildManifest, .NET bundles, the dyld cache, FIT, Img4, NSIS, OCI, OLE, pbze, pbzx, RPM, SEP64, Trustcache, UFS |
| Extracted here, identified only by binwalk | 15 | qcow, U-Boot, CFE, CHK, DLOB, JBOOT arm and stag, the three Linux boot-image formats, packimg, RTK, TP-Link and TP-Link RTOS, Android boot |
| Extracted by binwalk, identified only here | 0 | — |
| Extracted by binwalk, missing here entirely | 0 | — |
| Identified by binwalk, no entry here | 2 | Debian .deb, binhdr |
Limits
90 of the 104 coverage targets have no fixture binary at all. For those, the only claim on record is that the manifest and the dispatch source agree — no real file has ever been fed through them. That agreement is not uniform either: 12,977 of 13,766 items overall, but v850 sits at 86 of 111 with its whole load/store group unmatched, and Mach-O — a target that does have fixtures — at 439 of 632.
The measurement is a name-match between specification and dispatch source, so it drifts in both directions and neither direction is proof. RISC-V dispatches 25 bit-manipulation instructions that appear in no manifest. PA-RISC is the clearest case of the drift running the other way: it reports its integer ALU, logical, address-formation and load/store entries as undispatched — only 8 of its 37 items match — while the real Debian-hppa echo and true from coreutils 9.10-1 decode 5,796 and 5,300 four-byte text slots with 419 and 417 unknown (7.2% and 7.9%), both producing a terminated CFG with a call edge. The remaining unknowns there are floating-point, coprocessor, system and privileged operations, which are out of scope by design; the ceiling that would fail the run is 15%, and deleting the integer decode arms pushes the rate to about 19%.
| Limit | Consequence |
|---|---|
| None of the 24 decode-only architectures appears in the calling-convention table | beyond having no IL they also have no argument recovery and no register naming; a binary for one of them is readable, not analysable |
| ARM32's interworking tail is unresolved | 0.525–0.695% of instructions across all four ARM32 fixtures, glibc-dynamic and musl-static alike |
| ARM64X dual-ISA routing is proven only against a synthetic hybrid PE | no real ARM64X binary has been run through it |
| uClibc binaries report an unknown libc flavour | /lib/ld-uClibc.so.0 matches no prefix in the classifier |
| Windows syscall numbers outside the 57-entry versioned table resolve against the modern NT map | wrong for any build that renumbered them |
| Direct-syscall stub resolution runs on x86_64 PEs only | an ARM64 or 32-bit PE that calls the kernel directly yields no syscall names |
| The golden-vector harness skips a missing or unparseable corpus file without failing | a vendored corpus that disappears is indistinguishable from a decoder that passes every vector |
| Embedded-ISA lift and interval probes compile out unless the build names the architecture | 25 lift probes and 23 interval probes are green by absence in a build that did not ask for them |
| The corpus harvester drops extensionless files whose name matches a non-binary extension | bin/cfg, bin/sh and their kind never reach a decoder, so a coverage figure can be complete over an incomplete corpus |
Reference
- Linux syscall tables — the
syscall*.tblfiles each architecture ships, and the asm-genericunistd.hthat newer ports share. - rizin —
librz/arch/p/andlibrz/bin/p/, and thetest/db/asm/golden-vector corpus replayed here. - Ghidra Processors — SLEIGH specifications, the widest published set of processor definitions.
- ARM64EC and ARM64X — Microsoft's description of the hybrid PE layout.
- binwalk — the firmware-carving baseline: 111 formats identified, 72 with an internal extractor.