Skip to content
← All posts
· Delta1 Labs Decompiler.NETIL

How to View the IL of a .NET Assembly (and Why)

How to view the IL of a .NET assembly, what CIL actually is, and when reading IL beats decompiled C# — with a small, correct C#-to-IL worked example.

Most of the time, when you open an assembly you did not write, you want readable C#. A decompiler gives you that, and it is the right default. But every so often the decompiled C# does something that makes you squint — a cast that should not be there, a call you did not expect, output that reads oddly but is “technically correct.” That is the moment to stop reading the reconstruction and look at what the assembly actually contains: the IL.

This post is about the layer underneath the C#: what IL is, how to view it, and — more usefully — when reading IL is genuinely better than reading decompiled C#. I will keep the theory short and finish with a small, correct worked example you can reproduce.

TL;DR: IL is the real instruction set inside every managed DLL; decompiled C# is a reconstruction of it. To view IL, open the assembly in a tool like Glass.NET and flip any member from its C# view to its IL view (or use the SDK’s ildasm.exe). Read IL when the C# surprises you, when you need to see hidden behavior like boxing, or when the C# will not reconstruct cleanly.

What IL actually is

When you build a C# project, the compiler does not emit machine code. It emits Common Intermediate Language — CIL, often just called IL (and historically MSIL). IL is a compact, stack-based instruction set: instead of registers, most operations push and pop values on an evaluation stack. Alongside the IL, the compiler writes a rich metadata table describing every type, method, field and parameter by name and signature. Both live inside the .dll or .exe.

Native code is not produced until runtime. When a method is first called, the JIT (just-in-time) compiler turns its IL into machine instructions for the current CPU. This is why the same managed DLL runs on different architectures, and — relevant here — why the assembly carries a complete, readable description of your program. A decompiler reconstructs C# from that IL and metadata; an IL viewer just shows you the IL directly, with no reconstruction step in between. There is more background in how to decompile a .NET DLL to C#.

How to view IL

You have a few options, from friendliest to most bare-metal:

  • A decompiler with an IL view. The most convenient path. Open the assembly, select a method, and switch that member from its C# view to its IL view. In Glass.NET this is a per-member toggle, so you can read the reconstructed C# and the underlying IL of the same method side by side without leaving the tool. No source or PDB required.
  • ildasm.exe. The IL Disassembler ships with the .NET SDK / Windows SDK. It is the classic, no-frills way to dump an assembly’s IL and metadata to a window or a text file. Great for scripting and for a canonical dump.
  • monodis / other disassemblers. Cross-platform alternatives exist if you are not on the Microsoft toolchain.

For interactive work — jump to a method, read its C#, then confirm against its IL — a decompiler view is by far the fastest, because you are one keystroke from the C# you were already reading.

When reading IL beats reading C#

Decompiled C# is easier to read, so why ever drop to IL? Because the C# is a reconstruction, and there are cases where the reconstruction hides or reshapes something you need to see:

  • The C# looks surprising. A decompiler’s pattern-matching occasionally produces valid C# that reads oddly. The IL tells you what the method literally does, so you can confirm the behavior instead of second-guessing the decompiler.
  • Hidden behavior. Boxing, implicit conversions, string concatenation lowered to String.Concat, the exact target of a virtual vs non-virtual call — these are invisible or ambiguous in C# but explicit in IL.
  • Evaluation order and short-circuiting. When you need to know precisely what is evaluated and in what order, IL is unambiguous.
  • The C# will not reconstruct cleanly. Heavily optimized, obfuscated, or unusual assemblies sometimes defeat clean decompilation. The IL is still fully readable even when the C# is not.
  • Learning how the compiler works. If you want to understand how a language feature is lowered — how yield, async, lock, or a switch expression become IL — reading the IL is the whole point.

For everything else — understanding logic, tracing a bug, recovering source — decompiled C# is the better tool. IL is the scalpel you reach for when the C# is not enough.

A small worked example

Take the most boring method imaginable:

public int Add(int a, int b)
{
    return a + b;
}

In a release build with no locals, its IL is almost a one-to-one map of the stack machine:

.method public hidebysig instance int32 Add(int32 a, int32 b) cil managed
{
  .maxstack 2
  ldarg.1   // push a
  ldarg.2   // push b
  add       // pop both, push a + b
  ret       // return the top of the stack
}

ldarg.1 and ldarg.2 push the two arguments (ldarg.0 is this on an instance method), add pops them and pushes the sum, and ret returns it. Nothing surprising — which is exactly the point: for simple code, IL just confirms what the C# says.

Now watch IL reveal something the C# hides. This line:

object o = 42;

reads as a trivial assignment. But object is a reference type and 42 is an int (a value type), so the runtime has to box it — allocate a heap object to hold the value. The C# does not show that; the IL does:

ldc.i4.s   42          // push the constant 42
box        [System.Runtime]System.Int32   // box the int into an object
stloc.0                // store into local 'o'

That box instruction is a heap allocation you would never spot in the source. In a hot loop, spotting it in the IL is the difference between “why is this allocating?” and an actual answer. This is the everyday value of an IL view — it makes the invisible visible.

Honest limits

An IL view is a reading tool, not a magic decoder:

  • IL is lower-level, so it is slower to read. A method that is three lines of C# can be a dozen IL instructions. Use it selectively.
  • It is still static. Viewing IL does not run the code. If you need to watch values at runtime, that is a live debugger’s job — dnSpyEx for managed debugging.
  • Metadata names still apply. Obfuscated assemblies have obfuscated names in the IL too; the instructions are readable, but the identifiers may not be meaningful.
  • Only analyze what you have the right to. Reading the IL of your own binaries or dependencies you may inspect is routine; third-party software can be restricted by its license.

See the IL for yourself

If you have ever wanted to confirm what a method actually does — not what the decompiler thinks it does — an IL view is the answer. Glass.NET is a free .NET decompiler that shows you both: read the reconstructed C#, then flip any member to its IL in a keystroke. No license, no seats, no sign-up. Download it, open an assembly, and switch to the IL view. For the fuller reading workflow, see how to debug a .NET app when you don’t have the source.

Try Nebula.NET

Harden your .NET code in minutes — start with the free edition.