C# code has a high level of abstraction. The intermediate language has a lower level—it is a higher level than machine code, but harder to read than C#.
The IL has instructions that manipulate the stack. These instructions, called intermediate language opcodes, reside in a relational database called the metadata.
First, we examine a C# program and the intermediate language generated from the C# code with the C# compiler. The C# increment statement and its IL is highlighted.
L_0000
, you can see that the constant number zero is pushed onto the evaluation stack through the instruction ldc.WriteLine
is called, the location of the local variable is pushed to the evaluation stack. This is used as an argument.L_0011
implements another instruction that returns control to the beginning of the loop if the condition is met.using System; class Program { static void Main() { int i = 0; while (i < 10) { Console.WriteLine(i); i++; } } }.method private hidebysig static void Main() cil managed { .entrypoint .maxstack 2 .locals init ( [0] int32 num) L_0000: ldc.i4.0 L_0001: stloc.0 L_0002: br.s L_000e L_0004: ldloc.0 L_0005: call void [mscorlib]System.Console::WriteLine(int32) L_000a: ldloc.0 L_000b: ldc.i4.1 L_000c: add L_000d: stloc.0 L_000e: ldloc.0 L_000f: ldc.i4.s 10 L_0011: blt.s L_0004 L_0013: ret }
If
-statements are translated into branch instructions. One such intermediate language instruction, bne, stands for branch if not equal.
A()
. If its argument is equal to 1, it writes something to the console.int
) are not equal, we jump forward to line IL_000f
.using System; class Program { static void Main() { A(0); A(1); A(2); } static void A(int value) { if (value == 1) { Console.WriteLine("Hi"); } else { Console.WriteLine("Bye"); } } }Bye Hi Bye.method private hidebysig static void A(int32 'value') cil managed { // Code size 26 (0x1a) .maxstack 8 IL_0000: ldarg.0 IL_0001: ldc.i4.1 IL_0002: bne.un.s IL_000f IL_0004: ldstr "Hi" IL_0009: call void [mscorlib]System.Console::WriteLine(string) IL_000e: ret IL_000f: ldstr "Bye" IL_0014: call void [mscorlib]System.Console::WriteLine(string) IL_0019: ret } // end of method Program::A
A call instruction invokes a static
or Shared method. We learn about the call instruction in the .NET Framework's intermediate language.
Main
method, we create an instance of Program
and call its instance method A. Finally we call the static
method B.class Program { void A() { } static void B() { } static void Main() { Program p = new Program(); p.A(); Program.B(); } }.method private hidebysig static void Main() cil managed { .entrypoint // Code size 18 (0x12) .maxstack 1 .locals init ([0] class Program p) IL_0000: newobj instance void Program::.ctor() IL_0005: stloc.0 IL_0006: ldloc.0 IL_0007: callvirt instance void Program::A() IL_000c: call void Program::B() IL_0011: ret } // end of method Program::Main
This is an intermediate language instruction. It is used on instance methods. The call instruction meanwhile is used on static
methods.
class Test { public int M() { return 0; } } class Program { static void Main() { Test t = new Test(); int i = t.M(); } }.method private hidebysig static void Main() cil managed { .entrypoint .maxstack 1 .locals init ( [0] class Test t) L_0000: newobj instance void Test::.ctor() L_0005: stloc.0 L_0006: ldloc.0 L_0007: callvirt instance int32 Test::M() L_000c: pop L_000d: ret }class Test { static public int M() { return 0; } } class Program { static void Main() { int i = Test.M(); } }.method private hidebysig static void Main() cil managed { .entrypoint .maxstack 8 L_0000: call int32 Test::M() L_0005: pop L_0006: ret }
This instruction is common. It places an argument onto the evaluation stack. There are several common variants of ldarg in the .NET Framework.
ldarg.0
and ldarg.1
instructions.ldarg.0
pushes an int32
onto the evaluation stack, and ldarg.1
pushes a string
onto the evaluation stack.static int Something(int a, string b) { a += 1; int y = b.Length + a; return y; }.method private hidebysig static int32 Something(int32 a, string b) cil managed { // Code size 16 (0x10) .maxstack 2 .locals init ([0] int32 y) IL_0000: ldarg.0 IL_0001: ldc.i4.1 IL_0002: add IL_0003: starg.s a IL_0005: ldarg.1 IL_0006: callvirt instance int32 [mscorlib]System.String::get_Length() IL_000b: ldarg.0 IL_000c: add IL_000d: stloc.0 IL_000e: ldloc.0 IL_000f: ret } // end of method Program::Something
With the ldc instruction, the .NET Framework loads constant values (such as integers) onto the evaluation stack, making many program constructs possible.
Console.WriteLine
.ldc.i4.0
and ldc.i4.8
are used.using System; class Program { static void Main() { Console.WriteLine(0); Console.WriteLine(8); Console.WriteLine(9); Console.WriteLine(-1); Console.WriteLine('a'); Console.WriteLine(1.23); } }.method private hidebysig static void Main() cil managed { .entrypoint // Code size 47 (0x2f) .maxstack 8 IL_0000: ldc.i4.0 IL_0001: call void [mscorlib]System.Console::WriteLine(int32) IL_0006: ldc.i4.8 IL_0007: call void [mscorlib]System.Console::WriteLine(int32) IL_000c: ldc.i4.s 9 IL_000e: call void [mscorlib]System.Console::WriteLine(int32) IL_0013: ldc.i4.m1 IL_0014: call void [mscorlib]System.Console::WriteLine(int32) IL_0019: ldc.i4.s 97 IL_001b: call void [mscorlib]System.Console::WriteLine(char) IL_0020: ldc.r8 1.23 IL_0029: call void [mscorlib]System.Console::WriteLine(float64) IL_002e: ret } // end of method Program::Main
How are local variables used in the intermediate language? In IL locals are allocated separately and accessed by indexes with the ldloc instruction.
static int Example() { int a = 1; string y = "cat"; bool b = false; int result = a + y.Length + (b ? 1 : 0); return result; }.method private hidebysig static int32 Example() cil managed { // Code size 29 (0x1d) .maxstack 3 .locals init ([0] int32 a, [1] string y, [2] bool b, [3] int32 result) IL_0000: ldc.i4.1 IL_0001: stloc.0 IL_0002: ldstr "cat" IL_0007: stloc.1 IL_0008: ldc.i4.0 IL_0009: stloc.2 IL_000a: ldloc.0 IL_000b: ldloc.1 IL_000c: callvirt instance int32 [mscorlib]System.String::get_Length() IL_0011: add IL_0012: ldloc.2 IL_0013: brtrue.s IL_0018 IL_0015: ldc.i4.0 IL_0016: br.s IL_0019 IL_0018: ldc.i4.1 IL_0019: add IL_001a: stloc.3 IL_001b: ldloc.3 IL_001c: ret } // end of method Program::Example
In addition to storing values in static
fields, there exists an intermediate language instruction to load values from them.
static
field _a to 3. This is done with stsfld.ldc.i4.4
instruction.using System; class Program { static int _a; static void Main() { Program._a = 3; if (Program._a == 4) { Console.WriteLine(true); } } }.method private hidebysig static void Main() cil managed { .entrypoint // Code size 21 (0x15) .maxstack 8 IL_0000: ldc.i4.3 IL_0001: stsfld int32 Program::_a IL_0006: ldsfld int32 Program::_a IL_000b: ldc.i4.4 IL_000c: bne.un.s IL_0014 IL_000e: ldc.i4.1 IL_000f: call void [mscorlib]System.Console::WriteLine(bool) IL_0014: ret } // end of method Program::Main
The newarr instruction creates a vector. This is a one-dimensional array. Vectors have special support in the .NET Framework.
int
array of ten elements. The first element is initialized so the compiler does not eliminate any code.using System; class Program { static void Main() { int[] array = new int[10]; array[0] = 1; } }.method private hidebysig static void Main() cil managed { .entrypoint // Code size 13 (0xd) .maxstack 3 .locals init ([0] int32[] 'array') IL_0000: ldc.i4.s 10 IL_0002: newarr [mscorlib]System.Int32 IL_0007: stloc.0 IL_0008: ldloc.0 IL_0009: ldc.i4.0 IL_000a: ldc.i4.1 IL_000b: stelem.i4 IL_000c: ret } // end of method Program::Main
Every method executed has a ret instruction. This returns control from the method to the calling method. The instruction sometimes returns a value.
ldarg.0
. Finally, the ret instruction appears at the end of the method.static int Method(int value) { return 2 + value; }.method private hidebysig static int32 Method(int32 'value') cil managed { // Code size 4 (0x4) .maxstack 8 IL_0000: ldc.i4.2 IL_0001: ldarg.0 IL_0002: add IL_0003: ret } // end of method Program::Method
This stores a value inside an array element. Its performance depends on the type of the element and array. We test stelem in some C# programs.
class Program { static void Main() { char[] c = new char[100]; c[0] = 'f'; } }.method private hidebysig static void Main() cil managed { .entrypoint .maxstack 3 .locals init ( [0] char[] c) L_0000: ldc.i4.s 100 L_0002: newarr char L_0007: stloc.0 L_0008: ldloc.0 L_0009: ldc.i4.0 L_000a: ldc.i4.s 0x66 L_000c: stelem.i2 L_000d: ret }class Program { static void Main() { int[] i = new int[100]; i[0] = 5; } }.method private hidebysig static void Main() cil managed { .entrypoint .maxstack 3 .locals init ( [0] int32[] i) L_0000: ldc.i4.s 100 L_0002: newarr int32 L_0007: stloc.0 L_0008: ldloc.0 L_0009: ldc.i4.0 L_000a: ldc.i4.5 L_000b: stelem.i4 L_000c: ret }
What is the stsfld instruction? This stores the value on top of the evaluation stack into a static
field. We demonstrate this instruction.
ldc.i4.3
) onto the top of the evaluation stack.using System; class Program { static int _a; static void Main() { Program._a = 3; if (Program._a == 4) { Console.WriteLine(true); } } }.method private hidebysig static void Main() cil managed { .entrypoint // Code size 21 (0x15) .maxstack 8 IL_0000: ldc.i4.3 IL_0001: stsfld int32 Program::_a IL_0006: ldsfld int32 Program::_a IL_000b: ldc.i4.4 IL_000c: bne.un.s IL_0014 IL_000e: ldc.i4.1 IL_000f: call void [mscorlib]System.Console::WriteLine(bool) IL_0014: ret } // end of method Program::Main
The switch
instruction is implemented with a jump table. The switch
keyword—in the C# language—is typically compiled to a switch
instruction.
switch
(IL_0020
, IL_0027
, IL_002e
).switch
jumps to locations.using System; class Program { static void Main() { int i = int.Parse("1"); switch (i) { case 0: Console.WriteLine(0); break; case 1: Console.WriteLine(1); break; case 2: Console.WriteLine(2); break; } } }.method private hidebysig static void Main() cil managed { .entrypoint // Code size 53 (0x35) .maxstack 1 .locals init ([0] int32 i, [1] int32 CS$0$0000) IL_0000: ldstr "1" IL_0005: call int32 [mscorlib]System.Int32::Parse(string) IL_000a: stloc.0 IL_000b: ldloc.0 IL_000c: stloc.1 IL_000d: ldloc.1 IL_000e: switch ( IL_0020, IL_0027, IL_002e) IL_001f: ret IL_0020: ldc.i4.0 IL_0021: call void [mscorlib]System.Console::WriteLine(int32) IL_0026: ret IL_0027: ldc.i4.1 IL_0028: call void [mscorlib]System.Console::WriteLine(int32) IL_002d: ret IL_002e: ldc.i4.2 IL_002f: call void [mscorlib]System.Console::WriteLine(int32) IL_0034: ret } // end of method Program::Main
The intermediate language is not needed every day. It usually goes without notice. But it opens up a new way of understanding what C# code does.