本文主要是介绍c# IL 入门,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
看下面这个例子:
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 .NET和C#编译器编译出来的代码。编译器把代码编译成中间语言(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指针的引用
call:call一般用于调用静态方法,因为静态方法是在编译期就确定的。而这里的构造函数.ctor()也是在编译期就制定的。
而另一指令callvirt则表示调用实例方法,它是在运行时确定的,因为如前述,当调用方法的继承关系时,就要比较基类与派生类的同名函数的实现方法(virtual和new),以确定调用的函数所属的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指令大全
名称 |
这篇关于c# IL 入门的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!