univdreams
A universal compiler and decompiler suite. The premise:
Given a compiled binary
B, produce sourceSsuch that compilingSreproducesBbyte for byte.
Try it in your browser: https://karpeleslab.github.io/univdreams/
Most decompilers paraphrase. They lose the choices a compiler made — register allocation, instruction encoding, instruction order, padding, jump-table layout — and a recompile of their output is structurally similar but not identical to the input. univdreams treats those choices as first-class information in the source language: they live in attributes/directives that pin the compiler back to the same shape on the way out.
Status
The headline property is working and defended on every push, for every format:
lower_to_{elf,pe,macho,raw}(parse(decompile_to_text(bin))) == bin-bytes
For every committed fixture, the entire file — headers, segment / section tables, every section's content, every interstitial padding gap, and code-signature blobs — round-trips through the .ud source language byte-identically. The corpus covers 15 binaries totalling roughly 490 KB, plus the 420 KB wmpcdcs8-mpg4c32.dll (Windows msmpeg4 codec) as an opt-in external fixture.
What's working today:
- Byte-identical round-trip across ELF64, PE/COFF, thin Mach-O (x86-64 + arm64), and 16-bit Windows NE, plus 6502 raw images.
- Architectures: x86-64 + i386 via
iced-x86, AArch64 (decode + lift), 6502 (full assembler + disassembler). - Structured statement lifting, not just
@asm()dumps:if/switch/goto, register-named locals,dword ptr [global] = exprstores,lea-as-&address-of,sub_foo(arg_8, arg_c)calls with stdcall/cdecl push-chain folding,tail_F(args)for tail-jmps, prologue/epilogue auto-generation, SSA expression composition. - PE / Mach-O readability comparable to Ghidra's Headers + Memory Map + Symbol Table + Listing panes: structural decode of load commands (
LC_SEGMENT_64,LC_SYMTAB,LC_MAIN,LC_BUILD_VERSION,LC_LOAD_DYLIB, etc.), inline disassembly comments, IAT-resolved imports (GetDriverModuleHandle(arg_c)). - DWARF reader for typed function signatures (parameters and return type from
.debug_info). - Function discovery layered across
.symtab,.dynsym,.eh_frame, PE export table, byte-pattern signatures (CRT helpers), and size-filling for unsymbolised binaries. - WASM playground at https://karpeleslab.github.io/univdreams/ running the full pipeline in-browser.
- 250 tests across 17 crates, fmt + clippy + tests defended on every push.
What's not done yet:
- Higher-level decompilation: loops as
for/whilerather thangoto, struct-field naming from offset patterns, type recovery from access patterns when DWARF is absent. - libc / Win32 / Foundation runtime signature DBs beyond the current CRT-helper set.
- Fat (universal) Mach-O wrappers; 32-bit Mach-O.
- Mach-O code-signature regeneration after edits (currently the signature blob rides as opaque bytes — editing the source breaks the signature, same as any binary edit).
- Edit semantics:
@asmedits whose re-encoded length changes don't yet warn.
See docs/roadmap.md for what's done in detail and what's next.
What it does today
Decompile a binary to canonical .ud source:
) #[abi="stdcall", autogen_pro] {
if ();
if ();
&
) {
Compare the same idea for Mach-O:
; )
)
Recompile the same .ud source back to a byte-identical binary:
)
Targets
| Tier | Format | Architectures | Status |
|---|---|---|---|
| v1 (working) | ELF64 | x86-64 (SysV), x86 (32-bit), aarch64 | ✅ whole-binary source round-trip; DWARF; structured lifts |
| v1 (working) | PE/COFF | x86-64, x86 (Windows MSVC + MinGW) | ✅ whole-binary source round-trip; imports + signatures |
| v1 (working) | Mach-O (thin) | x86-64, arm64 (macOS) | ✅ whole-binary source round-trip; Ghidra-style listing |
| v1 (working) | NE | x86-16 (Windows 1.x–3.x) | ✅ whole-binary source round-trip; Ghidra-style listing (imports, exports, 16-bit disasm) — structured lifting pending |
| v1 (working) | raw / flat | 6502 (Apple I / WozMon) | ✅ round-trip + lift |
| 2 | Mach-O fat | universal binaries | demux to thin first |
| 2 | ARM / Thumb | 32-bit ARM | not yet |
| 3 | High-level lifting | loops, types, struct fields | partial — see roadmap |
The v1 complexity bar reached: scalar code from gcc / clang / MSVC / MinGW with or without debug info. Auto-vectorization, LTO/PGO, hand-tuned asm, and packed binaries are explicit non-goals for v1.
Use cases driving the design
- Reverse engineering and patching: edit a function, recompile, ship a binary indistinguishable from the original except for your edit.
- Education / research / CTF: see what the compiler actually did, in a language that explains itself.
- Reproducible-build verification (later): independently compile a vendor's source and verify it matches their shipped binary.
Binary porting between architectures is not in v1. A faithful round-trip on one arch is the precondition for cross-arch porting later.
Implementation language
Rust. Reasoning, briefly:
- Strong type system catches IR-transformation bugs that are otherwise silent.
- The ecosystem is the best fit:
iced-x86for x86 enc/dec,gimlifor DWARF /.eh_frame, hand-rolled ELF / PE / Mach-O readers / writers for round-trip control. - Memory safety matters when parsing untrusted binaries.
Repository layout
.
├── README.md
├── Cargo.toml # the single `univdreams` crate
├── docs/
│ ├── architecture.md # pipeline, IR, how directives preserve info
│ ├── roadmap.md # phased milestones, what's done
│ ├── source-language.md # the .ud language, directives, examples
│ ├── round-trip-contract.md # what "identical bytes" means precisely
│ └── installers.md # running Win32 installers under `ud analyze --monitor`
└── src/ # one crate, one module per subsystem
├── common/ # shared types: VAddr, Result, byte helpers
├── format/ # ELF64 + PE/COFF + thin Mach-O + NE + raw readers + writers (byte-identical)
├── arch/
│ ├── codec/ # the arch-codec trait + registry every backend implements
│ ├── x86/ # x86 decode + lift + Intel formatter + assembler
│ ├── aarch64/ # AArch64 decode + lift
│ ├── mos6502/ # 6502 decode + lift + assembler
│ └── bpf/ # eBPF / Solana SBF decode + lift
├── ir/ # Function, BasicBlock, Terminator (generic over arch)
├── analysis/ # function discovery (symtab / eh_frame / signatures)
├── signatures/ # byte-pattern DB (CRT helpers)
├── debug/ # DWARF reader → typed signatures
├── ast/ # .ud AST + canonical pretty-printer
├── translate/ # .ud → binary lowering + binary → .ud decompile (all formats)
├── emulator/ # x86 sandbox: MMU, CPU, PE/NE/ELF loaders, Win16/Win32/Linux hosts
├── cli/ # library side of the `ud` driver (feature `cli`, on by default)
├── wasm/ # wasm-bindgen bindings for the browser playground (feature `wasm`)
└── bin/ud/ # the `ud` binary
Quick start
# Build
# Run end-to-end byte-identical round-trip on the test corpus
# Decompile any supported binary to .ud (auto-detects ELF / PE / Mach-O / 6502)
# Source round-trip: decompile → emit → parse → lower → check byte-equality
# Verify an .ud file's @asm lines decode to canonical Intel-syntax form
Library use: drive a guest like a foreign library
univdreams::emulator ships an FFI-shaped front end over the underlying sandbox. A Rust consumer can drive a Windows DLL the same way they would dlopen a shared library — useful for codec analysis, malware triage, and any "load this 32-bit Win32 binary safely and tell me what it does" workflow. All in safe Rust; no unsafe, no host filesystem, network, or registry access from the guest unless an emulation [Context] explicitly attaches a virtual one.
use Guest;
let bytes = read?;
let mut guest = load?; // dlopen-shaped: also runs DllMain
// Call an exported function like an extern fn — typed in, typed out.
let version: u32 = guest.call?;
// Marshal a buffer into guest memory, pass the pointer, read it back.
let payload = vec!;
let ptr = guest.alloc?;
let rc: i32 = guest.call?;
let decoded = guest.read?;
| FFI concept | Guest API |
|---|---|
dlopen / LoadLibrary |
Guest::load(name, bytes) |
calling an extern fn |
guest.call("Export", (a, b, c)) |
| typed arguments | tuples (()–8-arity) of u32 / i32 / u16 / u8 / bool |
| typed return | R: FromRet — u32 / i32 / bool / (), inferred |
*const T |
guest.alloc(&bytes) → guest pointer |
*const c_char |
guest.alloc_cstr("…") |
| reading an out-param | guest.read(ptr, len) after the call |
The default call convention is stdcall (the Win32 norm — args pushed right-to-left, callee cleans the stack). cdecl exports work too: the run-loop unwinds to a synthetic return sentinel, so who-cleans-the-stack doesn't change the observed return. The underlying Sandbox stays reachable via guest.sandbox() / sandbox_mut() for coverage maps, the Context VFS/registry, and the VfW IC* helpers (ic_open, ic_decompress_*, ic_compress_*).
Pre-built variants for less-common cases: Guest::load_raw skips DllMain (useful when you want to instrument the module before any guest code runs); Guest::load_into / load_raw_into accept a caller-provided Sandbox so you can attach a Context, set an instruction budget, or seed coverage tracking before loading.
See cargo doc --open (the emulator module) for the full API.
How to read this repo
If you're skimming, read this README and skim docs/source-language.md — the directives in the example are the load-bearing idea.
If you want the full design and current state:
- docs/architecture.md — pipeline, crate roles, how the pieces fit.
- docs/round-trip-contract.md — what we promise to preserve and at which layer it's tested.
- docs/source-language.md — directive vocabulary with worked examples.
- docs/roadmap.md — what's shipped, what's in progress, what's still ahead.
- docs/installers.md — running Win32 installers under
ud analyze --monitor; how to extract an installer's payload + capture its install effects (files, registry).
License
MIT. Copyright (c) 2026 Karpeles Lab Inc. See LICENSE.