CIL is a full-fledget .NET programming language, with its own syntax, semantics, and compiler (ildasm.exe).
When you build a .NET assembly using your managed language of choice (C#,VB,F#) the associated compiler translates your source code into terms of CIL.
For ex:
Building dynamic assemblies using the System.Reflection.Emit namespace. This API allows you to generate an in-memory .NET assembly, which can optionally be persisted to disk. This is a useful technique for the tool builders of the world who need to generate assemblies on the fly.
Examining CIL Directives, Attributes, and OpCodes
The token set understood by the CIL compiler is subdivided into the following 3 bread categories based on semantics.
- CIL Directives
- CIL Attributes
- CIL Operation Codes (OpCodes)
The Role of CIL Directives
There is a set of well-known CIL tokens that are used to describe the overall structure of a .NET assembly. These tokens are called directives. CIL directives are used to inform the CIL compiler how to define the namespace(s), type(s) and member(s) that will populate an assembly.
Directives are represented syntactically using a single (.) dot prefix (e.g.".namespace",".class",".publickeytoken",".method",".assembly" etc.). Thus if your *.il file (the conventional extension for a file containing CIL code) has a single .namespace directive and 3 .class directives, the CIL compiler will generate an assembly that defines a single .NET Core namespace containing 3 .NET class types.
The Role of CIL Attributes
Many CIL directives can be further specified with various CIL attributes to qualify how a directive should be processed. For ex, the .class direcrive can be adorned with the public attribute (to establish the type visibility), the extends attribute (to explicitly specify the type's bas class), and the implements attribute (to list the set of interfaces supported by the type).
The Role of CIL Opcodes
Once a .NET assembly namespace and type set have been defined in terms of CIL using various directives and related attributes, the final remaining task is to provide the type's implementation logic. This is a job for operation codes, or simply opcodes.
For ex, if you need to load a string variable into memory, variable into memory, you do not use a friendly opcode names LoadString but rather "ldstr".
The CIL OpCode/CIL Mnemonic Distinction
Tokens such as ldstr are CIL Mnemonic for the actual binary CIL opcodes.
int Add(int x,int y)
{
return x + y;
}
The act of adding 2 numbers is expressed in terms of the CIL opcode 0x58. In a similary vein, substracting 2 numbers is expressed 0x59, and the fact of allocating a new object on the managed heap 0x73.
For each binary opcode of CIL, there is a corresponding mnemonic. For ex, the "add" mnemonic can be used rather than 0x58, "sub" rather than 0x59, and "newobj" rather than 0x73.
Look at ildasm.exe
Pushing and Popping : The Stack-Base Nature of CIL
CIL developers do not use an object of type Stack<T> to load and unload the values to be evaluated; however the same publishing and popping mindset still applies.
Formally speaking, the entity used to hold a set of values to be evaluated is termed the "virtual execution stack". CIL provides several opcodes that are used to push a value onto the stack; this process is termed loading. CIL defines additional opcodes that transfer the topmost value on the stack into memory (such as a local variable) using a process termed storing.
void PrintMessage()
{
string myMessage = "Hello.";
Console.WriteLine(myMessage);
}
Check CIL;
- PrintMessage() method defines a storage slot for a local variable using ".locals" directive.
- The local string is then loaded and stored in this local variable using the "ldstr(load string" and "stloc.0" opcodes (which can be read as "store the current value in a local variable at storage slot zero").
The value (again, at index 0) is then loaded into memory using the ldloc.0("local the local argument at index 0) opcode for use by the System.Console.WriteLine() method invocation (specified using the call opcode. Finally, the function returns via the "ret" opcode.)
Note: CIL supports code comments using the double-syntax (as well as /* ... */ syntax). As in C#, code comments are completely ignored by the CIL compiler.
Understanding Round-Trip Engineering
Once you have the CIL code at your disposal, you are free to edit and recompile the code base using the CIL compiler, ilasm.exe. This technique is termed round-trip engineering.
Console.WriteLine("Hello CIL code");
Console.ReadLine(); // dotnet build
ildasm.exe /all /METADATA /out=.\RoundTrip\RoundTrip.il RoundTrip.dllNote: ildasm.exe will also generate a *.res file when dumping the contents of an assembly to file.
- View the RoundTrip.il in Visual Studio
It's critical to understand that when interacting with .NET Core types (such as System.Console) in CIL; you will always need to use the type's fully qualified name.
IL_0005: call void [System.Console]System.Console::WriteLine(string)
The Role of CIL Code Labels
One thing you certainly have noticed is that each line of implementation code is prefixed with a token of the form IL_XXXX (e.g. IL_0000: , IL_0001, etc). Those tokens are called code labels and may be named in any manner you choose.
IL_0005: call void [System.Console]System.Console::WriteLine(string)
Interacting with CIL: Modifying an *.il File
The goal here is quite simple: change the message that is output to the console. To make the change, you need to alter the current implementation of the top-level statements, created as the <Main>$ method. Locate this method within the *.il file and change the message to "Hello from altered CIL Code!"
Compiling CIL Code with ILASM.exe
ilasm.exe /DLL RoundTrip.il /x64To execute the program, use the CLI:
dotnet RoundTrip.il
Compiling CIL Code with Microsoft.NET.SDK.il Projects
Create a project: Microsoft.NET.Sdk.IL and create a global.json file:
{
"msbuild-sdks": {
"Microsoft.NET.Sdk.IL": "6.0.0"
}}
Create a file named RoundTrip.ilproj and add this:
<Project Sdk="Microsoft.NET.Sdk.IL">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<MicrosoftNetCoreIlasmVersion>6.0.0</MicrosoftNetCoreIlasmVersion>
<ProduceReferenceAssembly>false</ProduceReferenceAssembly>
</PropertyGroup>
</Project>
dotnet build
Understanding CIL Directives and Attributes
Specifying Externally Referenced Assemblies in CIL
- Create a directory named CILTypes, and the global.json and CILTypes.ilproj.
- Create CILTypes.il for external assemblies.
.assembly extern System.Runtime
{
.publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A )
.ver 6:0:0:0
}
.assembly extern System.Runtime.Extensions
{
.publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A )
.ver 6:0:0:0
}
.assembly extern mscorlib
{
.publickeytoken = (B7 7A 5C 56 19 34 E0 89)
.ver 6:0:0:0
}
Defining the Current Assembly in CIL
The next order of business is to define the assembly you are interested in building using the ".assembly" directive.
.assembly CILType{}
Update your assembly definition to include a version number of 1..0.0.0 using the ".ver" directive:
// Our assembly
.assembly CILType{
.ver 1:0:0:0
}
Given that the CILTypes assembly is a single file assembly, you will finish up the assembly definition using the following single ".module" directive, which marks the official name of your .NET binary, CILTypes.dll
// Our assembly
.assembly CILType{
.ver 1:0:0:0
}
// The module of our single-file assembly.
.module CILTypes.dll
Directive
- .mresources = If your assembly uses internal resources, this directive is used to identify the name of the file that contains the resources to be embedded.
- .subsystem = This CIL directive is used to establish the preferred UI that the assembly wants to execute within.
Defining Namespaces in CIL
You can create a .NET Core namespace (MyNamespace) using the .namespace directive.
// Our assembly has a single namespace
.namespace MyNamespace{}
Like C#, CIL namespace definitions can be nested within further namespaces.
.namespace MyCompany{
.namespace MyNamespace{
}
} CIL allows you to define a nested namespace.
.namespace MyCompany.MyNamespace{
}
Defining Class Types in CIL
The ".class" directive is used to define a new class.
.namespace MyNamespace{
// System.Object base class assumed.
.class public MyBaseClass{}
}
When you are building a class type that derives from any class other than System.Object, you use "extends" attribute.
// This will not compile
.namespace MyNamespace{
// System.Object base class assumed.
.class public MyBaseClass{}
.class public MyDerivedClass extends MyBaseClass{}
}
You must specify the full name of MyBaseClass;
.class public MyDerivedClass extends MyNamespace.MyBaseClass{}
Various attributes used on conjunction with the ".class" directive.
| public,private,nested assembly,nested famandassem, | nested public, nested family, nested famorassem, nested private | CIL defines various attributes that are used to specify the visiblity of a given type. |
| abstract,sealed | These 2 attributes may be tacked onto a .class directive to define an abstract class or sealed class respectively. | |
| auto,sequential,explicit | These attributes are used to instruct the CLR how to lay out field data in memory. For class types, the default layout flag (auto) is appropriate. Changing this default can be helpful if you need to use P/Invoke to call into unmanaged C code. | |
| extends,implements | These attributes allow you to define the base class of a type (via extends) or implement an interface on a type (via implements). |
Defining and Implementing Interfaces in CIL
.namespace MyNamespace{
// An interface defition
.class public interface IMyInterface{}
// A simple base class
.class public MyBaseClass {}
// MyDerivedClass now implements IMyInterface and extends MyBaseClass
.class public MyDerivedClass extends MyNamespace.MyBaseClass implements MyNamespace.IMyInterface {}
}
The "extends" attribute cannot be used to derive interface A from interface B.
.class public interface IMyInterface{}
.class public interface IMyOtherInterface implements MyNamespace.IMyInterface{}
Defining Structures in CIL
The ".class" directive can be used to define a CTS Structure if the type System.ValueType. As well, the .class directive must be qualified with the "sealed" attribute.
// A structure definition is always sealed.
.class public sealed MyStruct extends [System.Runtime]System.ValueType{}
// Shorthand notation for declaring a structure.
.class public sealed value MyStruct{}
The value attribute, the new type will derive from [System.Runtime]System.ValueType automatically.
Defining Enums in CIL
When you want to define an enum in terms of CIL, simply extend [System.Runtime]System.Enum
// An Enum
.class public sealed MyEnum extends {System.Runtime]System.Enum{}
// Enum shorthand
.class public sealed enum MyEnum {}
Defining Generics in CIL
In terms of CIL, the number of type parameters is specified using a backward-leaning single tick (`), followed by a numerical value representing the number of type parameters.
// In C#: List<int> myInts = new List<int>();
newobj instance void class [System.Collections]System.Collections.Generic.List`1<int32>::.ctor()
// In C#: Dictionary<string, int> d = new Dictionary<string, int>();
newobj instance void class [System.Collections]
System.Collections.Generic.Dictionary`2<string,int32>
::.ctor()
Compiling the CILTypes.il File
You are able to compile a *.il file into a .NET Core DLL assembly:
dotnet build