6 min left
1248 words
6 minutes

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:

  1. Run the binary under Unicorn (or Iced, or whatever you have).
  2. Capture the routines you actually call.
  3. Lift them to C# with Alicorn.
  4. Compile and link the C# back into your project.
  5. Replace the Unicorn calls with the new C# calls.

How Alicorn replaces Unicorn on the hot path in four stages: emulate and observe with Unicorn, lift the x64 routine to a generated ICpuRuntime C# class, Roslyn-compile the C# into an in-memory assembly, then reuse the native CLR routine on every subsequent call — with a coverage-over-iterations chart converging after ~10 passes and a Unicorn-vs-Alicorn comparison table

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# class
var 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#

DimensionUnicornAlicorn
First-call costFastSlow (Roslyn warmup)
Hot-path speedEmulated (10–100x slower than native)Native CLR
Iteration loopNoneRequired (~10 passes)
Instruction coverageCompleteGrowing (no SSE/AVX/floats yet)
Drop-in replacementYes (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.

Series Navigation#

This article is part of the Projects series:

Beyond Unicorn: Why I Code-Gen Instead of Emulating
https://corentings.dev/blog/beyond-unicorn-alicorn-codegen/
Author
Corentin Giaufer Saubert
Published at
2026-07-10
License
CC BY-NC-SA 4.0
Share this post