使用MSIL采用Emit方式实现C#的代码生成与注入

发表于2017-05-12
评论2 7k浏览

本文主要使用微软提供的一套C#API函数,通过这些API函数,可以对已经编译过的.Net体系生成的EXE,DLL文件进行修改,而不是修改源码编译的方式,来完成新功能的加入、或者原有功能的修改。这个方式可以应用于修改没有源码DLLEXE文件、批量修改或插入代码功能到DLLEXE文件中。

背景介绍

    unity3d在苹果上的热更新,一直是业界热烈讨论的话题。我所在的项目正在考虑使用LUA作为热更新的实现方式。于是在这种情况下,HotFix实现的热更方式成为我们的一个选项。处于个人的好奇心,阅读的HotFix的实现方式。即使用在所有的带有HotFix的标签的类或函数上使用mono提供CIL实现出来一套代码注入方式。即在类中每个函数都注入一个静态函数变量,在代码执行的时候,优先判断静态函数变量是否为空,来判断是否执行LUA脚本函数。从而实现使用LUA来热更新已经在外网的功能。

       在研究代码注入的过程中,发现C#的代码注入有两套实现方案。一套是微软自身提供的API函数的方式,这套方式操作起来比较容易。另外一套是Mono实现的API函数,与Unity关系比较密切。两套方式在本质上都是一样的,都是对中间语言的修改与操纵。

       文本主要介绍微软自身提供的API函数的方式       的使用。比较利于了解和学习中间语言。另外,如果读者对于C++反汇编语比较熟悉,阅读会非常轻松。如果不熟悉,也没有关系。

基础准备

知识准备

      MSIL即微软中间语言是一种属于通用语言架构.NET框架的低阶(lowest-level)的人类可读编程语言。目标为.NET框架的语言被编译成CIL,然后汇编成字节码CIL类似一个面向对象的组合语言,并且它是完全基于堆栈的. ,由于C#和通用语言架构的标准化,在.Net开发平台下,所有语言(CVB.NETJManaged C++)都会被编译为MSIL,再由CLR负责运行,字节码现在已经官方地成为了CIL。【维基百科定义

工具准备

ILSpy versionILSpy是一个开源Net的浏览器和反编译器。下载地址:http://ilspy.net/

使用指南:能把C#生成二进制文件转换为MSIL 或者C# 任选一种,当想用Emit实现某一功能但是不知道怎么写时,可以先把该功能的C#代码写出来,再用ildasm.exe将其转换成MSIL,然后转化为C#先查看是否能够显示出来。

 

环境准备

本节主要介绍如何能够构成一个可以测试代码注入的环境。

怎么实现动态库调用

本节主要描述如何调用使用代码注入方式生成的动态库,来验证注入代码时候能够得到正常的运行,并且能够得到正确的结果。

 

静态函数调用

1
2
Type t = typeof(MyType);
myType.InvokeMember("SwitchMe",BindingFlags.InvokeMethod,null,null,newobject[] { 2 })


注释:SwitchMeint num)是一个静态函数,InvokeMember可以直接返回SwitchMe的返回值

 

构造函数调用生成:

1
2
3
4
Objectobj = t.InvokeMember(null,
                           BindingFlags.DeclaredOnly |
                           BindingFlags.Public | BindingFlags.NonPublic |
                           BindingFlags.Instance | BindingFlags.CreateInstance, null, null, args);

其中args是构造函数的参数列表,如果没有参数,可以为空。

成员函数调用生成:

1
2
3
4
Strings = (String)t.InvokeMember("ToString",
                                  BindingFlags.DeclaredOnly |
                                  BindingFlags.Public | BindingFlags.NonPublic |
                                  BindingFlags.Instance | BindingFlags.InvokeMethod, null, obj, null);


其中ToString是成员函数名称,ToString没有参数。Obj是成员的This指针。

 

动态创建动态库

这个主要介绍,如何使用C#代码生成动态库,而不是直接建立动态库工程来生成动态库,为后面修改动态库做铺垫。

下面介绍如何生成动态库:

第一步:首先建立AssemblyName,

AssemblyName assemblyName = new AssemblyName("Study");

第二步:建立AssemblyBuilder

AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.RunAndSave);

第三步:建立ModuleBuilder

ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("StudyModule","StudyOpCodes.dll");

第四步:保存动态库StudyOpCodes.dll

assemblyBuilder.Save("StudyOpCodes.dll");

指令集合

变量赋值与读取指令注入

读取指令介绍:

Ldloc      将指定索引处的局部变量加载到计算堆栈上

Ldfld      查找对象中其引用当前位于计算堆栈的字段的值。

Ldsfld     将静态字段的值推送到计算堆栈上。

Ldflda    查找对象中其引用当前位于计算堆栈的字段的地址。

 

Ldarg     将函数的参数(由指定索引值引用)加载到堆栈上。如: OpCodes.Ldarg, argIndex

Ldarg_0   将函数的参数的索引为 0 的参数加载到计算堆栈上。

Ldarg_1   将函数的参数索引为 1 的参数加载到计算堆栈上。

Ldarg_2   将函数的参数索引为 2 的参数加载到计算堆栈上。

Ldarg_3   将函数的参数索引为 3 的参数加载到计算堆栈

 

Ldstr      推送对元数据中存储的字符串的新对象引用

Ldnull     将空引用(类型)推送到计算堆栈上。

Ldobj     将地址指向的值类型对象复制到计算堆栈的顶部。

Ldc.I4    将所提供的 int32 类型的值作为 int32 推送到计算堆栈上

Ldc.I8    将所提供的 int64 类型的值作为 int64 推送到计算堆栈上

Ldc.R4   将所提供的 float32 类型的值作为 F (float) 类型推送到计算堆栈上。

Ldc.R8   将所提供的 float64 类型的值作为 F (float) 类型推送到计算堆栈上

 

写指令:

Stloc      从计算堆栈的顶部弹出当前值并将其存储到指定索引处的局部变量列表中。

Starg      将位于计算堆栈顶部的值存储到位于指定索引的参数槽中

Stfld       用新值替换在对象引用或指针的字段中存储的值 常常成员变量指赋值语句使用

Stsfld     来自计算堆栈的值替换静态字段的值

Starg      将位于计算堆栈顶部的值存储到位于指定索引的参数槽中。

Starg.S  将位于计算堆栈顶部的值存储在参数槽中的指定索引处(短格式)。写指令介绍:

 

建立一个静态类变量

FieldBuilder fieldName = typeBuilder.DefineField("StaticName", typeof(string), FieldAttributes.Private | FieldAttributes.Static);

建立一个成员类变量

FieldBuilder fieldName = typeBuilder.DefineField("Name", typeof(string), FieldAttributes.Private);

成员变量:

FieldBuilder fieldUser =   typeBuilder.DefineField("objUser", typeof(Object), FieldAttributes.Private );

读:读取this.objUser

OpCodes.Ldarg_0)

OpCodes.Ldfld, fieldUser //fieldUser为成员变量

写:创建一个新的对象,并赋给指定的变量fieldUser,即相当于this.objUser= new Object();

OpCodes.Ldarg_0

OpCodes.Newobj, typeof(Object).GetConstructor(new Type[0]) //赋新值

OpCodes.Stfld, fieldUser

 

示例子string localName = “guo”

LocalBuilder localName = ilOfShow.DeclareLocal(typeof(string));

OpCodes.Ldstr, "guo");

OpCodes.Stloc, localName);

静态变量:

对于静态变量

FieldBuilder fieldName = typeBuilder.DefineField(

"Name", typeof(string), FieldAttributes.Private | FieldAttributes.Static);

读:class::Name

     OpCodes.Ldfld, fieldName//fieldName为成员变量

写:class::Name = "new Name"

OpCodes.Ldstr "new Name"

OpCodes.Stfld, Name

 

函数调用指令注入

普通函数调用

函数调用分两步:

第一步:设置参数

第二步:需要调用需要的函数

第三步:对于函数有返回值,但友不需要赋值的情况,需要把返回值推出栈 OpCodes.Pop

生成代码:

MethodInfo WriteMethodInfo = typeof(System.Console).GetMethod("WriteLine", new Type[]       { typeof(string) });

   ilOfShow.Emit(OpCodes.Ldstr, "Test");

   ilOfShow.Emit(OpCodes.Call, WriteMethodInfo);

C#代码

    System.Console.WriteLine("Test");

不定参数函数调用

不定参数的调用方式是:把所有的参数封装成最基础的object数组,然后对于所有的值类型和枚举类型需要采用Box的去封装一下

 

涉及指令:  

Box 将值类转换为对象引用 如:OpCodes.Box, 类型参数,后再完成赋值操作。

Unbox_Any  从中提取数据 obj,将其装箱表示形式 OpCodes.Unbox_Any, 类型参数 函数返回值可直接使用

    其过程分三步:

1.      一个对象引用 obj 推送到堆栈上。

2.    对象引用是从堆栈中弹出和取消装箱到指令中指定的类型。

3.    生成的对象引用或值类型推送到堆栈上。

注:unbox 指令将转换的对象引用 (类型 O),则为指针值类型装箱值类型,表示形式 (托管的指针,类型 &),将其未装箱的形式。 提供的值类型 (valType是表示类型的装箱对象中包含的值类型的元数据标记。

与不同 Box,所需的对象中制作一份使用的值类型 unbox 不需要从对象复制的值类型。 通常,它只计算已存在的已装箱对象内的值类型的地址。

InvalidCastException 如果该对象未被装箱为,则引发 valType

NullReferenceException 如果对象引用为空引用将引发。

TypeLoadException 如果值类型,则引发 valType 找不到。  Microsoft 中间语言 (MSIL) 指令转换为本机代码,而不是在运行时,通常是检测到此问题。


数组读取与写入指令注入

数组创建:

       第一步:设置数组的大小 OpCodes.Ldc_I4, paramsCount

       第二步:new 出数组 OpCodes.Newarr, assembly.MainModule.Import(typeof(object))

这里主要介绍数组的赋值,

       第一步:需要把变量读入到栈上 OpCodes.Ldfld, fieldArray

       第二步:把索引加入到栈上OpCodes.Ldc_I4, 1

    第三步:把需要赋值内容加载到栈上OpCodes.Ldstr, "aaa"

       第四步:用计算堆栈中的值替换给定索引处的数组元素,其类型在指令中指定OpCodes.Stelem_Ref

需要介绍的数组指令:

写数组指令:

       Stelem   用计算堆栈中的值替换给定索引处的数组元素,其类型在指令中指定。

       Stelem.I 用计算堆栈上的 native int 值替换给定索引处的数组元素。

       Stelem.I1      用计算堆栈上的 int8 值替换给定索引处的数组元素。

       Stelem.I2      用计算堆栈上的 int16 值替换给定索引处的数组元素。

       Stelem.I4      用计算堆栈上的 int32 值替换给定索引处的数组元素。

       Stelem.I8      用计算堆栈上的 int64 值替换给定索引处的数组元素。

       Stelem.R4     用计算堆栈上的 float32 值替换给定索引处的数组元素。

       Stelem.R8     用计算堆栈上的 float64 值替换给定索引处的数组元素。

读数组的指令:

       Ldelem  按照指令中指定的类型,将指定数组索引中的元素加载到计算堆栈的顶部。

       Ldelem.I       将位于指定数组索引处的 native int 类型的元素作为 native int 加载到计算堆栈的顶部。

       Ldelem.I1     将位于指定数组索引处的 int8 类型的元素作为 int32 加载到计算堆栈的顶部。

       Ldelem.I2     将位于指定数组索引处的 int16 类型的元素作为 int32 加载到计算堆栈的顶部。

       Ldelem.I4     将位于指定数组索引处的 int32 类型的元素作为 int32 加载到计算堆栈的顶部。

       Ldelem.I8     将位于指定数组索引处的 int64 类型的元素作为 int64 加载到计算堆栈的顶部。

       Ldelem.R4    将位于指定数组索引处的 float32 类型的元素作为 F 类型(浮点型)加载到计算堆栈的顶部。

       Ldelem.R8    将位于指定数组索引处的 float64 类型的元素作为 F 类型(浮点型)加载到计算堆栈的顶部。

       Ldelem.Ref   将位于指定数组索引处的包含对象引用的元素作为 O 类型(对象引用)加载到计算堆栈的顶部。

 

生成代码:

FieldBuilder fieldArray = typeBuilder.DefineField("objUser", typeof(string[]), FieldAttributes.Private);

MethodBuilder ArrayMethod = typeBuilder.DefineMethod("InitArray", MethodAttributes.Public, null, new Type[] { typeof(int), typeof(string)});

{

    //objUser = new string[3];

    ILGenerator ilOfShow = ArrayMethod.GetILGenerator();

 

    ilOfShow.Emit(OpCodes.Ldarg_0);

    ilOfShow.Emit(OpCodes.Ldc_I4, 3);

    ilOfShow.Emit(OpCodes.Newarr, typeof(string));

 

    ilOfShow.Emit(OpCodes.Stfld, fieldArray);

    //this.objUser[0] = "aaa";

    ilOfShow.Emit(OpCodes.Ldarg_0);

    ilOfShow.Emit(OpCodes.Ldfld, fieldArray);

    ilOfShow.Emit(OpCodes.Ldc_I4, 0);

    ilOfShow.Emit(OpCodes.Ldstr, "aaa");

    ilOfShow.Emit(OpCodes.Stelem_Ref);

    //this.objUser[1] = "bbb";

    ilOfShow.Emit(OpCodes.Ldarg_0);

    ilOfShow.Emit(OpCodes.Ldfld, fieldArray);

    ilOfShow.Emit(OpCodes.Ldc_I4, 1);

    ilOfShow.Emit(OpCodes.Ldstr, "bbb");

    ilOfShow.Emit(OpCodes.Stelem_Ref);

 

   MethodInfo WriteMethodInfo = typeof(System.Console).GetMethod("WriteLine", new Type[]         { typeof(string) });

   ilOfShow.Emit(OpCodes.Ldarg_0);

   ilOfShow.Emit(OpCodes.Ldfld, fieldArray);

   ilOfShow.Emit(OpCodes.Ldc_I4, 1);

   ilOfShow.Emit(OpCodes.Ldelem_Ref);

   ilOfShow.Emit(OpCodes.Call, WriteMethodInfo);

 

    ilOfShow.Emit(OpCodes.Ret);

}

C#代码

1
2
3
4
5
6
7
8
publicvoidInitArray(int num, string text)
{
    this.objUser = newstring[3];
    this.objUser[0] = "aaa";
    this.objUser[1] = "bbb";
 
    Console.WriteLine(this.objUser[1]);
}

 

if语句指令注入

函数的使用常常是使用指令 解释如下:

Brfalse

如果 value  false、空引用(Visual Basic 中的 Nothing)或零,则将控制转移到目标指令。

Brfalse.S

如果 value  false、空引用或零,则将控制转移到目标指令。

Brtrue

如果 value  true、非空或非零,则将控制转移到目标指令。

Brtrue.S

如果 value  true、非空或非零,则将控制转移到目标指令(短格式)。

另外还需要设置一个跳转的地点 即Label,即在需要的地方跳转到标识,标识真正生效是在MarkLabel的时候。微软对MarkLabel的解释送 在MSIL中标识当前指令流的位置给给定的标识。 如下图示例:

生成代码:

MethodBuilder myifMeBuilder = typeBuilder.DefineMethod("IfCall", MethodAttributes.Public | MethodAttributes.Static, null, new Type[] { typeof(int) });

{

MethodInfo ifMethodInfo = typeof(System.Console).GetMethod("WriteLine",new Type[] { typeof(string) });

     MethodInfo ifStringMethodInfo = typeof(string).GetMethod("IsNullOrEmpty", new Type[] { typeof(string) });

 

      ILGenerator ilOfShow = myifMeBuilder.GetILGenerator();

      Label defaultCase = ilOfShow.DefineLabel();

 

      ilOfShow.Emit(OpCodes.Ldstr, "this");

      ilOfShow.Emit(OpCodes.Call, ifStringMethodInfo);

      ilOfShow.Emit(OpCodes.Brfalse_SdefaultCase);

 

      ilOfShow.Emit(OpCodes.Ldstr, "Test");

      ilOfShow.Emit(OpCodes.Call, ifMethodInfo);

      ilOfShow.MarkLabel(defaultCase);

      ilOfShow.Emit(OpCodes.Ret);

}

 

C#代码

1
2
3
4
5
6
7
public static void IfCall(int num)
{
    if (string.IsNullOrEmpty("this"))
    {
        Console.WriteLine("Test");
    }
}



SwichCase语句调通指令注入

Switch

实现跳转表 包含两个有效指令标识

Br.S

无条件地将控制转移到目标指令

注:在Switch中,并没有条件转移,而是按照跳转列表的方式,和判断的参数,直接分成到 128从而实现一个短地址跳转指令集合。然后把跳转控制参数做为一个索引,从而实现一个类似数组作为地址的跳转方式。当索引大于索引的最大值的时候,就不跳转,直接执行Switch下一条IL指令。

生成代码:

MethodBuilder mySwitchMeBuilder = typeBuilder.DefineMethod("SwitchMe",MethodAttributes.Public |MethodAttributes.Static,typeof(string),new Type[] { typeof(int) });

{

    ILGenerator ilOfShow = mySwitchMeBuilder.GetILGenerator();

    Label defaultCase = ilOfShow.DefineLabel();

    Label endOfMethod = ilOfShow.DefineLabel();

    Label[] jumpTable = new Label[] { ilOfShow.DefineLabel(),ilOfShow.DefineLabel()};

 

    ilOfShow.Emit(OpCodes.Ldarg_0);

    ilOfShow.Emit(OpCodes.Switch, jumpTable);

 

    // Branch on default case

    ilOfShow.Emit(OpCodes.Br_S, defaultCase);

    // Case arg0 = 0

    ilOfShow.MarkLabel(jumpTable[0]);

    ilOfShow.Emit(OpCodes.Ldstr, "are no bananas");

    ilOfShow.Emit(OpCodes.Br_S, endOfMethod);

 

    // Case arg0 = 1

    ilOfShow.MarkLabel(jumpTable[1]);

    ilOfShow.Emit(OpCodes.Ldstr, "is one banana");

    ilOfShow.Emit(OpCodes.Br_S, endOfMethod);

 

    // Default case

    ilOfShow.MarkLabel(defaultCase);

    ilOfShow.Emit(OpCodes.Ldstr, "are many bananas");

 

    ilOfShow.MarkLabel(endOfMethod);

    ilOfShow.Emit(OpCodes.Ret);

}

C#代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
publicstaticstringSwitchMe(int num){
    string arg_23_0;
    switch (num) {
    case0:
        arg_23_0 = "are no bananas";
        break;
    case1:
        arg_23_0 = "is one banana";
        break;
    default:
        arg_23_0 = "are many bananas";
        break;
    }
    return arg_23_0;
}



循环指令注入

IL中,循环指令是依靠if跳转指令实现,这里就不在赘述。

类型处理指令注入

建立一个新类

名字为StudyOpCodes

TypeBuilder typeBuilder = moduleBuilder.DefineType("StudyOpCodes", TypeAttributes.Public);

C#代码:

publicclass StudyOpCodes{}

建立一个构造函数

ConstructorBuilder ctorMethod = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, new Type[] { });

{

    ILGenerator ilOfShow = ctorMethod.GetILGenerator();

    ilOfShow.Emit(OpCodes.Ldarg_0);

//调用object的构造函数

    ilOfShow.Emit(OpCodes.Call, typeof(object).GetConstructor(new Type[] { }));

    ilOfShow.Emit(OpCodes.Nop);

    ilOfShow.Emit(OpCodes.Ret);

}

C#代码:上面的生成的代码与默认的构造函数相同,所以不再列出

成员变量,并new 一个新对象,并赋值

在成员函数中,使用成员变量分两步

第一步:由于成员函数默认的第一个参数是this,而实际在访问成员变量的时候,需要通过this去索引,所以需要OpCodes.Ldarg_0

第二步:直接可以去取货设置成员变量。

FieldBuilder fieldUser = typeBuilder.DefineField("objUser", typeof(Object), FieldAttributes.Private );

MethodBuilder GetnewMethod = typeBuilder.DefineMethod("CreaterNewObejcts", MethodAttributes.Public, typeof(object), new Type[] { });

{

    ILGenerator ilOfShow = GetnewMethod.GetILGenerator();

    ilOfShow.Emit(OpCodes.Ldarg_0);

    ilOfShow.Emit(OpCodes.Newobj, typeof(Object).GetConstructor(new Type[0]));

    ilOfShow.Emit(OpCodes.Stfld, fieldUser);

ilOfShow.Emit(OpCodes.Ldarg_0);

    ilOfShow.Emit(OpCodes.Ldfld, fieldUser);

    ilOfShow.Emit(OpCodes.Ret);

}

C#代码

1
2
3
4
publicobjectCreaterNewObejcts(){
   his.objUser = newobject();
   returnthis.objUser;
}


建立GetSet函数

FieldBuilder fieldAge = typeBuilder.DefineField("objectAge", typeof(object), FieldAttributes.Private);

MethodBuilder SetMethod = typeBuilder.DefineMethod("SetObjectAge", MethodAttributes.Public, null, new Type[] { typeof(float)});

{

//this.objectAge = (int)num;

    ILGenerator ilOfShow = SetMethod.GetILGenerator();

    ilOfShow.Emit(OpCodes.Ldarg_0);

    ilOfShow.Emit(OpCodes.Ldarg_1);

    ilOfShow.Emit(OpCodes.Box, typeof(int)); //俗称装箱 或者类型转化

    ilOfShow.Emit(OpCodes.Stfld, fieldAge);

    ilOfShow.Emit(OpCodes.Ret);

}

MethodBuilder GetMethod = typeBuilder.DefineMethod("GetObjectAge", MethodAttributes.Public, typeof(object), new Type[] { });

{

    //return this.objectAge;

    ILGenerator ilOfShow = GetMethod.GetILGenerator();

    ilOfShow.Emit(OpCodes.Ldarg_0);

    ilOfShow.Emit(OpCodes.Ldfld, fieldAge);

    ilOfShow.Emit(OpCodes.Ret);

}

C#代码

1
2
3
4
5
6
7
8
9
publicvoidSetObjectAge(float num)
{
    this.objectAge = (int)num;
}
 
publicobjectGetObjectAge()
{
    returnthis.objectAge;
}


总结

       本文通过C#使用IL语言注入的介绍,能够让大家能白代码注入的实现原理。同时清楚了C#IL语言的转化关系,以便更够更高效的使用C#语言,就像了解C++的反汇编能够帮助大家对C++语言更了解一样。进一步能够使用代码注入的方式来实现自己需要的一些功能。

       再提一点,在CC++时代,黑客高手能够通过修改汇编指令,给一些应用增加一些新的功能,而不用了解其原理。那么在C#中也可实现这样的功能。比如自己写一个DLL,然后把这个DLL中指令复制到中间语言DllEXE中,然后修改IL指令,改变调用方式,来给目标DLL增加新功能。

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引