Introduction
CIL, the Common Intermediate Language (also called MSIL or just IL), is the instruction set of the .NET runtime. Compilers for C#, F#,
Visual Basic and other languages do not produce machine code; they produce CIL, and the runtime turns it into machine code when the program runs (or
ahead of time). The CIL of a program is stored in method bodies inside a .NET assembly: each
MethodDef row of the metadata gives the RVA of one body. The instruction set is defined by Ecma International’s
ECMA-335, partition III; the layout of a method body is in
partition II (section 25.4).
CIL is a stack machine. An instruction takes its inputs from an evaluation stack and leaves its result there: ldarg.0 pushes
the first argument, ldc.i4.1 pushes the number 1, add pops two numbers and pushes their sum, stloc.0 pops a value into
the first local variable. Instructions that name something outside the method (a method to call, a field, a type, a string literal) carry a
metadata token: a four-byte number whose top byte says which metadata table and whose low three bytes say which row. A string literal is
such a token into the #US heap (table number 0x70), which has no table of its own.
This is the format of the bytes, not a decompiler: the pages say what each instruction is and does, and the explorer decodes every body instruction by instruction. It does not turn the instructions back into C# and it names compiler habits only where a specification says so.
Structure of a method body
Headertiny (1 byte: code size in the upper six bits) or fat (12 bytes: flags, max stack, code size, local variable signature token)method bodiesCodethe instructions, back to back: an opcode of one or two bytes, then its operandcode size bytesPaddingzero bytes up to the next 4-byte boundary (only when sections follow)0 to 3 bytesExtra data sectionsthe exception clauses: try ranges with their catch, filter, finally or fault handlerssections
How an instruction is encoded
An instruction is its opcode followed by its operand. Most opcodes are one byte (0x00 to 0xE0). The rest are two
bytes: a first byte 0xFE and a second byte, so ceq is FE 01 and ldftn is FE 06. The bytes
0xF8 to 0xFF are reserved (prefix7 to prefix1 and prefixref in the runtime’s own opcode table; 0xFE
is the real prefix); no runtime accepts the others. Numbers in operands are little-endian. The operand kinds are:
| Operand kind | Size | What it holds | Used by |
|---|---|---|---|
InlineNone | 0 | nothing | most instructions |
ShortInlineVar | 1 | an argument or local variable number, 0 to 255 | the .s forms: ldarg.s, stloc.s … |
InlineVar | 2 | an argument or local variable number | ldarg, ldloc, stloc … (two-byte opcodes) |
ShortInlineI | 1 | a signed byte (ldc.i4.s); for unaligned. and no. an unsigned byte | ldc.i4.s, unaligned., no. |
InlineI | 4 | a signed 32-bit integer | ldc.i4 |
InlineI8 | 8 | a signed 64-bit integer | ldc.i8 |
ShortInlineR | 4 | an IEEE 754 single | ldc.r4 |
InlineR | 8 | an IEEE 754 double | ldc.r8 |
InlineString | 4 | a token with table number 0x70: an offset into the #US heap | ldstr |
InlineField | 4 | a Field or MemberRef token | ldfld, stfld, ldsfld … |
InlineMethod | 4 | a MethodDef, MemberRef or MethodSpec token | call, callvirt, newobj, ldftn … |
InlineType | 4 | a TypeDef, TypeRef or TypeSpec token | castclass, box, newarr, initobj … |
InlineTok | 4 | a field, method or type token | ldtoken |
InlineSig | 4 | a StandAloneSig token: a method signature | calli |
ShortInlineBrTarget | 1 | a signed offset from the end of this instruction to the target | the .s branches, leave.s |
InlineBrTarget | 4 | the same, 32 bits | the long branches, leave |
InlineSwitch | 4 + 4n | a count n, then n signed 32-bit offsets, each from the end of the whole switch instruction | switch |
A branch target is the offset of the first byte of another instruction in the same body, counted from the start of the code. Tools write it as a label
IL_0012, the offset in hex. A target in the middle of an instruction, or outside the code, is an error the runtime rejects; the explorer reports it
and goes on.
Six instructions are prefixes: tail., volatile., unaligned., constrained., readonly. and
no. change how the next instruction works and are written before it; they are separate instructions in the bytes and one unit in the
listing. See prefixes.
The evaluation stack
The stack holds values of a few kinds only: int32, int64, native int, a floating-point value F (single or
double widened to the machine’s internal format), an object reference O, a managed pointer &, and value types. Smaller
integers (int8, uint16, bool, char …) are widened to int32 when loaded and narrowed when stored.
Every instruction has a fixed effect on it, which is why the opcode table records how many items an instruction pops and pushes (calls and ret are the
exceptions: they depend on a signature). The header’s max stack says how deep the stack gets. At a branch target the stack must hold the same
types whichever way control arrived; the instruction pages show each instruction’s effect as before → after.
Verifiability. The specification calls code verifiable when a verifier can prove it type-safe without trusting it. Each page notes, where
the specification says so, instructions that are not verifiable (raw pointers, localloc, cpblk …). The .NET runtime of today does not run a
verifier on ordinary code; the notes describe the specification, not a check the explorer makes.
The reference pages
- Method bodies – tiny and fat headers, flags, max stack, code size, the local variable signature, and the small and fat exception sections.
- Opcode map – all 0x00 to 0xFF and the 0xFE-prefixed opcodes in two grids, each cell linking to its entry.
- Loads and stores – arguments, locals, constants, indirect loads and stores, block copy and fill,
dupandpop. - Arithmetic and conversion – add, subtract, multiply, divide, logic, shifts, comparisons, and every
convform. - Branches and switch – the short and long conditional and unconditional branches and the jump table.
- Calls and returns –
call,callvirt,calli,newobj,ret,jmp, function pointers and the variable argument list. - Fields, types and boxing – field access, casts, boxing,
ldtoken,sizeofand typed references. - Arrays –
newarr,ldlen, and every element load and store. - Exceptions –
throw,rethrow,leave,endfinallyandendfilter. - Prefixes – the instructions that modify the next one.
- Other instructions –
nop,break,locallocand the reserved bytes.
Each instruction entry gives the encoding, the operand, the stack before and after, what the instruction does, the exceptions it can throw and, where the specification says so, a note on verifiability. The descriptions are written for this site from the specification, not copied from it.
Versions
The set of instructions is the same in every .NET runtime to the extent that matters for reading a file: an instruction decoded from a 2002 assembly and from a
2026 one has the same bytes and the same effect. What changed is what surrounds it. Generics arrived with the 2.0 runtime and the second edition of ECMA-335; the
instructions that belong to them (constrained. and readonly., and the type-operand forms ldelem, stelem and
unbox.any) are described together with generics in the specification. no. is defined by the specification’s partition III but has
no entry in the runtime library’s opcode table (System.Reflection.Emit.OpCodes); the explorer decodes it. The pages do not say which runtime
version first accepted which opcode, because that was not checked against old runtimes.
In the explorer
Open a .dll or .exe that is a .NET assembly and use Read as → .NET assembly (or the Managed code (.NET) child of the
CLR header in the PE view). The tree then shows each method body as one node: opened, it holds the header, the IL code with one node per instruction (a prefix
grouped with the instruction it modifies, each instruction split into opcode and operand) and the exception clauses. The IL listing section is an
ildasm-style text of the whole assembly. The options bar controls it: IL offsets on or off, tokens resolved to names or left as numbers, branch targets as
IL_xxxx labels or as distances, and the stack effect of each instruction. A damaged body (running past the end of the file, an opcode that does not
exist, a branch into the middle of an instruction, a clause out of range) is shown as far as it can be read and marked with an issue, never refused.
How it was checked: the opcode table against every field of System.Reflection.Emit.OpCodes; every method body of the 3,198 assemblies in the
.NET SDK against Microsoft’s System.Reflection.Metadata (header, instructions, operands, clauses); the fixtures and a sample of SDK assemblies against
ildasm; and hand-written IL assembled with ilasm and decoded again. See the not-described list below for what these checks do not cover.
Not described
Known, not yet covered
- Stack verification: the viewer shows each instruction’s pops and pushes from the opcode table but does not simulate the stack, so it does not check max stack, stack depth at branch targets or types.
- Debug information: sequence points and local names from a portable PDB are not joined to the instructions.
- Native code in the image: methods implemented in native code (C++/CLI) and ReadyToRun precompiled code are left as bytes; only IL is decoded.
- Per-runtime acceptance: which runtime version accepts which opcode, and the verification rules each applies, were not checked.
Unknown
- How obfuscators and other tools that deliberately break the rules (opcodes that exist only for one tool, bodies that overlap) are read by each runtime: the explorer reports what it finds and goes on.
Links
Software
Developers
- System.Reflection.Emit.OpCodes – the runtime library’s table of every opcode (.NET)
- System.Reflection.Metadata MethodBodyBlock – reads method bodies, exception regions and local signatures (.NET)
- Mono.Cecil – library that reads and rewrites IL (.NET)
Reference
- ECMA-335 – the Common Language Infrastructure standard: partition III is the instruction set, partition II section 25 the method bodies
- ECMA-335 augments – the runtime repository’s corrections and additions to the standard
- .NET assembly file format – the metadata the tokens in the instructions point into