C#/.NET JIT和IL(MSIL或CIL)实现跨平台

  • 内容
  • 评论
  • 相关

所有 .NET 支持的语言编写出来的程序,在对应的编译器编译之后,会先产出程序集,其主要内容是中间语言 IL 和元数据。

之后,JIT 再将 IL 翻译为机器码(不同机器实现方式不同)。

IL 使得跨平台成为可能,并且统一了各个框架语言编译之后的形式,使得框架实现的代价大大降低了。

比如,.NET 框架有N种语言,那么每种语言都必须有自己的编译器。而 .NET 框架又决定跨 M 种平台,那么,就需要有 M 种 JIT。

如果不存在 IL,则 .NET 框架为了支持 N 种语言跨 M 种平台,需要 MxN 个编译器。

但如果所有 .NET 框架的 N 种语言经过编译之后,都变成相同的形式,那么只需要 M+N 个编译器就可以了。因此,IL 大大降低了跨平台的代价。

什么是 IL(CIL)

在 .NET 的开发过程中,IL 的官方术语是 MSIL 或 CIL(Common Intermediate Language, 公共中间语言)。

因此,IL、MSIL 和 CIL 指的是同一种东西,我们统一使用 IL 进行指代。

使用不同语言(例如 C# 和 VB)经过不同编译器(例如 C# 编译器和 VB 编译器),编译一段功能相似的代码(区别仅仅在于语法),其 IL 也基本相似。

可以通过反编译工具加载任意的 .NET 程序集并分析它的内容,包括它所包含的 IL 代码和元数据,以及反编译之后的 C# 代码。

在 C# 没有开源之前,这项技能是开发者进阶的必备技能,这是因为有些性质是必须通过查看 IL 或反编译才能得知的,例如装箱和拆箱发生了多少次(box 是 IL 指令),using 的本质实际上是一个 try-finally 块,闭包和委托涉及密封类、迭代器和状态机等等。

另外,也可以自己书写 IL 代码,然后使用 .NET 自带的 ilasm. exe 编译为程序集。

初识 IL

IL 虽然比 C# 低级一些,但它实际上也拥有很多助记符和指令,这些指令使得 IL 的可读性没有想象中那么差。

IL 中的关键字可以分为三类:指令、特性和操作码。

IL 指令在语法上使用一个点前缀来表示。

在 ILSpy 中,IL 指令是绿色的。这些指令用来描述代码文件的结构,包括 .namespace.class、.method、.field、.property等等。例如,如果你的代码文件包括了三个 .class,这意味着 C# 源代码包含三个类型。

下面的 IL 代码中 包括四个字段和一个方法:

// Fields
.field private object '<>2_current'
.field private int32 '<>1_state '
.field public class DataStructureLab.People '<>4_this'
.field public int32 '<i>5_1'
// Methods
.method private final hidebysig newslot virtual
instance bool MoveNext () cil managed
{

IL 特性和 IL 指令一起修饰成员。例如,一个 .class 可以被 public 修饰,指定它的可见性,也可以被 extend 修饰,指定它的父类,也可以被 static 或 instance 修饰指定它是静态还是实例成员等等。

IL 操作码提供了可以在 IL 上实现的各种操作。

真正的操作码是无法一眼理解的二进制数据(例如相加的操作码是 0x58),但是,每个操作码都对应一个助记符(例如相加的助记符是 add),助记符的长度设计得较短,这使它们有时会让人难以理解,例如创建一个新的字 符串,需要使用 Idstr 助记符。

1) IL 以栈为基础

IL 实际上是完全以栈为基础的。IL 提供了将变量压入虚拟执行栈中(称为加载,这会使栈的成员增加 1)的操作码,然后,也提供了将栈顶的值拿出来移动到内存中(称为存储,这会使栈的成员减少 1 )的操作码。

初始化局部变量时,必须将它加载入栈,然后再弹岀来赋给本地变量。

因此,初始化完局部变量之后,栈应当是空的。当使用局部变量时,必须将其从栈顶弹出,不能直接访问。

加载的助记符中,最常见的是 ldloc/ldc/ldstr,存储的助记符中,最常见的一个是 stloc。

我们先看一个非常简单的例子:

class Program
{
    static void Main(string[] args)
    {
        int i = 999;
        int j = 888;
        Console.WriteLine( i + j);
    }
}

该段代码对应的未被优化的 IL 代码(存在很多 nop 指令,它是空指令):

.class private auto ansi beforefieldinit AssemblyLab.Program extends [mscorlib] System.Object
{
    // Methods
    .method private hidebysig static void Main (string[] args) cil managed
    {
        //Method begins at RVA 0x207c
        // Code size 15 ( Oxf)
        .maxstack 2
        .entrypoint
        .locals init (
            [0] int32 i,
            [1] int32 j
        )
        IL_0000: nop
        IL_0001: ldc.i4 999
        IL_0006: stloc.0
        IL_0007: ldc.i4 888
        IL_000c: stloc.1
        IL_000d: ldloc.0
        IL_000e: ldloc.1
        IL_000f: add
        IL_0010: call void [mscorlib]System.Console::WriteLine(int32)
        IL_0015: nop
        IL_0016: ret
} // end of method Program::Main
.method publie hidebysig specialname rtspecialname instance void .ctor () cil managed
{
        // Method begins at RVA 0x2 097
        // Code size 7 ( 0x7 )
        maxstack 8
        L_0000: ldarg.0
        L_0001: call instance void [mscorlib]System.Obj ect::.ctor()
        IL_0006: ret
    } // end of method Program::.ctor
} // end of class AssemblyLab. Program

Main 方法的大部分代码都含有一个助记符。其中,nop 是编译器在 Debug 模式下插入的方便我们调试设置断点的空操作,所以,这里我们就忽略 nop。

首先看看方法的定义:

.method private hidebysig static void Main (string[] args) cil managed

IL 指令 .method 指出后面的代码为一个方法。IL 特性 private 指出该方法是私有的 (如果一个方法在 C# 中,没有显式给出可见性关键字,则默认的关键字是 private)。

而 hidebysig 的意思是,这个方法会被隐藏,当且仅当其父类存在同名且同签名 (相同的输入和输出参数个数和类型 ) 的方法 (hide by name and signature)。

后面的 static、void 和 C# 的意思是一样的。Main 方法接受一个字符串数组作为参数,cil managed 顾名思义是表示该方法为托管的。

下面的这一段代码中,我们看到了栈的身影:

.maxstack 2
.entrypoint
.locals init (
    [0] int32 i,
    [1] int32 j
)

由于代码仅仅有两个变量,因此栈的最大空间为2。之后,.entrypoint 指令指示编译器, 代码的入口点在此。

.local init 指令定义两个 int 类型的变量 i 和 j。

使用类之前,如果没有声明构造函数,C# 自动提供一个构造函数,用来调用它的所有父类的构造函数。

Program 类没有显式声明父类那么它的父类就是 System.Object。

Program 的构造函数以 .ctor 作为名称 ( 这是实例构造函数,静态构造函数以 .cctor 作为名称):

.method public hidebysig specialname rtspecialname instance void .ctor () cil managed
{
    //Method begins at RVA 0x2097
    // Code size 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

Program类Main方法的IL代码主体如下:

IL_0000: nop
IL_0001: ldc.i4 999
IL_0006: stloc.0
IL_0007: ldc.i4 888
IL_000c: stloc.1
IL_000d: ldloc.0
IL_000e: ldloc.1
IL_000f: add
IL_0010: call void [mscotlib]System.Console::WriteLine(int32)
IL_0015: nop
IL_0016: ret

0001 行加载了第一个变量(通过 ldc.i4), 其中,i4 代表 int32 类型,而后面的 999 则是变量的值。

0006 行则把刚刚加载的变量从栈中第 0 个位置弹出,并赋值给第 0 个局部变量 i。

0007 和 000c 行与前面的逻辑类似,如下图所示。

本文标题:C#/.NET JIT和IL(MSIL或CIL)实现跨平台

本文地址:https://www.hosteonscn.com/4993.html

评论

0条评论

发表评论

邮箱地址不会被公开。 必填项已用*标注