Back to articles

Tag archive

#decompilation

D
Oct 22, 2026

Decompiling C# expression trees: the lambda that became a tree of factory calls

The same `x => x + 1` compiles two completely different ways depending on the type it is assigned to. As a delegate it is a hidden method and an ldftn; as an Expression<Func<int, int>> it is a nest of Expression.Parameter, Expression.Constant, Expression.Add and Expression.Lambda calls with members referenced by ldtoken. Here is exactly what the compiler emits for an expression tree — captured variables as a Constant of a closure, string equality with its op_Equality, nested lambdas wrapped in Quote — and how a decompiler folds the factory calls back into the lambda you wrote.

Oct 22, 202612 min read0 reactions0 comments
D
Oct 21, 2026

Decompiling C# string interpolation and interpolated string handlers

An interpolated string has no IL of its own — the compiler picks one of three completely different lowerings depending on your target framework, the hole types, and whether a method opted into a handler. Here is exactly what `$"..."` emits as string.Concat, as string.Format, and as the DefaultInterpolatedStringHandler dance, plus the custom-handler case where the string is never built at all, and where a decompiler can fold each one back to `$"..."`.

Oct 21, 20269 min read0 reactions0 comments
D
Oct 14, 2026

Decompiling foreach: the enumerator, the hidden try/finally, and when it is really a for loop

There is no foreach in IL. The C# compiler lowers it to one of three completely different shapes depending on what you iterate — a call to GetEnumerator with a while loop and a hidden try/finally that disposes the enumerator, a struct-enumerator version with no dispose at all, or, over an array or span, a plain index-counted for loop with no enumerator in sight. A decompiler sees only the lowered code and has to recognize which pattern it is looking at to print foreach back instead of the machinery. This post reads the real IL for each case, shows why the Dispose lives in a finally the source never wrote, why the struct enumerator skips it, and how Glass.NET matches the shape — the GetEnumerator/MoveNext/get_Current trio, the try/finally, the IDisposable check — to recover the loop you actually wrote.

Oct 14, 20267 min read0 reactions0 comments
D
Oct 14, 2026

Decompiling control flow: how Glass.NET rebuilds loops and if/else from a pile of branches

IL has no while, no for, no else — it has basic blocks and branch instructions: br, brtrue, brfalse, switch. A for loop and a goto-spaghetti rewrite of the same loop can compile to identical IL, so a decompiler cannot read the structure back; it has to reconstruct it. This post walks the real pipeline Glass.NET runs: split the method into basic blocks at every branch and branch target, build a control-flow graph, compute dominators to find which block guards which, detect natural loops from back edges to the block that dominates them, and structure the forward branches into if/else — then explains the honest hard case, irreducible control flow, where no clean loop exists and a decompiler must either duplicate code or fall back to labels and goto.

Oct 14, 202610 min read0 reactions0 comments
D
Oct 13, 2026

Decompiling anonymous types: the generated generic class behind new { a, b }

An anonymous type has no name you wrote, but it is a real class in the metadata — a generic one the compiler synthesizes, named something like <>f__AnonymousType0`2, with a readonly property per member, a constructor that takes them all, and overridden Equals, GetHashCode and ToString. The trick that makes it reusable is that the generated class is generic over its property types, so two new { } sites with the same property names in the same order share one backing type instantiated with different type arguments. This post reads the generated class and the newobj call site in IL, shows where the property names actually live (on the generated type’s properties, not on the <Name>j__TPar type parameters), walks the structural Equals and the hash-combine GetHashCode the compiler writes, and explains how Glass.NET recognizes the pattern and folds it back into new { Name = ..., Total = ... } instead of leaving you an unspeakable generic type.

Oct 13, 20267 min read0 reactions0 comments
D
Oct 12, 2026

Decompiling operator overloads: op_ names, checked operators, and the conversions that only compile as operators

A user-defined operator is a static method with a reserved name and the specialname flag — op_Addition, op_Implicit, op_Equality — and mapping it back to an operator declaration looks like a table lookup. Three things make it harder than the table. User-defined conversions may be overloaded on return type alone, which is legal IL and illegal C#: two op_Explicit methods with identical parameters differ only in what they return, so a decompiler that emits them as ordinary methods produces source that cannot compile. C# 11 added checked operators — op_CheckedAddition, op_CheckedExplicit — that must be rendered with the checked keyword and paired with their unchecked twin, or every checked(a + b) call site decompiles to a method call. And op_True/op_False, op_Increment and the C# 14 instance compound-assignment operators each compile to call patterns that have to be folded back into syntax rather than shown as invocations. This post reads the IL for each, shows the call sites that drive the reconstruction, and walks how Glass.NET turns them back into the operators the author wrote.

Oct 12, 202610 min read0 reactions1 comments
D
Oct 12, 2026

Decompiling ref fields and scoped: how C# 11 ref structs look in IL

A ref field is the first time a C# field stores a reference rather than a value, and it compiles to metadata that earlier decompilers never had to read. The field''s signature carries a byref element type — ELEMENT_TYPE_BYREF inside a FIELD signature, which was illegal before .NET 7 — and the ref-safety rules that keep it from outliving what it points at are encoded in modreq and attributes, not in IL you can execute. scoped is the loudest of those: it is a ScopedRefAttribute on a parameter, with no runtime effect at all, that a decompiler either surfaces as the scoped keyword or drops, quietly changing the method''s ref-safety contract. This post reads the raw signatures and attribute blobs for a ref field, a ref struct that holds one, and a scoped ref parameter, shows where the ref escape rules live in metadata, and walks through how Glass.NET reconstructs ref, ref readonly, scoped and the [UnscopedRef] escape hatch so the decompiled source compiles with the same safety the author wrote.

Oct 12, 20268 min read0 reactions0 comments
D
Oct 11, 2026

Decompiling init-only setters and required members: the modreq and attributes that make them

init and required are two of the smallest-looking C# keywords and two of the easiest for a decompiler to lose. An init accessor is just a set accessor whose signature carries a required modifier — modreq(IsExternalInit) — and a decompiler that does not look at the modifier shows it as an ordinary set, quietly handing you a type whose properties can be reassigned after construction. required is worse: it is three separate attributes working together — RequiredMemberAttribute on each member, RequiredMemberAttribute on the type, and CompilerFeatureRequired on the type — plus a SetsRequiredMembers on any constructor that satisfies them, and a decompiler that drops them produces code whose callers are no longer forced to set anything. This post reads the IL and the metadata for both features, shows the exact byte-level shape of the modreq and the attribute blobs, walks through how Glass.NET reconstructs init, required, and [SetsRequiredMembers], and explains why skipping the step silently changes a type''s public contract.

Oct 11, 20267 min read0 reactions0 comments
D
Oct 11, 2026

Decompiling nullable reference types: where string? actually lives in the metadata

string and string? are the same type to the runtime — System.String, one TypeRef, no difference in any signature. The question mark exists only at compile time, and the compiler has to smuggle it into the assembly somewhere so that consumers of your library get the right warnings. It does that with two embedded attributes, NullableAttribute and NullableContextAttribute, a three-value byte vocabulary, a preorder flattening of generic type trees into byte arrays, and an aggressive compression scheme that omits whatever matches the enclosing default. This post reads those bytes directly from the IL, shows the exact blob encoding for the single-byte and array forms, walks through how a decompiler reconstructs ?, notnull and class? from them, and explains why skipping this step silently changes a library''s public contract.

Oct 11, 202610 min read0 reactions0 comments