.NET Base Class Library, C# , and CIL Data Type Mappings

Note: The System.IntPtr and System.UIntPtr types map to "native int" and "native unsigned int".

Defining Type Members in CIL

Defining Field Data in CIL

Enumerations, structures and classes can all support field data. In each case, the ".field" directive will be used.

.class public class sealed enum MyEnum{
         .field public static literal valuetype MyNamespace.MyEnum A = int32(0)
         .field public static literal valuetype MyNamespace.MyEnum B = int32(1)
         .field public static literal valuetype MyNamespace.MyEnum C = int32(2)
 }

static and literal values set up the field data to be a fixed value accessible from the type itself.

Note: The values assigned to an enum value may also be in hexadecimal with a 0x prefix.

When you want to define a point of field data within a class or structure, you are not limited to a point of public static literal date. You could update MyBaseClass to support 2 points of private, instance-level field data, set to default values.

.class public MyBaseClass{
         .field private string stringField = "Hello"
         .field private int32 intField = int32(42)
 }

Defining Type Constructors in CIL

In terms of CIL, instance-level constructors are represented using the ".ctor" token, while a static-level construct is expressed via ".cctor" (class constructor). Both CIL tokens must be qualified using the rtspecialname (return type special name) and specialname attributes. These attributes are used to identify a specific CIL token that can be treated in unique ways by a given .NET language. For ex, in C#, constructors do not define a return type; however, in terms of CIL, the return value of a constructor is indeed void.

.class public MyBaseClass{
         .field private string stringField
         .field private int32 intField
         .method public hidebysig specialname rtspcialname instance void .ctor(string s, int32 i) cil managed{
                 //TO DO : Add implementation code
         }
 }

Defining Properties in CIL

Properties and methods have specific CIL representations. If MyBaseClass were updated to support a public property named TheString;

.class public MyBaseClass
 {
 ...
 .method public hidebysig specialname
 instance string get_TheString() cil managed
 {
 // TODO: Add implementation code...
 }
 .method public hidebysig specialname
 instance void set_TheString(string 'value') cil managed
 {
 // TODO: Add implementation code...
 }
 .property instance string TheString()
 {
 .get instance string
 MyNamespace.MyBaseClass::get_TheString()
 .set instance void MyNamespace.MyBaseClass::set_TheString(string)
 }
 }

Defining Member Parameters

public static void MyMethod(int inputInt, ref int refInt, ArrayList ar, out int outputInt) { }

If you were to map this method into CIL terms, you would find that C# reference parameters are marked with an ampersand (&) suffixed to the parameter's underlying data type (int32&).

Output parameters also we the & suffix, but they are rare further qualified using the CIL[out] token.

.method public hidebysig static void MyMethod(int32 inputInt,
 int32& refInt,
 class [System.Runtime.Extensions]System.Collections.ArrayList ar,
 [out] int32& outputInt) cil managed
 {
 ...
 }

Examining CIL Opcodes

  1. Opcodes that control program flow.
  2. Opcodes that evaluate expressions
  3. Opcodes that access values in memory (via parameters, local variables, etc.)

Opcodes

  1. add,sub,mul,div,rem -> These CIL opcodes allow you to add,subtract,multiply and divide 2 values (rem = modulo)
  2. and,or,not,xor -> These CIL opcodes allow you to perform bit-wise operations on 2 values..
  3. ceq,cgt,clt : These CIL opcodes allow you to compare 2 values on the stack in various manners.

ceq : Compare for equality.

cgt : Compare for greater than

clt : Compare for less than

  1. box,unbox : These CIL opcodes are used to convert between reference types and value types.
  2. Ret -> This CIL opcode is used to exit a method and return a value to the caller.
  3. beg,bgt,ble,blt,switch -> These CIL opcodes (in addition to many other related opcodes) are used to control branching logic within a method. All the branch-centric opcodes require that you specify a CIL code label to jump to if the result of the test is true. Here are some examples:
  4. beg : Break to code label if equal
  5. bgt : Break to code label if greater than
  6. ble : Break to code label if less than or equal to
  7. blt : Break to code label if less than.
  8. Call : This CIL opcode is used to call a member on agiven type.
  9. newarr,newobj : These CIL opcodes allow you to allocate a new array or new object type into memory (respectively).
  10. ldarg (with numerous variations) : Loads a method's arguments onto the stack. In addition to the general ldarg (which works in conjunction with a given index that identifies the argument), there are numerous other variations. For ex, ldarg opcodes that have a numerical suffix (ldarg.0) hard-code which argument to load. As well, variations of the ldarg opcode allow you to hard-code the data type using the CIL constant notation (ldarg_I4 for int32) as well as the data type and value (ldarg_I4_5, to load an int32 with the value of 5)
  11. ldc : Loads a constant value onto the stack.
  12. ldfld : Loads the value of an instance-level field onto the stack.
  13. ldloc : Loads the value of a local variable onto the stack.
  14. ldobj : Obtains all the values gathered by a heap-based object and places them object and places them on the stack.
  15. ldstr : Loads a string value onto stack.

Various Pop-Centric Opcodes

  1. Pop : Removes the value currently on top of the evaluation stack but does not bother to store the value.
  2. Starg : Stores the value on top of the stack into the method argument at a specified index.
  3. stloc : Pops the current value from the top of the evaluation stack and stores it in a local variable list at a specified index.
  4. Stobj : Copies a value of a specified type from the evaluation stack into a supplied memory address.
  5. Stsfld : Replaces the value of a static field with a value from the evaluation stack.

Various CIL opcodes will implicitly pop values off the stack to perform the task at hand.

The .maxstack Directive

.maxstack establishes the maximum number of variables that may be pushed onto the stack at any given time during the execution of the method.

.maxstack directive has a default value (8)
 
.method public hidebysig instance void Speak() cild managed {
         // During the scope of this method, exactly 1 value (the string literal) is on the stack.
         .maxstack 1
         ldstr "Hello"
         call void [mscorlib]System.Console::WriteLine(string)
         ret
 }

Declaring Local Variables in CIL

The first step taken to allocate local variables in new CIL is to use the ".locals" directive, which is paired with the "init" attribute.

public static void MyLocalVariables()
 {
     string myStr = "CIL code is fun!";
     int myInt = 33;
     object myObj = new object();
 }
 
.method public hidebysig static void MyLocalVariables() cil managed
{
.maxstack 8
// Define three local variables.
.locals init (string myStr, int32 myInt, object myObj)
// Load a string onto the virtual execution stack.
ldstr "CIL code is fun!"
// Pop off current value and store in local variable [0].
stloc.0
// Load a constant of type "i4"
// (shorthand for int32) set to the value 33.
ldc.i4.s 33
// Pop off current value and store in local variable [1].
stloc.1
// Create a new object and place on stack.
newobj instance void [mscorlib]System.Object::.ctor()
// Pop off current value and store in local variable [2].
stloc.2
ret
}

Mapping Parameters to Local Variables in CIL

public static int Add(int a,int b)
 {
     return a + b;
 }
 
.method public hidebysig static int32 Add(int32 a,int32 b) cil managed
{
.maxstack 2
ldarg.0 // Load "a" onto the stack.
ldarg.1 // Load "b" onto the stack.
add // Add both values.
ret
}

The Hidden this Reference

One thing to be mindful of when you are examining or authoring CIL code is that every nonstatic method that takes incoming arguments automatically receives an implicit additional parameter, which is a reference to the current object (like the C# tihs keyword).

// No longed static
public int Add(int a,int b)
 {
     return a + b;
 }

Then the incoming a and b arguments are loaded using ldarg.1 and ldarg.2 (rather than the expected ldarg.0 and lrdarg.1 opcodes). Again, the reason is that slot 0 contains the implicit this reference.

// This is JUST pseudocode!
.method public hidebysig static int32 AddTwoIntParams(
MyClass_HiddenThisPointer this, int32 a, int32 b) cil managed
{
ldarg.0 // Load MyClass_HiddenThisPointer onto the stack.
ldarg.1 // Load "a" onto the stack.
ldarg.2 // Load "b" onto the stack.
...
}

Representing Iteration Constructs in CIL

public static void CountToTen()
 {
     for (int i = 0; i < 10; i++)
     {
     }
 }
 
.method public hidebysig static void CountToTen() cil managed
{
.maxstack 2
.locals init (int32 V_0, bool V_1)
IL_0000: ldc.i4.0 // Load this value onto the stack.
IL_0001: stloc.0 // Store this value at index "0".
IL_0002: br.s IL_0007 // Jump to IL_0008.
IL_0003: ldloc.0 // Load value of variable at index 0.
IL_0004: ldc.i4.1 // Load the value "1" on the stack.
IL_0005: add // Add current value on the stack at index 0.
IL_0006: stloc.0
IL_0007: ldloc.0 // Load value at index "0".
IL_0008: ldc.i4.s 10 // Load value of "10" onto the stack.
IL_0009: clt // check less than value on the stack
IL_000a: stloc.1 // Store result at index "1"
IL_000b: ldloc.1 // Load value at index "1"
IL_000c: brtrue.s IL_0003 // if true jump back to IL_0003
IL_000d: ret
}