Allen Kashiwa Blog

羁旅红尘不曾醉,笑歌年少且长行。

专注游戏开发与设计


唯佳人与游戏不可辜负

使用Mono.Cecil实现IL代码注入

前言

腾讯开源的Unity热更解决方案xLua有一个非常吸引人的特性就是Hotfix,其原理是使用Mono.Cecil库对进行C#层编译出来的dll程序集进行IL代码注入。其作者也在知乎的回答中简单说明了原理:如何评价腾讯在Unity下的xLua(开源)热更方案? - 车雄生的回答 - 知乎

如果你需要在自己的lua方案中添加这样的Hotfix特效,那可以考虑用这个方法。本文的目的就旨在演示说明Mono.Cecil库的一些基本用法,希望可以帮到你。

本文首发于我的个人博客,示例工程在我的github中的CecilInject。

演示

新建一个Unity项目,新建一个空场景,在Assets/Scripts目录(没有则新建一个,下文提到的目录也一样)下新建脚本Test.cs,在场景的主摄像机中挂载该脚本,内容修改如下:


using System;
using UnityEngine;

public class TestInjectAttribute : Attribute
{
}

public class Normal
{
    public static int GetMax(int a, int b)
    {
        Debug.LogFormat("a = {0}, b = {1}", a, b);
        return a > b ? a : b;
    }
}

[TestInject]
public class Inject
{
    public static int GetMax(int a, int b)
    {
        return a;
    } 
}

public class Test : MonoBehaviour
{
    void Start()
    {
        Debug.LogFormat("Normal Max: {0}", Normal.GetMax(6, 9));
        Debug.LogFormat("Inject Max: {0}", Inject.GetMax(6, 9));
    }
}

这个文件只是简单定义了一个名为Test的MonoBehaviour;一个TestInjectAttribute;两个都实现了静态方法GetMax的类,其中Inject类赋予了TestInjectAttribute特性。运行场景,我们可以在Console窗口中得到如下结果:

image

显然这里Inject类中的GetMax的实现是错误的,我们靠IL注入来修复这个错误。

要使用Mono.Cecil我们首先需要将其包含到我们的项目中,在Unity的安装目录(我的在C:\Program Files\Unity\Editor\Data\Managed供参考),找到对应的程序集文件,将Mono.Cecil.dll、Mono.Cecil.Mdb.dll、Mono.Cecil.Pdb.dll其放在Assets/Plugins/Cecil目录中。你也可以选择其开源版本

在Assets/Editor目录下新建一个InjectTool.cs:


using System;
using System.Linq;
using UnityEditor;
using Mono.Cecil;
using Mono.Cecil.Cil;
using UnityEngine;

public static class InjectTool
{

    private const string AssemblyPath = "./Library/ScriptAssemblies/Assembly-CSharp.dll";

    [MenuItem("Custom/Inject")]
    public static void Inject()
    {
        Debug.Log("InjectTool Inject  Start");

        if (Application.isPlaying || EditorApplication.isCompiling)
        {
            Debug.Log("You need stop play mode or wait compiling finished");
            return;
        }

        // 按路径读取程序集
        var readerParameters = new ReaderParameters { ReadSymbols = true };
        var assembly = AssemblyDefinition.ReadAssembly(AssemblyPath, readerParameters);
        if (assembly == null)
        {
            Debug.LogError(string.Format("InjectTool Inject Load assembly failed: {0}",
                AssemblyPath));
            return;
        }

        try
        {
            var module = assembly.MainModule;
            foreach (var type in module.Types)
            {
                // 找到module中需要注入的类型
                var needInjectAttr = typeof(TestInjectAttribute).FullName;
                bool needInject = type.CustomAttributes.Any(typeAttribute => typeAttribute.AttributeType.FullName == needInjectAttr);
                if (!needInject)
                {
                    continue;
                }
                // 只对公有方法进行注入
                foreach (var method in type.Methods)
                {
                    if (method.IsConstructor || method.IsGetter || method.IsSetter || !method.IsPublic)
                        continue;
                    InjectMethod(module, method);
                }
            }
            assembly.Write(AssemblyPath, new WriterParameters { WriteSymbols = true });
        }
        catch (Exception ex)
        {
            Debug.LogError(string.Format("InjectTool Inject failed: {0}", ex));
            throw;
        }
        finally
        {
            if (assembly.MainModule.SymbolReader != null)
            {
                Debug.Log("InjectTool Inject SymbolReader.Dispose Succeed");
                assembly.MainModule.SymbolReader.Dispose();
            }
        }

        Debug.Log("InjectTool Inject End");
    }

    private static void InjectMethod(ModuleDefinition module, MethodDefinition method)
    {
        // 定义稍后会用的类型
        var objType = module.ImportReference(typeof(System.Object));
        var intType = module.ImportReference(typeof(System.Int32));
        var logFormatMethod =
            module.ImportReference(typeof(Debug).GetMethod("LogFormat", new[] {typeof(string), typeof(object[])}));

        // 开始注入IL代码
        var insertPoint = method.Body.Instructions[0];
        var ilProcessor = method.Body.GetILProcessor();
        // 设置一些标签用于语句跳转
        var label1 = ilProcessor.Create(OpCodes.Ldarg_1);
        var label2 = ilProcessor.Create(OpCodes.Stloc_0);
        var label3 = ilProcessor.Create(OpCodes.Ldloc_0);
        ilProcessor.InsertBefore(insertPoint, ilProcessor.Create(OpCodes.Nop));
        ilProcessor.InsertBefore(insertPoint, ilProcessor.Create(OpCodes.Ldstr, "a = {0}, b = {1}"));
        ilProcessor.InsertBefore(insertPoint, ilProcessor.Create(OpCodes.Ldc_I4_2));
        ilProcessor.InsertBefore(insertPoint, ilProcessor.Create(OpCodes.Newarr, objType));
        ilProcessor.InsertBefore(insertPoint, ilProcessor.Create(OpCodes.Dup));
        ilProcessor.InsertBefore(insertPoint, ilProcessor.Create(OpCodes.Ldc_I4_0));
        ilProcessor.InsertBefore(insertPoint, ilProcessor.Create(OpCodes.Ldarg_0));
        ilProcessor.InsertBefore(insertPoint, ilProcessor.Create(OpCodes.Box, intType));
        ilProcessor.InsertBefore(insertPoint, ilProcessor.Create(OpCodes.Stelem_Ref));
        ilProcessor.InsertBefore(insertPoint, ilProcessor.Create(OpCodes.Dup));
        ilProcessor.InsertBefore(insertPoint, ilProcessor.Create(OpCodes.Ldc_I4_1));
        ilProcessor.InsertBefore(insertPoint, ilProcessor.Create(OpCodes.Ldarg_1));
        ilProcessor.InsertBefore(insertPoint, ilProcessor.Create(OpCodes.Box, intType));
        ilProcessor.InsertBefore(insertPoint, ilProcessor.Create(OpCodes.Stelem_Ref));
        ilProcessor.InsertBefore(insertPoint, ilProcessor.Create(OpCodes.Call, logFormatMethod));
        ilProcessor.InsertBefore(insertPoint, ilProcessor.Create(OpCodes.Ldarg_0));
        ilProcessor.InsertBefore(insertPoint, ilProcessor.Create(OpCodes.Ldarg_1));
        ilProcessor.InsertBefore(insertPoint, ilProcessor.Create(OpCodes.Ble, label1));
        ilProcessor.InsertBefore(insertPoint, ilProcessor.Create(OpCodes.Ldarg_0));
        ilProcessor.InsertBefore(insertPoint, ilProcessor.Create(OpCodes.Br, label2));
        ilProcessor.InsertBefore(insertPoint, label1);
        ilProcessor.InsertBefore(insertPoint, label2);
        ilProcessor.InsertBefore(insertPoint, ilProcessor.Create(OpCodes.Br, label3));
        ilProcessor.InsertBefore(insertPoint, label3);
        ilProcessor.InsertBefore(insertPoint, ilProcessor.Create(OpCodes.Ret));
    }
}

这里的重点,也就是Cecil接口的使用,和IL指令的编写。接口的使用看代码就可以了。这里的IL指令怎么得到呢?我们通过ILDasm.exe工具反编译我们项目自己的程序集文件得到,即反编译/Library/ScriptAssemblies/Assembly-CSharp.dll文件。该工具在Windows SDK中能够找到,我用的是这个路径下的,供大家参考:

C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6 Tools\x64

双击运行该工具,在文件菜单栏中选择打开我们项目自己的程序集文件,找到Normal那一项,双击GetMax接口就能在一个新的窗口中看到对应的IL字节码,此时我们将其翻译为对应的Cecil接口调用就行了。

Normal的GetMax方法的IL如图所示:

image

可以看到我们的代码中的Opcode和这里一一对应。此时Inject的GetMax方法的IL如图所示:

image

关闭ILDasm.exe,避免对dll文件的占用。现在运行项目中的Custom/Inject菜单项,然后重新运行ILDasm.exe反编译dll文件。此时Inject的GetMax方法的IL变成了下面这样:

image

这样一来注入就完成了,现在我们运行场景,此时Console窗口的信息如下:

image

Hotfix

如前所述,本文仅对Mono.Cecil库的使用作一下简单演示,要实现Hotfix功能,可以结合自己的Lua方案通过此方法截断CS层的方法调用即可。

用支付宝请我喝咖啡

用微信请我吃辣条

最近的文章

UGUI中几种不规则按钮的实现方式

前言UGUI中的按钮默认是矩形的,若要实现非矩形按钮该怎么做呢?比如这样的按钮:本文将介绍两种实现方式供大家选择。使用alphaHitTestMinimumThresholdImage类的alphaHitTestMinimumThreshold是一个浮点值,Raycast检测时只有图片中高于该值的部分会抛出点击事件。因此我们可以使用一张alpha通道的值高于该设置值的Sprite用于自定义按钮的点击相应区域。我们准备一张点击区域alpha高于某值,非点击区域alpha低于某值的Sprite...…

继续阅读
更早的文章

使用subst命令快速跳转到工作间

应用场景程序员或多或少都会在命令行下面工作。我个人比较常使用命令行,特别是使用git做版本管理的时候。并且开发人员一般对自己的文件会有条理的组织。比如我会在某个盘符下创建一个WorkSpace文件夹放置我几乎所有的工作相关的文件。这时到达工作路径需要至少两条指令:如果d盘下有同意以”W”开头的其他文件夹,那我们还需要多打几个字母才能跳转到工作间,而使用subst指令可以让这个过程更简单,这是我最近知道的。substDOS下的subst指令可以将路径与驱动器号关联,我们通过/?参数来看看这条...…

继续阅读