c# IL 入门

本文介绍了C#编译成IL(中间语言)的概念,通过一个简单的示例程序,详细解析了IL代码,包括类、方法、构造函数等的IL表示。IL是.NET框架的一部分,由编译器生成,被CLR(公共语言运行库)解释执行。文章还讨论了托管代码、程序集、元数据和JIT编译等核心概念。
摘要由CSDN通过智能技术生成

看下面这个例子:

using System;

using System.Collections.Generic;

 

namespace ConsoleApplication3

{

    class Program

    {

        delegate void Printer();

        //代理相当于一个类型

        static void Print1()

        {

            Console.WriteLine("print1");

        }

        static void Print2()

        {

            Console.WriteLine("print2");

        }

        static void Print3()

        {

            Console.WriteLine("print3");

        }

 

        static void Main(string[] args)

        {

            Printer p = Print1;

            //方法到一个兼容委托类型的隐式转换

            //相当于复制构造函数,然而c#是没有什么隐式类型转换这种说法的

            p += Print2;//不管你加不加这后面的两个方法,Printer依然生成继承 MulticastDelegate 的类

            p += Print3;

            p.Invoke();

        }

    }

}


首先说一个概念,托管代码:

托管代码就是Visual Basic .NETC#编译器编译出来的代码。编译器把代码编译成中间语言(IL),而不是能直接在你的电脑上运行的机器码。IL是独立于CPU且面向对象的指令集

中间语言IL被封装在一个叫程序集 (assembly)的文件中,程序集中包含了描述你所创建的类,方法和属性(例如安全需求)的所有元数据。


用IL DASM 反汇编这个程序:

 

 

 

 

 

工具界面上面的一些标识的含义:

 


所以结合IL我们可以看出来:

这里表示的就是Printer类的详细信息

 


逐步分析:

.class auto ansi sealed nested private Printer

       extends [mscorlib]System.MulticastDelegate

{

} // end of class Printer

 

.class 表示Program是一个类。并且它继承自程序集—mscorlib的System. MulticastDelegate类

 

Auto 程序的加载是由CLR来管理内存的

CLR的核心功能:内存管理,程序集加载,安全性,异常处理,线程同步等等。

CLR是公共语言运行库(Common Language Runtime)和Java虚拟机一样也是一个运行时环境

 

ansi,是为了在没有托管代码与托管代码之间实现无缝转换。这里主要指C、C++代码等

 

sealed 不可被继承 nested 嵌套类

 

 

再来看看更上层的Program类的详细信息:

.class private auto ansi beforefieldinit ConsoleApplication3.Program

       extends [mscorlib]System.Object

{

} // end of class ConsoleApplication3.Program

 

 

 

Beforefieldinit是用来标记运行库(CLR)可以在静态字段方法生成后的任意时刻,来加载构造函数,否则CLR就需要在一个精准的时间加载构造函数

 

接下来看一下复制构造函数

.method public hidebysig specialname rtspecialname

        instance void  .ctor(object 'object',

                             native int 'method') runtime managed

{

} // end of method Printer::.ctor

 

Hidebysig表示当把此类作为基类,存在派生类时,此方法不被继承,同上构造函数

 

cil managed:表示其中为IL代码,指示编译器编译为托管代码(上面写过的概念)

 

 

再来看普通构造函数:

.method public hidebysig specialname rtspecialname

        instance void  .ctor() cil managed

{

  // 代码大小       8 (0x8)

  .maxstack  8

  IL_0000:  ldarg.0

  IL_0001:  call       instance void [mscorlib]System.Object::.ctor()

  IL_0006:  nop

  IL_0007:  ret

} // end of method Program::.ctor

 

.maxstack:表示调用构造函数.otor期间的评估堆栈(Evaluation Stack)

 

IL_0000:标记代码行开头


ldarg.0:表示转载第一个成员参数,在这里其实是这个对象的this指针的引用


callcall一般用于调用静态方法,因为静态方法是在编译期就确定的。而这里的构造函数.ctor()也是在编译期就制定的。

而另一指令callvirt则表示调用实例方法,它是在运行时确定的,因为如前述,当调用方法的继承关系时,就要比较基类与派生类的同名函数的实现方法(virtualnew),以确定调用的函数所属的Method Table

 

call与callvirt: call主要用来调用静态方法,callvirt则用来调用普通方法和需要运行时绑定的方法(也就是用instance标记的实例方法)。不过也存在特殊情况,那就是call去调用虚方法,比如在密封类中的虚方法因为一定不可能会被重写因此使用call可提高性能。为什么会提高性能呢?不知道你是否还记得创建一个对象去调用这个对象的方法时,我们经常会判断这个对象是否为null,如果这个对象为null时去调用方法则会报错。之所以出现这种情况是因为callvirt在调用方法时会进行类型检测,此外判断是否有子类方法覆盖的情况从而动态绑定方法,而采用call则直接去调用了。另外当调用基类的虚方法时,比如调用object.ToString方法就是采用call方法,如果采用callvirt的话因为有可能要查看子类(一直查看到最后一个继承父类的子类)是否有重写方法,从而降低了性能。不过说到底call用来调用静态方法,而callvirt调用与对象关联的动态方法的核心思想是可以肯定的,那些采用call的特殊情况都是因为在这种情况下根本不需要动态绑定方法而是可以直接使用的


ret:表示执行完毕,返回

 

 

 

 

print1():最简单的一个函数

static void Print1(){ Console.WriteLine("print1"); }

 

 

.method private hidebysig static void  Print1() cil managed

{

  // 代码大小       13 (0xd)

  .maxstack  8

  IL_0000:  nop

  IL_0001:  ldstr      "print1"

  IL_0006:  call       void [mscorlib]System.Console::WriteLine(string)

  IL_000b:  nop

  IL_000c:  ret

} // end of method Program::Print1

 

ldstr:表示将字符串压栈,是用来把一个字符串加载到内存或评估堆栈中。在我们使用这些变量之前,是需要把这些变量加载到评估堆栈(evaluation stack )中去的,实际上是将字符串引用加载到栈中而不是用newobj

 

.NET运行时任何有意义的操作都是在堆栈上完成的,而不是直接操作寄存器。这就为.NET跨平台打下了基础

IL中压栈通常以ld开头,出栈则以st开头

 

 

最后是main函数:

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

{

  .entrypoint   //程序入口

  // 代码大小       70 (0x46)

  .maxstack  3  //计算堆栈大小

  .locals init ([0] class ConsoleApplication3.Program/Printer p)

  IL_0000:  nop

  IL_0001:  ldnull //将空引用(O 类型)推送到计算堆栈上

  IL_0002:  ldftn      void ConsoleApplication3.Program::Print1()

                    //将指向实现特定方法的本机代码的非托管指针(native int 类型)推送到计算堆栈上。

 

  IL_0008:  newobj     instance void ConsoleApplication3.Program/Printer::.ctor(object, native int)

                    //构造Printer

                    // C#中使用new创建一个对象时则在IL中对应的是newobj,另外还有值类型也是可以通过new来创建的,不过在IL中它对应的则是initobj

                    // newobj用来创建一个对象,首先会分配这个对象所需的内存,接着初始化对象附加成员同步索引块和类型对象指针然后再执行构造函数进行初始化并返回对象引用。initobj则是完成栈上已经分配好的内存的初始化工作,将值类型置0引用类型置null即可。

 

  IL_000d:  stloc.0 //把计算堆栈顶部的值放到调用堆栈索引0处

 IL_000e:  ldloc.0      //把调用堆栈索引为0处的值复制到计算堆栈

 

  IL_000f:  ldnull

  IL_0010:  ldftn      void ConsoleApplication3.Program::Print2()

  IL_0016:  newobj     instance void ConsoleApplication3.Program/Printer::.ctor(object, native int)

                                //又构造了一个Printer

 

  IL_001b:  call       class [mscorlib]System.Delegate [mscorlib]System.Delegate::Combine(class [mscorlib]System.Delegate,                                                                                          class [mscorlib]System.Delegate)

            //调用System.Delegate::Combine,把两个System.Delegate合并

            //源码里面的 “+=”就是在这里实现的

 

  IL_0020:  castclass  ConsoleApplication3.Program/Printer

            //尝试将引用传递的对象转换为指定的类

  IL_0025:  stloc.0

  IL_0026:  ldloc.0

  IL_0027:  ldnull

  IL_0028:  ldftn      void ConsoleApplication3.Program::Print3()

  IL_002e:  newobj     instance void ConsoleApplication3.Program/Printer::.ctor(object,

                                                                                native int)

  IL_0033:  call       class [mscorlib]System.Delegate [mscorlib]System.Delegate::Combine(class [mscorlib]System.Delegate,

                                                                                          class [mscorlib]System.Delegate)

  IL_0038:  castclass  ConsoleApplication3.Program/Printer

  IL_003d:  stloc.0

  IL_003e:  ldloc.0

 

  IL_003f:  callvirt   instance void ConsoleApplication3.Program/Printer::Invoke()

        // 对对象调用后期绑定方法,并且将返回值推送到计算堆栈上

 

  IL_0044:  nop

  IL_0045:  ret

} // end of method Program::Main

 

 

 

 

 

 

 

 

 

总结一下常用的IL指令:

.entrypoint:指令表示CLR加载程序时,是首先从.entrypoint开始的,即从Main方法作为程序的入口函数

stloc.X:把计算堆栈顶部的值放到调用堆栈索引为X

ldloc.X:把调用堆栈X处的值复制到计算堆栈
.newobj 用于创建引用类型的对象;
.ldstr
:用于创建String对象变量;
.newarr
:用于创建数组型对象;
.box
:在值类型转换为引用类型的对象时,将值类型拷贝至托管堆上分配内存。

.assembly:指令告诉编译器,我们准备去用一个外部的类库(不是我们自己写的,而是提前编译好的

 

 

 

对于流程控制,主要是br、brture和brfalse这3条指令,其中br是直接进行跳转,brture和brture则是进行判断再进行跳转。

具体内容参考:https://www.cnblogs.com/fangyz/p/5547433.html

 

 

 

一个像exe这样的程序集,结构如下图:

 

一个程序集是有多个托管模块组成的,一个模块可以理解为一个类或者多个类一起编译后生成的程序集

 

程序集清单指的是描述程序集的相关信息,PE文件头描述PE文件的文件类型、创建时间等。CLR头描述CLR版本、CPU信息等,它告诉系统这是一个.NET程序集

 

元数据用来描述类、方法、参数、属性等数据,.NET中每个模块包含44个元数据表,主要包括定义表、引用表、指针表和堆。定义表包括类定义表、方法表等,引用表描述引用到类型或方法之间的映射记录,指针表里存放着方法指针、参数指针等。元数据表就相当于一个数据库,多张表之间有类似于主外键之间的关系

 

我们调用一个方法表中的方法,这个方法会指向一个触发JIT编译器地址和方法对应的IL地址,于是JIT编译器便将这个方法指向的IL编译成本地代码。生成本地代码后这个方法将会有一条引用指向本地代码首地址,这样下次调用这个方法的时候将直接执行指向的本地代码

 

 

 

 


 

 

 

 

 

IL指令大全

名称

说明

Add

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值