If you have ever been handed a "precompiled" .NET assembly and assumed a decompiler would choke on it, ReadyToRun is the happy surprise. R2R is how .NET ships fast-starting code — the runtime libraries themselves are R2R, and PublishReadyToRun=true turns it on for your apps and container images — and it sounds like it should defeat decompilation, because it contains AOT-compiled native code. It does not. An R2R assembly decompiles to clean C# because the one thing R2R never throws away is the IL. Understanding why makes a whole category of "can I read this?" questions easy to answer.
What an R2R image actually contains
A normal .NET assembly is metadata + IL, JIT-compiled to native code on first execution. An R2R image is produced by crossgen2, which runs that JIT ahead of time and stores the resulting native code in the assembly — so the runtime can skip JITting on startup. Crucially, it stores the native code in addition to, not instead of, the original metadata and IL.
So the PE file contains three things that matter here: the ECMA-335 metadata tables (types, methods, signatures — untouched), the IL method bodies (also present), and a ReadyToRun section with precompiled native code plus the tables the runtime needs to use it. A ReadyToRunHeader, reachable through the CLI header's managed-native-header directory, ties it together and marks the image as R2R.
.b{fill:none;stroke:#64748b;stroke-width:1.5}.t{fill:currentColor;font-size:13px}.s{fill:#64748b;font-size:11px}.hl{stroke:#6366f1;stroke-width:2}.ar{stroke:#64748b;stroke-width:1.5;marker-end:url(#x)}
R2R assembly (one PE file):
Metadata + IL(intact — what you read)
ReadyToRun sectionprecompiled native code
R2R headermarks the image
Decompiler → reads IL → C#
The native section is the runtime's fast path; the decompiler ignores it.
Why the decompiler ignores the native side
A decompiler like Glass is an IL-to-C# reconstructor: it walks the metadata tables to recover types and signatures, then reads each method's IL body and lifts it back to C#. R2R's native code lives in a separate section with its own lookup tables, and the decompiler simply does not consult it — the IL is the authoritative, portable definition of every method, and it is still right there. So opening an R2R image is indistinguishable, from the decompiler's point of view, from opening a JIT-only assembly: same metadata, same IL, same C# out.
This is why you can decompile the .NET runtime libraries (which ship as R2R), a PublishReadyToRun app, or a crossgen'd container image without any special handling. The "it's precompiled" worry is about the native bytes, and those are not what a decompiler reads.
Recognizing R2R, and the one real exception
To confirm an assembly is R2R, look for the ReadyToRun header (tools label it "ReadyToRun" and often show an R2R version number), or simply know its origin: crossgen2 / PublishReadyToRun. The IL being present is the guarantee you can decompile it.
The exception that trips people up is NativeAOT, which sounds similar but is the opposite case. NativeAOT compiles the entire app to a self-contained native executable with no IL and no managed metadata to recover — there is nothing for an IL decompiler to read. The quick test: if a tool can load the file as a .NET assembly at all, it is managed (plain IL or R2R) and decompiles; if it is just a native binary, it is NativeAOT and you are looking at a native-disassembler job, not a decompilation one. (For how this affects your threat model as a vendor, see protecting .NET code from decompilation.)
Doing this with Glass
Glass opens ReadyToRun assemblies transparently — it reads the intact IL and reconstructs C#, ignoring the native R2R section, so the runtime libraries, a crossgen'd service, or a PublishReadyToRun app all read like any other assembly. It is the same workflow as decompiling a DLL to C#; R2R simply does not get in the way.
Glass is free — point it at an R2R image and see the IL come back as C#.
Top comments (0)