《精通C#》第18章-CIL和动态程序集的作用

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《精通C#》第18章-CIL和动态程序集的作用相关的知识,希望对你有一定的参考价值。

 

CIL是一种底层语言,CIL语言仅仅定义了一组通用的关键字,由CIL编译器进一步将这些关键字分为指令,特性以及操作码。指令是指用来描述程序集总体结构的CIL关键字,指令在语法上使用一个“.”做前缀来表示,特性是指定以何种方式执行指令的CIL关键字,比如.public,操作码就是实现程序集实现逻辑。CIL是一个以栈为基础的开发语言,在CIL中实现栈的是虚拟执行栈,因为这种的数据访问方式,导致CIL不能直接访问一个数据,必须先显示加载入栈中,在需要使用该值的时候,再使用一系列的操作码,将数值从栈中存储到内存中,例如:public void PrintMessage()

{

 string myMessage=""Hello.";

 Console.WriteLine(myMessage);

}

通过C#编译器将这个方法翻译为CIL代码:

.method public hidebysig instance void PrintMessage() cil managed

{

 .maxstack 1 //虚拟栈的长度,可自定义,默认为8,此处为自定义

 .locals init([0] string maMessage) //定义一个本地字符串变量(在索引0处)

 .ldstr "Hello." //加载字符串到虚拟栈

 .stloc.0 //存储字符串到本地变量

 .ldloc.0 //调用索引0处的值

 .call void[mscorlib]System.Console::WriteLine(string) //使用当前的值调用方法

 .ret

}

这一开发语言作用就在于可以在只有bll文件的情况下直接修改源代码,当然这需要有一定的CIL知识,这叫做正反向工程,首先我们写一个简单的控制台应用程序:

using System;
namespace HelloProgram
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello CIL code!");
            Console.ReadLine();
        }
    }
}

先使用编译器或者csc.exe将文件编译为exe文件,之后是vs开发人员命令提示中使用ildasm.exe打开exe文件,进行转储为il文件或者使用 “ildasm /?”进行查看命令,根据命令直接转储il文件,以下是转储的结果,我对一些注释进行了删除,已方便看起来更简洁:

//引用的程序集
.assembly extern mscorlib
{
  .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )                 
  .ver 4:0:0:0
}

//我的程序集
.assembly  HelloProgram
{
  .hash algorithm 0x00008004
  .ver 1:0:0:0
}
.module HelloProgram.exe
// MVID: {F2A7E657-9431-4A33-BF31-C8C1898B8A22}
.imagebase 0x00400000
.file alignment 0x00000200
.stackreserve 0x00100000
.subsystem 0x0003       // WINDOWS_CUI
.corflags 0x00020003    //  ILONLY 32BITPREFERRED
// Image base: 0x002F0000


// =============== CLASS MEMBERS DECLARATION ===================

.class  private auto ansi beforefieldinit HelloProgram.Program
       extends [mscorlib]System.Object
{
  .method private hidebysig static
          void  Main(string[] args) cil managed
  // SIG: 00 01 01 1D 0E
  {
    .entrypoint
    // 方法在 RVA 0x2050 处开始
    // 代码大小       19 (0x13)
    .maxstack  8
    .language ‘{3F5162F8-07C6-11D3-9053-00C04FA302A1}‘, ‘{994B45C4-E6E9-11D2-903F-00C04FA302A1}‘, ‘{5A869D0B-6611-11D3-BD2A-0000F80849BD}‘
// Source File ‘D:\\HelloProgram\\HelloProgram\\Program.cs‘
    .line 9,9 : 9,10 ‘D:\\\\HelloProgram\\\\HelloProgram\\\\Program.cs‘
//000009:         {
    IL_0000:  nop
//000010:             Console.WriteLine("Hello CIL code!");
    IL_0001:  ldstr      "Hello CIL code!"
    IL_0006:   call       void [mscorlib]System.Console::WriteLine(string)
    IL_000b: nop
    .line 11,11 : 13,32 ‘‘
//000011:             Console.ReadLine();
    IL_000c: call  string [mscorlib]System.Console::ReadLine()
    IL_0011: pop
    .line 12,12 : 9,10 ‘‘
//000012:         }
    IL_0012:  ret
  } // end of method Program::Main

  .method  public hidebysig specialname rtspecialname
          instance void  .ctor() cil managed
  // SIG: 20 00 01
  {
    // 方法在 RVA 0x2064 处开始
    // 代码大小       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

} // end of class HelloProgram.Program
如以上的cil代码注释所言,.assembly extern标记用来表示我们所引用的外部程序集,比如我们添加一个新的引用时:

.assembly extern System.Windows.Forms
{
  .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )                 
  .ver 4:0:0:0
},

同时需要注意的是,在CIL中与.Net类型交互时,总是需要使用这个类型的完全限定名,如示例中的system.Console,而且还需要这完全限定名之前使用方括号括起该类型所在程序集的友好名字,比如我们调用system.form的MessageBox.Show()方法:

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

{

.entrypoint
    .maxstack  8

ldstr "CIL is way cool"

call valuetype [System.window.Forms] System.window.Forms.DialogResult [System.window.Forms] System.window.Forms.MessageBox::Show(string)

pop

ret

}

同时我们发现,几乎每个成员的作用域中都有类似IL_xxxx的标记,这玩意叫做代码标签,除了在使用多个分支循环的CIL代码需要通过这些标签指定逻辑流的流向以外,其他的时候是可以忽略的.

使用ilasm.exe将il文件编译为dll程序集

 有ildasm.exe可以将程序集转化为il文件,自然就有方法可以将il文件转化会程序集,ilasm是其中的一种,还是打开vs开发人员命令提示,输入ilasm -?就可以获取ilasm的诸多命令,这里着重介绍下较为常用的几个命令:

参数 作用
/debug 包括调试信息
/dll 输出*.dll文件
/exe 输出*.exe文件
/key 编译程序集时使用给定的*.snk文件强名字
/output 指定输出的文件名和扩展名,如果没有使用此参数,那么会自动使用源文件名

如图:技术分享

CIL的特性和指令

 现在将CIL中的各个部分从上到下解析下其中包含的CIL指令

1.是在CIL中指定外部引用程序集,需要使用到external特性限定.assembly指令,如果引用的是一个强名字程序集,则还需要.publickeytoken和.ver指令,如:

.assembly extern mscorlib
{
  .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )                 
  .ver 4:0:0:0
}

2.在CIL中定义当前程序集,也就是我们编译的这个程序集,最简单的作法就是直接给定一个二进制文件名

.assembly CILTypes{}

也可以加上.ver,如:.assembly CILTypes{.ver 1:0:0:0}

3.在CIL中定义命名空间

需要用到.namespace指令,格式为:.namespace spaceName{},同理可进行嵌套命名空间namespace spaceName{namespace spaceName1{}},也可以写成:namespace spaceName.spaceName1{}

4.在CIL中定义类类型

.class就是用来定义一个新类,比如:.class public className{},其中public是CIL 特性,同C#相同的是若是不显式的给出基类,那么就是默认继承与system.object,若是该类是从其他的类继承的,那么就需要extends特性,要注意的是,就算是父类和子类是在一个程序集内,CIL也要求使用完整的父类名称,比如:

.namespace MyNamespace

{

 .class public MyBaseClass{}

.class public  MyDerivedClass{} extends MyNamespace.MyBaseClass{}

}

 在上面一个CIL代码中,特意用红色字体标注的auto就是在定义类的时候使用,默认是auto,也就是由CIL自动分配内存,若是需要使用平台访问非托管的C代码,则可以使用sequential或者explicit。

5.在CIL中定义接口类型

实现一个类型的接口使用implements,而且接口的定义,一人会用.class指令,例如:

.namespace MyNamespace

{

 .class public interface IMyInterface{}//第一接口

.class public MyBaseClass{}//一个简单的基类

.class public MyDerivedClass extends MyNamespace.MyBaseClass implements MyNamespace.IMyInterface{}

}

若是要实现接口的继承,则是需要使用implements特性,例如:

.class public interface IClassA{}

.class public interface IClassB implements MyNamespace.IClassA{}

6.在CIL中定义结构

可以使用.class与sealed特性(结构是不能被继承的)以及继承System.ValueType定义结构,

.class public sealed MyStruct  extends [mscorlib]System.ValueType{}

也可以使用简单的方法创建,使用value特性:

.class public sealed value MyStruct{}

7.在CIL中定义枚举

枚举类型是基础与system.Enum,后者同样继承与System.ValueType,所以同样需要密封

.class public sealed MyEnum extends [mscorlib]System.Enum{}

简写为.class public sealed enum MyEnum{}

8.定义泛型

除了需要使用完全限定名称之外还需要使用(`)对参数个数进行标记,若是定义一个List集合,则:newobj instance void class [mscorlib]System.Collections.Generic.List`1<int32>::.ctor()//List<int> myInts=new List<int>();

若是定义Dictionary则:newobj instance void class [mscorlib]System.Collections.Generic.Dictionary`2<string,int32>::.ctor()//Dictionary<string,int> d=new Dictionary<string,int>();

9.定义数据字段

使用field指令定义字段,在.NET中,System.Enum的类型作用域内可以支持static和literal特性,设置这些特性之后,字段数据可以通过类型直接访问。

10.构造函数

在CIL中,实例化层次的构造函数使用.ctor来表示,而静态的构造函数通过.cctor(类构造函数)来表示。同时还需要和specialname以及rtspecialname(返回类型的指定名字)特性结合才可以使用。这些特性用于标识出根据所给出的.NET语音进行特别处理的CIL标记,如果需要定义的是一个非静态的构造函数的话,需要与instance特性结合使用,在CIL中构造函数的实际返回值是void,还需要使用cilmanaged特性标识出这个方法是CIL代码而不是非托管代码,例如:

.class public MyBaseClass

{

.field private string stringField

.field private int32 intField

.method public hidebysig specialname rtspecialname instance void .ctor(string s,int32 i) cil managed

{

//可在此处对stringFiled与intField进行赋值

}

}

11.定义属性

就CIL而言,属性其实就是映射到以get_和set_为前缀的方法对,.property指令使用相关的.get和.set指令将属性语法映射到正确的方法上。如:

.class public MyBaseClass

{

.method public hidebysig specialname instance string get_TheString() cil manage{//实现代码}

.method public hidebysig specialname instance void set_TheString(string ‘value’) cil manage{//实现代码}

.property instance string TheString(){

.get instance string MyNamespace.MyBaseClass::get_TheString()

.set instance void MyNamespace.MyBaseClass::set_Thestring(string)

}

}

12.

C#:

public static void MyMethod(int inputInt,ref int refInt,ArrayList ar,out int outputInt)

{

 outputInt=0;

}

在CIL中,引用参数需在参数类型后面加上&符号,输出参数除了需要加上[out] 标记以外,也要加上&符号。如果一个参数是引用类型,则需要加上一个class标记的前缀:

.method public hidebysig static void MyMethod(int32 intputInt,int32& refInt,class [mscorlib]System.Collections.ArrayList ar,[out] int32& outputInt) cil managed{...}

CIL操作码

 操作码是一种简单的CIL标记,用于构造指定成员的实现逻辑部分,它大体可以划分为三个部分

1、控制操作流程的操作码

2、求值表达式的操作码

3、访问内存值的操作码

常用到的一些操作码

操作码     作用
add、sub、mul、div和rem 用于加减乘除两个值(rem用于返回除法操作的余数)
and、or、not和xor 用于在两个值上进行二进制操作
ceq、cgt和clt  用不同的方法比较两个在栈上的值,例如:ceq用于比较是否相等,cgt用于比较是否大于,clt用于比较是否小于
 box和unbox 在引用类型和值类型之间转换
 ret    退出方法和返回一个值
 beq、bgt、ble、blt和swith 控制方法的条件分支,例如:beq用于表示如果相等就中止到代码标签,bgt用于表示如果大于就中止到代码标签,ble用于表示小于等于就中止到代码标签,blt表示如果小于就中止到代码标签,所有的分支控制操作码都需要你给出一个CIL代码标签作为条件为真的跳转目的。
 call   调用一个给定类型的成员
 newarr和newobj 在内存中创建一个新的数组或者新的对象类型

主要的CIL栈操作码(压入操作)

操作码 作用
ldarg 加载方法的参数到栈中
ldc 加载一个常数值到栈中
ldfld 加载一个对象实例的成员到栈中
ldloc 加载一个本地变量到栈中
ldobj 获得一个堆对象的所有数据,并将它们放在栈中
ldstr   加载一个字符串数据到栈中

弹出操作码

操作码   作用
pop 删除当前栈顶的值,但是并不影响存储值
starg 存储栈顶的值到参数,根据索引确定这个值
stloc 弹出当前栈顶的值,并且存储在一个本地变量列表中,根据索引确定这个参数
stobj 从栈中复制一个特定的类型到指定的内存地址
stsfld 用从栈中获得的值替换静态成员的值

在CIL中声明本地变量

C#:

public static void MyLocalVariables()

{

string myStr="CIL code isfun!";

int myInt=33;

object myObj=new object();

}

CIL:

.method public hidebysig static void MyLocalVariables() cil managed

{

 .maxstack 8

.locals init ([0] string myStr,[1]int32 myInt,[2]object myObj)//定义3个本地变量

ldstr "CIL code is fun!" //加载字符串到虚拟执行栈中

stloc.0//弹出当前值,并存入本地变量[0]

ldc.i4 33 //加载常量到类型4(int的简写),设置值为33

stloc.1 //弹出当前的值,并存入本地变量[1]

newobj instance void[mscorlib]System.Object::.ctor() //创建一个新对象并放在栈上

stloc.2 //弹出当前的值,并存入本地变量[2]

ret

}

在CIL中映射参数到本地变量

C#:

public static int Add(int a,int b)

{

return a+b;

}

CIL:

.method public hidebysig static int32 Add(int32 a,int32 b) cil managed

{

.maxstack 2

ldarg.0//加载a到栈中

ldarg.1//加载b到栈中

add//求和

ret

}

需要注意的是,如果方法不是静态方法,那么CIL代码会自动隐式的接受一个附加参数,此时CIL中的加载操作码就需要变为ldarg.1和ldarg.2

CIL中的循环分支

C#:

public static void CountToTen()

{

for(int i=0;i<10;i++);

}

CIL:

.method public hidebysig static void CountToTen() cil managed

{

.maxstack 2

.locals init([0] int32 i)//初始化本地变量 i

IL_0000:ldc.i4.0//加载这个值到栈中

IL_0001:stloc.0//存储这个值到索引0处

IL_0002:br.s IL_0008//跳到IL_0008

IL_0003:ldloc.0//加载索引0的值

IL_0004:ldc.i4.1//加载值1

IL_0005:add//增加当前在索引0处的值

IL_0006:stloc.0

IL_0007:ldloc.0//加载在索引0处的值

IL_0008:ldc.i4.s 10//加载10到栈上

IL_0009:blt.s IL_0004//小于?如果是就跳至IL_0004

IL_000b:ret

}

动态程序集

静态程序集是.NET直接从磁盘存储器加载的.NET二进制文件,比如编译C#代码之后生成的程序集。动态程序集是通过System.Reflection.Emit命名空间提供的类型在内存中创建。那么在什么情况下需要使用到动态程序集呢?

1.构建需要格局用户输入来生成程序集文件的.NET开发工具。

2.构建需要在运行时通过元数据来生成远程类型的代理的程序。

3.希望加载静态程序集并能够动态插入新的类型到二进制图像中。

了解System.Reflection.Emit命名空间的一些成员

成员  作用
AssemblyBuilder 运行时创建程序集文件(.dll或者.exe)。.exe必须调用ModuleBuilder.SetEntryPoint()来设置模块的入口函数,如果没有指定入口函数,那么就会生成.dll文件
ModuleBuilder 定义当前程序集中的模块集
EnumBuilder 创建.NET枚举类型
TypeBuilder 运行时创建模块中的类、接口、结构、委托
MethodBuilder、LocalBuilder、PropertyBuilder、FieldBuilder、ConstructorBuilder、CustomAttributeBuilder、ParamterBuilder、EventBuilder 运行时创建类型成员,如方法、本地变量、属性、构造函数以及特性等
ILGenerator 产生CIL操作码到给定的类型成员
OpCodes 提供了很多可以映射到CIL操作码上的成员。这个类型需要同System.Reflection.Emit.ILGenerator提供的成员结合使用

其中较为常用的是ILGenerator,部分方法如下

方法 作用
BeginCatchBlock() 开始一个catch程序块
BeginExceptionBlock() 开始一个没有过滤的异常捕获块
BeginFinallyBlock() 开始一个finally块
BeginScope() 开始一个词汇作用域
 DeclareLocal() 定义一个本地变量
 DefineLabel() 定义一个新标签
 Emit() 被重载多次以生成CIL操作码
 EmitCall() 压入一个call或者callvirt操作码到CIL流
 EmitWriteLine() 根据不同类型的值,产生一个队console.writeline()的调用
 EndExceptionBlock() 结束一个异常程序块
 EndScope() 结束一个词汇作用域
 ThrowException() 产生抛出异常的指令
 UsingNamespace() 指定用来对本地变量求值的命名空间,并监控当前活动的程序块

生成动态的程序集

将下列的方法在运行时创建

public class HelloWorld

{

private string theMessage;

HelloWorld(){}

HelloWorld(string s){theMessage=s;}

public string GetMsg(){return theMessage;}

public void SayHello()

{

System.Console.WriteLine(“Hello from the HelloWorld class”);

}

}

首先,需要定义程序集的特征(名字、版本),其次实现HelloWorld类型,最后保存内存中的程序集到一个物理文件。

 public static void CreateMyAsm(AppDomain curAppDomain)
    {
      // Establish general assembly characteristics.
      AssemblyName assemblyName = new AssemblyName();
      assemblyName.Name = "MyAssembly";
      assemblyName.Version = new Version("1.0.0.0");

      // Create new assembly within the current AppDomain.
      AssemblyBuilder assembly =
        curAppDomain.DefineDynamicAssembly(assemblyName,
        AssemblyBuilderAccess.Save);

      // Given that we are building a single-file
      // assembly, the name of the module is the same as the assembly.
      ModuleBuilder module =
        assembly.DefineDynamicModule("MyAssembly", "MyAssembly.dll");

      // Define a public class named "HelloWorld".
      TypeBuilder helloWorldClass = module.DefineType("MyAssembly.HelloWorld",
        TypeAttributes.Public);

      // Define a private String member variable named "theMessage".
      FieldBuilder msgField =
        helloWorldClass.DefineField("theMessage", Type.GetType("System.String"),
        FieldAttributes.Private);

      // Create the custom ctor.
      Type[] constructorArgs = new Type[1];
      constructorArgs[0] = typeof(string);
      ConstructorBuilder constructor =
        helloWorldClass.DefineConstructor(MethodAttributes.Public,
        CallingConventions.Standard,
        constructorArgs);
      ILGenerator constructorIL = constructor.GetILGenerator();
      constructorIL.Emit(OpCodes.Ldarg_0);
      Type objectClass = typeof(object);
      ConstructorInfo superConstructor =
        objectClass.GetConstructor(new Type[0]);
      constructorIL.Emit(OpCodes.Call, superConstructor);
      constructorIL.Emit(OpCodes.Ldarg_0);
      constructorIL.Emit(OpCodes.Ldarg_1);
      constructorIL.Emit(OpCodes.Stfld, msgField);
      constructorIL.Emit(OpCodes.Ret);

      // Create the default ctor.
      helloWorldClass.DefineDefaultConstructor(MethodAttributes.Public);

      // Now create the GetMsg() method.
      MethodBuilder getMsgMethod =
        helloWorldClass.DefineMethod("GetMsg", MethodAttributes.Public,
        typeof(string), null);
      ILGenerator methodIL = getMsgMethod.GetILGenerator();
      methodIL.Emit(OpCodes.Ldarg_0);
      methodIL.Emit(OpCodes.Ldfld, msgField);
      methodIL.Emit(OpCodes.Ret);

      // Create the SayHello method.
      MethodBuilder sayHiMethod =
        helloWorldClass.DefineMethod("SayHello",
        MethodAttributes.Public, null, null);
      methodIL = sayHiMethod.GetILGenerator();
      methodIL.EmitWriteLine("Hello from the HelloWorld class!");
      methodIL.Emit(OpCodes.Ret);

      // ‘Bake‘ the class HelloWorld.
      // (Baking is the formal term for emitting the type)
      helloWorldClass.CreateType();

      // (Optionally) save the assembly to file.
      assembly.Save("MyAssembly.dll");
    }

常用的AssemblyBuilderAccess枚举值

作用
ReflectionOnly 表示一个动态程序集只能够通过反射访问
Run 表示一个动态程序集可以在内存中执行但不可以保存在硬盘上
RunAndSave 表示一个动态程序集可以在内存中执行也可以保存在硬盘上
Save 表示一个动态程序集可以保存到硬盘但是不可以在内存中执行

ModuleBuilder类型的作用

ModuleBuilder支持一系列成员方法,可用在定义模块包含的各种类型(类、接口和结构等)和嵌入资源(字符串表、图像等)

方法 作用
DefineEnum() 产生枚举类型定义
DefineResource() 产生存储在这个模块中的托管的嵌入资源
DefineType() 构造一个TypeBuilder,可以用来定义值类型、接口和类类型

其中最为关键的是DefineType,他通过System.Reflection.TypeAttributes枚举来进一步描述类型的格式。

成员 作用
Abstract 类型是抽象的
Class 类型是类
Interface 类型是接口
NestedAssembly 具有程序集访问级别的嵌套类,只能被所在的程序集方法访问
NestedFamAndAssem 具有程序集访问级别和成员访问级别的嵌套,因此只能够被属于成员和程序集交集的方法访问
NestedFamily 具有成员访问级别的嵌套类,只能够被本类型及子类型的方法访问
NestedFamORAssm 具有程序集访问级别和成员访问级别的嵌套类,能够被属于成员和程序集并集的方法访问
NestedPrivate 私有访问级别的嵌套类
NestedPublic 公有访问级别的嵌套
NotPublic 非公有的类
Public 公有的类型
Sealed 具体的不可扩展的类
Serializable 可以序列化的类
   

 

以上是关于《精通C#》第18章-CIL和动态程序集的作用的主要内容,如果未能解决你的问题,请参考以下文章

C#利用反射动态调用DLL并返回结果,和获取程序集的信息

C#动态加载dll 时程序集的卸载问题

《精通ASP.NET MVC5》第7章 SportStore:一个真正的应用程序

为啥这个非常简单的 C# 方法会产生如此不合逻辑的 CIL 代码?

《机器学习实战》第3章决策树程序清单3-1 计算给定数据集的香农熵calcShannonEnt()运行过程

RTX——第18章 内存管理