DEV Community

Cover image for How to View the IL of a .NET Assembly (and Why)
Zero Heartbeat
Zero Heartbeat

Posted on Edited on Originally published at delta1labs.com

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

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;
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)