FlowingDev

.NET Assemblies, explained: the X-ray vision for your compiled C# code

Uncover the secrets of .NET DLLs and EXEs by diving into intermediate language (IL), metadata, and the art of decompilation.

Try the tool: .NET Assembly Inspector

In one sentence

A .NET Assembly Inspector is a digital autopsy table for compiled applications, letting you rip open a DLL or EXE to reverse-engineer its original C# source code and explore the low-level instructions that make it tick.

The problem it solves

To understand why this tool exists, we have to rewind to the early 2000s. When Microsoft introduced the .NET Framework, they fundamentally changed how Windows code was compiled. Instead of taking your C# (or VB.NET) code and turning it directly into machine code tied to a specific processor architecture—the way C++ does—.NET compilers translate your code into a halfway state.

This middle-ground format is called CIL (Common Intermediate Language, historically called MSIL). It gets packaged up with an enormous amount of descriptive metadata into a .dll or .exe file known as an Assembly. Only at the very last second, right as you run the app, does the .NET runtime (the CLR) step in and "Just-In-Time" (JIT) compile that CIL into raw machine code.

This design was brilliant. It meant you could write code once and theoretically run it anywhere the .NET runtime lived. But it created a fascinating side effect: .NET Assemblies are extremely chatty. Because the JIT compiler needs to know everything about the code to run it safely, the Assembly contains nearly all the structural information of your original source code. The classes, the methods, the properties, the generic types—it’s all preserved perfectly.

The problem? Sometimes a DLL acts like a black box. You might inherit a legacy application where the source code was lost in a hard drive crash. You might be using a closed-source third-party library that keeps throwing a NullReferenceException with terrible documentation. Or, as a developer, you might just want to know what kind of messy, convoluted logic the C# compiler is hiding beneath neat "syntactic sugar" like async/await or LINQ.

Traditionally, breaking open these black boxes required downloading heavy desktop reverse-engineering tools like Lutz Roeder's legendary .NET Reflector, ILSpy, or dotPeek. Today, modern wizardry allows us to parse, inspect, and fully decompile these complex binary structures entirely in the browser.

How it works under the hood

Opening a .NET Assembly isn't magic; it's just parsing a very well-documented file format. Let’s crack open the binary anatomy of a DLL.

The PE/COFF Shell

If you inspect a .NET DLL with a hex editor, you might be surprised to find it looks exactly like a standard Windows C++ executable. It starts with the letters MZ (the DOS header) and contains a Portable Executable (PE) header.

This is a clever disguise. To make sure Windows knows how to load them, .NET assemblies are wrapped in a standard Windows executable shell. However, the PE header contains a special pointer—the CLR Header (Common Language Runtime Header). When Windows sees this header, it stops treating the file like a normal program and hands control over to the .NET runtime. Everything past this header is pure .NET architecture.

Metadata Tables and Tokens

A .NET assembly is essentially a miniature relational database. It relies on a series of Metadata Tables to describe everything inside it. There is a TypeDef table for your classes, a MethodDef table for your functions, and a MemberRef table for functions you call from other DLLs.

Every single element is assigned a Metadata Token—a 32-bit integer that acts as a unique ID.

  • A token starting with 0x02 means it’s a Type (e.g., 0x02000004).
  • A token starting with 0x06 means it’s a Method (e.g., 0x0600000C).

When you browse an assembly, the inspector is reading these tables, joining them together (figuring out that Method 0x0600000C belongs to Type 0x02000004), and painting a tree view of the architecture.

Intermediate Language (IL)

Inside those methods lives the actual executable logic, written in Common Intermediate Language (IL). IL is a stack-based assembly language. Instead of using CPU registers, it pushes values onto a virtual "evaluation stack" and pops them off to perform operations.

If you write a simple C# function to add two numbers:

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

The compiler generates IL that looks something like this:

.method public hidebysig instance int32 Add(int32 a, int32 b) cil managed {
  .maxstack 2
  IL_0000: ldarg.1    // Push argument 'a' onto the stack
  IL_0001: ldarg.2    // Push argument 'b' onto the stack
  IL_0002: add        // Pop two values, add them, push the result
  IL_0003: ret        // Return the value at the top of the stack
}

An assembly inspector parses these raw bytecodes (like 0x03 for ldarg.1 and 0x58 for add) and translates them into this readable text.

The Art of Decompilation

Reading IL is great for hardcore optimization, but most devs want to see C#. Decompilation is the act of running the compilation process in reverse.

The tool reads the IL stack instructions and maps them back into an Abstract Syntax Tree (AST). It notices patterns: "Ah, this code initializes an enumerator, loops until MoveNext() is false, and uses a try/finally block to dispose it. That's a foreach loop!" It transforms the low-level IL concepts back into high-level C# constructs, giving you an almost exact replica of the original source code.

Real-world stories

The Case of the Vanished Source Code

A junior developer built a critical internal billing utility five years ago. They eventually left the company, and due to a migration error, the Git repository holding the source code was permanently deleted. Fast forward to today: the utility is breaking because a third-party API it talks to updated its URL.

Instead of spending three weeks rewriting the tool from scratch, the lead developer drags the BillingUtility.exe into an assembly inspector. Within seconds, the code is fully decompiled. They locate the hardcoded URL, copy the decompiled C# into a fresh Visual Studio project, fix the URL, recompile, and push to production by lunchtime. Because .NET preserves metadata so well, the compiled EXE essentially served as an emergency backup of the source code.

The Third-Party Black Box

An enterprise team is paying for an expensive, closed-source PDF generation library. In production, a specific function keeps throwing an ArgumentOutOfRangeException. The vendor's support team is unresponsive, and the official documentation is completely blank on this specific method.

Frustrated, a developer opens the vendor's DLL in an inspector, navigates to the crashing method, and looks at the decompiled C#. They discover a hidden, undocumented internal check: if the PDF title string is longer than 50 characters, the library throws an exception. Armed with this knowledge, the developer truncates their strings before passing them to the library, instantly bypassing the crash without having to wait weeks for a patch.

The Syntactic Sugar Betrayal

A performance-obsessed backend dev decides to use C#'s fancy new yield return feature to create an elegant sequence generator. It looks clean, but the application is suddenly suffering from severe garbage collection lag.

The developer compiles the code and inspects the resulting DLL. To their horror, they discover that their simple four-line method didn't actually compile to four lines of IL. Under the hood, the C# compiler generated a massive, hidden state-machine class implementing IEnumerator, allocating memory on the heap every single time the method was called. Seeing the raw IL broke the illusion of the syntactic sugar, prompting the dev to rewrite the function using a simpler, zero-allocation span approach.

Common mistakes and traps

  • Forgetting about Obfuscation: You might pop open a DLL hoping to read beautiful C#, only to find methods named a, b, and c, filled with unreadable spaghetti code. This is intentionally done by tools called Obfuscators, which scramble the metadata and IL to protect intellectual property. An inspector will accurately show you the obfuscated mess, but it can't un-scramble it.
  • Assuming decompiled code is the literal original: Decompilers make highly educated guesses. If you wrote a for (;;) loop, the decompiler might spit out a while (true) loop. The logic and execution are identical, but the text might differ slightly from what the original author typed.
  • Panicking over "Compiler Generated" anomalies: When inspecting code, you will routinely see bizarre classes with names like <>c__DisplayClass0_0. Don't panic. This is how the C# compiler handles things like closures, lambda expressions, and async state machines behind the scenes. They are compiler-generated artifacts, not malware.
  • Inspecting Debug builds instead of Release builds: If you decompile a DLL compiled in "Debug" mode, you will see a ton of nop (No Operation) instructions in the IL. These exist purely so debuggers have places to attach breakpoints. To see what the code actually does in production, always inspect the "Release" build where optimizations are enabled.

Why it belongs on your radar

Every serious .NET developer reaches a point where they need to peek behind the curtain. An assembly inspector transitions you from a developer who just writes C# into an engineer who understands how C# is actually executed.

It breaks the illusion of "magic" features, helping you understand the real performance costs of LINQ, async programming, and pattern matching. It serves as an ultimate parachute when you are tasked with maintaining undocumented, source-less legacy binaries. Most importantly, it empowers you to verify dependencies: you never have to blindly trust a third-party NuGet package again when you have the tools to audit its raw, compiled logic directly in your browser.

Go deeper

Theory done. Time to get your hands dirty — 100% in your browser.

Try the tool: .NET Assembly Inspector