通用中间语言(Common Intermediate Language,简称CIL)(亦被称作MSIL或IL)是一种属于通用语言基础架构和.NET框架的低阶(lowest-level)的人类可读的编程语言,它是一种代码指令集。CIL可以在任何支持CLI(Common Language Infrastructure,通用语言基础架构)的环境中运行,.NET是微软对CLI的实现,Mono是Xamarin对CLI的又一实现,所以CIL能在.NET运行时以及Mono运行时中运行,跟具体的平台或者CPU无关,这样就无需为不同的平台部署不同的内容了。
如果想让一份代码在不同的平台中运行,只需要把代码的编译分为两部分即可:
1、把源代码编译成CIL的预编译过程(其实之后CIL还会被编译成一种位元码,生成一个CLI Assembly)
2、运行时把CIL(其实是CLI Assembly)编译成本地指令的即时编译过程
C#代码:
namespace ConsoleTest
{
class Program
{
// Methods
private static void Main(string[] args)
{
Console.WriteLine("Hello World");
Console.ReadKey();
}
}
}
CIL代码:
.class private auto ansi beforefieldinit ConsoleTest.Program
extends [mscorlib]System.Object
{
.method public hidebysig specialname rtspecialname
instance void .ctor() cil managed
{
// 代码大小 7 (0x7)
.maxstack 8
IL_0000: ldarg.0
IL_0001: call instance void [mscorlib]System.Object::.ctor()
IL_0006: ret
} // end of method Program::.ctor
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// 代码大小 19 (0x13)
.maxstack 8
IL_0000: nop
IL_0001: ldstr "Hello World"
IL_0006: call void [mscorlib]System.Console::WriteLine(string)
IL_000b: nop
IL_000c: call valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey()
IL_0011: pop
IL_0012: ret
} // end of method Program::Main
} // end of class ConsoleTest.Program
代码虽然简单,但也足够说明一些问题,下面让我们了解一下CIL代码:
1、以”.”一个点号开头的,例如上面这份代码中的.class、.method以及本例中未出现的.namespace,我们称之为CIL指令,用于描述.NET程序集总体结构的标记。为啥需要它呢?因为你总得告诉编译器你处理的是啥吧。
2、CIL代码中还出现了private、public,我们称之为CIL特性(attribute)。通过CIL指令并不能完全说明.NET成员和类,CIL特性的作用就是针对CIL指令进行补充说明成员或者类的特性。市面上常见的还有:extends、implements等等。
3、每一行CIL代码都使用了CIL操作码,例如:ldarg.0、ldstr、call、nop、ret等。
然后比较C#代码与对应的CIL 代码的不同之处:
1、C#代码Program类中没有构造方法,CIL代码中却有,说明编译器自动为类生成了无参构造方法。
2、为什么程序运行后总是执行Main()方法呢?因为Main()方法中设置了程序入口点(即.entrypoint),并不是因为它的名字是Main就得先执行……
3、经过对比发现:CIL代码比C#代码更接近程序的本质
CIL是基于堆栈的,也就是说CIL 的VM(mono运行时)是一个栈式机。这就意味着数据是先推入堆栈,然后通过堆栈来操作的,而不是通过CPU寄存器来操作,这更加验证了其和具体CPU架构没有关系。
CIL之所以是基于堆栈而非CPU的一个原因是相比较于CPU的寄存器,操作堆栈实在太简单了,只需要简单的压栈和弹出,这对于虚拟机的实现来说再合适不过了。如果想要更具体地了解CIL基于堆栈这一点,各位可以去看一下堆栈这方面的内容,这里就不拓展了。
就像C#一样,CIL同样是面向对象的。这就意味着在CIL中你可以创建对象,调用对象的方法,访问对象的成员。而这里需要注意的就是对方法的调用,调用静态方法和调用实例方法是不一样的。
举例说明,假设类Guo中有一个静态方法和一个实例方法,代码如下:
namespace ConsoleTest
{
public class Guo
{
public static int Subtract(int num1, int num2)
{
return num1 - num2;
}
public void Show(string message)
{
Console.WriteLine(message);
}
}
}
a. 静态方法Subtract的CIL代码如下:
.method public hidebysig static int32 Subtract(int32 num1,
int32 num2) cil managed
{
// 代码大小 9 (0x9)
.maxstack 2
.locals init ([0] int32 CS$1$0000)//初始化局部变量。由于本方法的返回值为int型,所以声明了一个int32类型的局部变量,用以保存返回值。
IL_0000: nop
IL_0001: ldarg.0 //将索引为0的参数加载到计算堆栈上
IL_0002: ldarg.1 //将索引为1的参数加载到计算堆栈上
IL_0003: sub //计算
IL_0004: stloc.0 //弹出当前计算堆栈顶弹部的值并将其存储到索引为0的局部变量中
IL_0005: br.s IL_0007
IL_0007: ldloc.0 //将索引为0的局部变量加载到计算堆栈上
IL_0008: ret //弹出当前计算堆栈顶弹部的值并返回
} // end of method Guo::Subtract
我们可以直接通过类名来调用这个静态方法:
Guo.Subtract(5,6);
对应的CIL代码为:
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// 代码大小 10 (0xa)
.maxstack 8
IL_0000: nop
IL_0001: ldc.i4.5
IL_0002: ldc.i4.6
IL_0003: call int32 ConsoleTest.Guo::Subtract(int32,
int32)
IL_0008: pop
IL_0009: ret
} // end of method Program::Main
可见CIL直接call了类ConsoleTest.Guo的Subtract方法,不需要加载类ConsoleTest.Guo的实例。
b. 实例方法Show的CIL代码如下
.method public hidebysig instance void Show(string message) cil managed
{
// 代码大小 9 (0x9)
.maxstack 8
IL_0000: nop
IL_0001: ldarg.1 //将索引为1的参数加载到计算堆栈上,静态方法中第一个参数的索引是0
IL_0002: call void [mscorlib]System.Console::WriteLine(string)
IL_0007: nop
IL_0008: ret
} // end of method Guo::Show
实例方法不能直接通过类名来调用,需要通过类的实例来调用:
Guo objGuo = new Guo();
objGuo.Show("Hello");
对应的CIL代码为:
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// 代码大小 20 (0x14)
.maxstack 2
.locals init ([0] class ConsoleTest.Guo objGuo)//声明一个ConsoleTest.Guo类型的局部变量,用以保存类的对象
IL_0000: nop
IL_0001: newobj instance void ConsoleTest.Guo::.ctor()
IL_0006: stloc.0
IL_0007: ldloc.0
IL_0008: ldstr "Hello"
IL_000d: callvirt instance void ConsoleTest.Guo::Show(string)
IL_0012: nop
IL_0013: ret
} // end of method Program::Main
CIL调用类ConsoleTest.Guo的Show方法时,需要先把类ConsoleTest.Guo的实例加载到计算堆栈(ldloc.0),然后再callvirt调用。由此也验证了:实例方法不能直接通过类名来调用,需要通过类的实例来调用。