Beyond Unicorn: Why I Code-Gen Instead of Emulating
Beyond Unicorn
When a binary does something interesting, you usually have three options: reverse it manually, call it as a black box through P/Invoke, or emulate it instruction-by-instruction with something like Unicorn Engine.
Alicorn is the fourth option I keep coming back to: lift the routine to C# and run it natively.
The first option is slow. The second is fine until the binary doesn’t want to be called from outside. Emulation works for the rest.
TL;DR — Alicorn lifts x64 routines to native C# so the second call is 10–100x faster than Unicorn. After ~10 iterations, coverage converges and Unicorn falls out of the hot path.
Unicorn is a fantastic tool: portable, scriptable, well-documented, with bindings for nearly every language you’d want to use. I wrote the C# bindings for Unicorn myself, so I know how complete they are. They get you in the door.
But emulation has a runtime cost.
The Cost of Emulating x64
Every instruction Unicorn runs goes through a software CPU: instruction decode, micro-op dispatch, register state, memory translation, interrupt handling. For a 50-instruction crypto routine, that’s nothing. For something larger, it hits a performance ceiling.
You can measure the ceiling. A simple XOR loop running under Unicorn is 10x to 100x slower than native. SHA-256 is in the same range. AES, depending on mode and key schedule, can do better — but the moment you add a wrapper that does non-trivial work, you’re back to double-digit slowdowns.
Emulation works for exploration. For repeated calls, it adds too much overhead.
A popular extraction game ships with a runtime crypto routine that runs on every payload. Most people in the community lift it with Ghidra, understand it, then call it via Unicorn from tooling. That works, but every call pays the full emulation tax.
The Code-Gen Loop
Alicorn started as a transpiler — x64 to C#, lifted into the CLR. The original question was “what if you dissolved the native/managed boundary?”
The runtime question is sharper: what if you don’t emulate at all?
Here’s the shape:
- Run the binary under Unicorn (or Iced, or whatever you have).
- Capture the routines you actually call.
- Lift them to C# with Alicorn.
- Compile and link the C# back into your project.
- Replace the Unicorn calls with the new C# calls.

The first time you do it, maybe 30% of the routine lifts cleanly. The rest falls back to Unicorn. You profile, you see what’s missing, you improve the lifter.
Run it again. 60% is native.
Run it again. 85%.
After about ten iterations, coverage converges. The remaining gaps are usually weird edge cases — exotic flag combinations, instructions you’ve never seen, memory models you haven’t modeled yet. They’re a tiny fraction of the runtime cost.
The code that started as emulated bytes now runs at CLR speed. Unicorn sits cold in the background, only touched when a path is genuinely new.
Why This Works
The assumption behind the loop: you’re calling the same routines repeatedly with similar inputs. If that’s true, the amortized cost of code-gen drops to nearly zero after a few iterations.
This holds for most workloads that matter:
- Crypto routines (TLS handshakes, payload encryption, license validation)
- Serialization formats
- Custom VMs in DRM systems
- Any binary function called in a tight loop
The pattern is consistent: a binary function you call repeatedly.
Replacing Unicorn with Alicorn
If you’ve already written code against Unicorn — and many people have — the swap is small. The C# bindings expose the API you’d expect: hooks, memory maps, register access, single-step execution.
A typical Unicorn call, using UnicornNet:
using UnicornNet;
var unicorn = new Unicorn(Unicorn.Architecture.X86, Unicorn.Mode.Mode64);// ... map memory, write code, set up stack ...var hook = unicorn.AddCodeHook((engine, address, size, state) =>{ // inspect instructions as they execute});unicorn.EmuStart(entry, until);The Alicorn version of the same thing:
var transpiler = new AlicornTranspiler();var nativeMethod = transpiler.Lift(binary, entryPoint);// nativeMethod is a generated C# classvar result = nativeMethod.Call(args);Same contract, faster mechanism. A routine that took 80ms under emulation now takes 4ms native. Multiply that by the number of times you call it per session, and the workflow that used to feel sluggish becomes interactive.
That’s the idea for anyone already using Unicorn for hot-path work: keep your bindings, keep your workflow, but route the hot routines through Alicorn. The emulator stays for everything else.
Where This Is Going: Roslyn JIT
The current loop forces a recompile between iterations. Stop the process, generate the C#, run dotnet build, restart. It’s fast, but it’s a wait.
The next step removes it.
The idea is to use Roslyn — Microsoft’s .NET compiler platform — at runtime to emit IL for the missing pieces. Instead of writing a .cs file and recompiling, you build a SyntaxTree, compile it with CSharpCompilation, and load the resulting assembly with AssemblyLoadContext:
var compilation = CSharpCompilation.Create("AlicornJIT") .WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)) .AddReferences(MetadataReference.CreateFromFile(typeof(object).Assembly.Location)) .AddSyntaxTrees(SyntaxFactory.ParseSyntaxTree(sourceCode));
using var ms = new MemoryStream();var result = compilation.Emit(ms);ms.Seek(0, SeekOrigin.Begin);var assembly = AssemblyLoadContext.Default.LoadFromStream(ms);
var method = assembly.GetType("Alicorn.Generated")?.GetMethod("Execute");method?.Invoke(null, new object[] { args });When Alicorn hits a code path it hasn’t lifted yet, it generates the C# on the spot, compiles it with Roslyn, and runs it. The first call is still slow — Roslyn has its own startup cost — but the second call, and every call after, is native.
Tooling round-trips and cold starts disappear. Code is generated where it’s needed and executed when it’s called.
I’m still in the early stages here. Roslyn’s API is vast and the integration points aren’t obvious yet. The gap between “I have to restart” and zero-touch iteration is closing with each release.
Unicorn vs Alicorn
| Dimension | Unicorn | Alicorn |
|---|---|---|
| First-call cost | Fast | Slow (Roslyn warmup) |
| Hot-path speed | Emulated (10–100x slower than native) | Native CLR |
| Iteration loop | None | Required (~10 passes) |
| Instruction coverage | Complete | Growing (no SSE/AVX/floats yet) |
| Drop-in replacement | — | Yes (same call shape) |
The Honest Limits
I haven’t fully implemented floating point, SSE/AVX, system calls, or complex memory layouts yet. The iteration loop helps, but it doesn’t replace instruction coverage.
The Roslyn JIT approach has its own cost. Compilation is expensive, and emitting code at runtime means you’re paying a one-time penalty per new path. If your workload is wildly diverse — every call takes a unique path — the savings don’t materialize. The loop assumes some locality. The trade is amortized across many calls, not paid per call.
Still, for any use case where you’re calling a binary repeatedly — and that’s most of them — this is the right shape of the problem. Emulation belongs in research. Code-gen takes over on the hot path.
The Future I’m Building Toward
Alicorn’s reasoning is simple: if you’re going to call a routine thousands of times, the routine should run at full speed.
I’m working toward:
- Expanding instruction coverage (every release)
- Tighter Roslyn integration for the JIT path
- A clean drop-in for existing Unicorn code
- Eventually, a runtime that lifts, compiles, and runs without ever leaving the process
I’m not there yet. But each iteration builds on the last. Each pass gets faster, each path gets covered, each recompile gets shorter.
The question behind Alicorn has been the same one since the start: where does the boundary between native and managed actually live? Wherever we decide to put it. Alicorn keeps moving that boundary, one routine at a time.
Related
- Building a Native x64-to-C# Transpiler — the previous article in this series, on what Alicorn does
- Why I Built ZaString — another C# performance experiment
- Go Pipeline Pattern — how I think about transforming data in stages
Series Navigation
This article is part of the Projects series:
- Previous: Building a Native x64-to-C# Transpiler
- Series: Projects
Related Posts
Building a Native x64-to-C# Transpiler: Why I Built Alicorn
Building a native x64-to-C# transpiler sounds simple until you meet the RFLAGS register. The question behind Alicorn, and what the gap keeps teaching me.
Why I Built ZaString
On zero allocations, Span<T>, and the pursuit of performance without sacrifice.
How to Merge PGN Files in F#: Streaming, Performance, and Discriminated Unions
How I built a CLI tool to merge chess PGN files using F#'s type system, streaming I/O, and functional patterns — merging gigabytes of games with 64 KB of memory.