当前位置: 首页 > 工具软件 > CIL > 使用案例 >

CIL之——初识CIL

傅正阳
2023-12-01

通用中间语言(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)编译成本地指令的即时编译过程

初始CIL代码

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中你可以创建对象,调用对象的方法,访问对象的成员。而这里需要注意的就是对方法的调用,调用静态方法和调用实例方法是不一样的。

  • 静态方法:ldarg.0没有被占用,所以参数从ldarg.0开始
  • 实例方法:ldarg.0被this占用,所以参数从ldarg.1开始

举例说明,假设类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调用。由此也验证了:实例方法不能直接通过类名来调用,需要通过类的实例来调用。

 类似资料: