动态替换C#方法的内容?

Dynamically replace the contents of a C# method?

我要做的是更改调用C方法时它的执行方式,以便我可以编写如下内容:

1
2
3
4
5
6
7
8
[Distributed]
public DTask<bool> Solve(int n, DEvent<bool> callback)
{
    for (int m = 2; m < n - 1; m += 1)
        if (m % n == 0)
            return false;
    return true;
}

在运行时,我需要能够分析具有分布式属性(我已经可以这样做)的方法,然后在函数体执行之前和函数返回之后插入代码。更重要的是,我需要能够在调用solve的地方或函数开始时(在编译时;在运行时这样做是目标)不修改代码。

目前我已经尝试了这段代码(假设t是存储求解的类型,m是求解的方法信息):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private void WrapMethod(Type t, MethodInfo m)
{
    // Generate ILasm for delegate.
    byte[] il = typeof(Dpm).GetMethod("ReplacedSolve").GetMethodBody().GetILAsByteArray();

    // Pin the bytes in the garbage collection.
    GCHandle h = GCHandle.Alloc((object)il, GCHandleType.Pinned);
    IntPtr addr = h.AddrOfPinnedObject();
    int size = il.Length;

    // Swap the method.
    MethodRental.SwapMethodBody(t, m.MetadataToken, addr, size, MethodRental.JitImmediate);
}

public DTask<bool> ReplacedSolve(int n, DEvent<bool> callback)
{
    Console.WriteLine("This was executed instead!");
    return true;
}

但是,methodRental.swapmethodBody只在动态模块上工作,而不是那些已经编译并存储在程序集中的模块。

因此,我正在寻找一种有效地对已经存储在已加载并正在执行的程序集中的方法执行swapMethodBody的方法。

注意,如果必须将该方法完全复制到动态模块中,这不是一个问题,但是在这种情况下,我需要找到一种跨IL复制的方法,并更新所有调用to solve(),以便它们指向新的副本。


对于.NET 4及更高版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
using System;
using System.Reflection;
using System.Runtime.CompilerServices;


namespace InjectionTest
{
    class Program
    {
        static void Main(string[] args)
        {
            Target targetInstance = new Target();

            targetInstance.test();

            Injection.install(1);
            Injection.install(2);
            Injection.install(3);
            Injection.install(4);

            targetInstance.test();

            Console.Read();
        }
    }

    public class Target
    {
        public void test()
        {
            targetMethod1();
            Console.WriteLine(targetMethod2());
            targetMethod3("Test");
            targetMethod4();
        }

        private void targetMethod1()
        {
            Console.WriteLine("Target.targetMethod1()");

        }

        private string targetMethod2()
        {
            Console.WriteLine("Target.targetMethod2()");
            return"Not injected 2";
        }

        public void targetMethod3(string text)
        {
            Console.WriteLine("Target.targetMethod3("+text+")");
        }

        private void targetMethod4()
        {
            Console.WriteLine("Target.targetMethod4()");
        }
    }

    public class Injection
    {        
        public static void install(int funcNum)
        {
            MethodInfo methodToReplace = typeof(Target).GetMethod("targetMethod"+ funcNum, BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
            MethodInfo methodToInject = typeof(Injection).GetMethod("injectionMethod"+ funcNum, BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
            RuntimeHelpers.PrepareMethod(methodToReplace.MethodHandle);
            RuntimeHelpers.PrepareMethod(methodToInject.MethodHandle);

            unsafe
            {
                if (IntPtr.Size == 4)
                {
                    int* inj = (int*)methodToInject.MethodHandle.Value.ToPointer() + 2;
                    int* tar = (int*)methodToReplace.MethodHandle.Value.ToPointer() + 2;
#if DEBUG
                    Console.WriteLine("
Version x86 Debug
"
);

                    byte* injInst = (byte*)*inj;
                    byte* tarInst = (byte*)*tar;

                    int* injSrc = (int*)(injInst + 1);
                    int* tarSrc = (int*)(tarInst + 1);

                    *tarSrc = (((int)injInst + 5) + *injSrc) - ((int)tarInst + 5);
#else
                    Console.WriteLine("
Version x86 Release
"
);
                    *tar = *inj;
#endif
                }
                else
                {

                    long* inj = (long*)methodToInject.MethodHandle.Value.ToPointer()+1;
                    long* tar = (long*)methodToReplace.MethodHandle.Value.ToPointer()+1;
#if DEBUG
                    Console.WriteLine("
Version x64 Debug
"
);
                    byte* injInst = (byte*)*inj;
                    byte* tarInst = (byte*)*tar;


                    int* injSrc = (int*)(injInst + 1);
                    int* tarSrc = (int*)(tarInst + 1);

                    *tarSrc = (((int)injInst + 5) + *injSrc) - ((int)tarInst + 5);
#else
                    Console.WriteLine("
Version x64 Release
"
);
                    *tar = *inj;
#endif
                }
            }
        }

        private void injectionMethod1()
        {
            Console.WriteLine("Injection.injectionMethod1");
        }

        private string injectionMethod2()
        {
            Console.WriteLine("Injection.injectionMethod2");
            return"Injected 2";
        }

        private void injectionMethod3(string text)
        {
            Console.WriteLine("Injection.injectionMethod3" + text);
        }

        private void injectionMethod4()
        {
            System.Diagnostics.Process.Start("calc");
        }
    }

}


Harmony是一个开放源码库,设计用于在运行时替换、修饰或修改任何类型的现有C方法。它的主要焦点是用Mono编写的游戏和插件,但该技术可以用于任何.NET版本。它还处理对同一方法的多个更改(它们会累积而不是覆盖)。

它为每个原始方法创建类型为dynamicmethod的方法,并向其发出在开始和结束时调用自定义方法的代码。它还允许您编写过滤器来处理原始IL代码,从而可以对原始方法进行更详细的操作。

为了完成这个过程,它编写了一个简单的汇编程序跳转到原始方法的蹦床中,该蹦床指向编译动态方法生成的汇编程序。这适用于Windows、MacOS和Mono支持的任何Linux上的32/64位。


您可以在运行时修改方法的内容。但不应该这样做,强烈建议出于测试目的保留它。

请看一下:

http://www.codeproject.com/articles/463508/net-clr-injection-modify-il-code-during-run-time

基本上,你可以:

  • 通过methodinfo.getmethodbody().getilasbytearray()获取IL方法内容
  • 弄乱了这些字节。

    如果您只想预先编写或附加一些代码,那么只需预先编写/附加您想要的操作码(不过,要注意保持堆栈干净)

    以下是一些"未编译"现有IL的提示:

    • 返回的字节是一系列IL指令,后跟它们的参数(如果它们有一些参数,例如,.call有一个参数:被调用的方法标记,.pop没有参数)
    • 可以使用opcodes.your opcode.value(它是保存在程序集中的实际操作码字节值)找到IL代码与返回数组中的字节之间的对应关系。
    • 根据调用的操作码,附加在IL代码之后的参数可能具有不同的大小(从一个字节到几个字节)。
    • 您可以通过适当的方法找到这些参数引用的标记。例如,如果您的IL包含".call 354354"(在hexa中编码为28 00 05 68 32,28h=40为".call"opcode,56832h=354354),则可以使用methodbase.getmethodfromhandle(354354)找到相应的调用方法。
  • 一旦修改,您的IL字节数组可以通过injectionhelper.updateilcodes(methodinfo方法,byte[]ilcodes)-参见上面提到的链接

    这是"不安全"的部分…它工作得很好,但这包括黑客内部的clr机制…


  • 如果方法不是虚拟的、非泛型的、不属于泛型类型、不属于内联的和在x86平台上,则可以替换它:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    MethodInfo methodToReplace = ...
    RuntimeHelpers.PrepareMetod(methodToReplace.MethodHandle);

    var getDynamicHandle = Delegate.CreateDelegate(Metadata<Func<DynamicMethod, RuntimeMethodHandle>>.Type, Metadata<DynamicMethod>.Type.GetMethod("GetMethodDescriptor", BindingFlags.Instance | BindingFlags.NonPublic)) as Func<DynamicMethod, RuntimeMethodHandle>;

    var newMethod = new DynamicMethod(...);
    var body = newMethod.GetILGenerator();
    body.Emit(...) // do what you want.
    body.Emit(OpCodes.jmp, methodToReplace);
    body.Emit(OpCodes.ret);

    var handle = getDynamicHandle(newMethod);
    RuntimeHelpers.PrepareMethod(handle);

    *((int*)new IntPtr(((int*)methodToReplace.MethodHandle.Value.ToPointer() + 2)).ToPointer()) = handle.GetFunctionPointer().ToInt32();

    //all call on methodToReplace redirect to newMethod and methodToReplace is called in newMethod and you can continue to debug it, enjoy.


    logman的解决方案,但带有用于交换方法体的接口。还有一个简单的例子。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    using System;
    using System.Linq;
    using System.Reflection;
    using System.Runtime.CompilerServices;

    namespace DynamicMojo
    {
        class Program
        {
            static void Main(string[] args)
            {
                Animal kitty = new HouseCat();
                Animal lion = new Lion();
                var meow = typeof(HouseCat).GetMethod("Meow", BindingFlags.Instance | BindingFlags.NonPublic);
                var roar = typeof(Lion).GetMethod("Roar", BindingFlags.Instance | BindingFlags.NonPublic);

                Console.WriteLine("<==(Normal Run)==>");
                kitty.MakeNoise(); //HouseCat: Meow.
                lion.MakeNoise(); //Lion: Roar!

                Console.WriteLine("<==(Dynamic Mojo!)==>");
                DynamicMojo.SwapMethodBodies(meow, roar);
                kitty.MakeNoise(); //HouseCat: Roar!
                lion.MakeNoise(); //Lion: Meow.

                Console.WriteLine("<==(Normality Restored)==>");
                DynamicMojo.SwapMethodBodies(meow, roar);
                kitty.MakeNoise(); //HouseCat: Meow.
                lion.MakeNoise(); //Lion: Roar!

                Console.Read();
            }
        }

        public abstract class Animal
        {
            public void MakeNoise() => Console.WriteLine($"{this.GetType().Name}: {GetSound()}");

            protected abstract string GetSound();
        }

        public sealed class HouseCat : Animal
        {
            protected override string GetSound() => Meow();

            private string Meow() =>"Meow.";
        }

        public sealed class Lion : Animal
        {
            protected override string GetSound() => Roar();

            private string Roar() =>"Roar!";
        }

        public static class DynamicMojo
        {
            /// <summary>
            /// Swaps the function pointers for a and b, effectively swapping the method bodies.
            /// </summary>
            /// <exception cref="ArgumentException">
            /// a and b must have same signature
            /// </exception>
            /// <param name="a">Method to swap</param>
            /// <param name="b">Method to swap</param>
            public static void SwapMethodBodies(MethodInfo a, MethodInfo b)
            {
                if (!HasSameSignature(a, b))
                {
                    throw new ArgumentException("a and b must have have same signature");
                }

                RuntimeHelpers.PrepareMethod(a.MethodHandle);
                RuntimeHelpers.PrepareMethod(b.MethodHandle);

                unsafe
                {
                    if (IntPtr.Size == 4)
                    {
                        int* inj = (int*)b.MethodHandle.Value.ToPointer() + 2;
                        int* tar = (int*)a.MethodHandle.Value.ToPointer() + 2;

                        byte* injInst = (byte*)*inj;
                        byte* tarInst = (byte*)*tar;

                        int* injSrc = (int*)(injInst + 1);
                        int* tarSrc = (int*)(tarInst + 1);

                        int tmp = *tarSrc;
                        *tarSrc = (((int)injInst + 5) + *injSrc) - ((int)tarInst + 5);
                        *injSrc = (((int)tarInst + 5) + tmp) - ((int)injInst + 5);
                    }
                    else
                    {
                        throw new NotImplementedException($"{nameof(SwapMethodBodies)} doesn't yet handle IntPtr size of {IntPtr.Size}");
                    }
                }
            }

            private static bool HasSameSignature(MethodInfo a, MethodInfo b)
            {
                bool sameParams = !a.GetParameters().Any(x => !b.GetParameters().Any(y => x == y));
                bool sameReturnType = a.ReturnType == b.ReturnType;
                return sameParams && sameReturnType;
            }
        }
    }


    存在两个框架,允许您在运行时动态更改任何方法(它们使用用户152949提到的ICLRProfiling接口):

    • 普里格:免费和开源!
    • Microsoft Fakes:商业版,包含在Visual Studio高级版和终极版中,但不包括社区和专业版
    • Telerik Justmock:商业版,提供"Lite"版本
    • 型号模拟隔离器:商用

    还有一些框架使用.NET的内部结构进行模拟,这些框架可能更脆弱,并且可能无法更改内联代码,但另一方面,它们是完全独立的,不需要使用自定义启动程序。

    • 和声:麻省理工学院授权。似乎已经成功地在一些游戏模式中使用,同时支持.net和mono。
    • 偏差过程仪表引擎:GPLv3和商用。.NET支持目前标记为"实验性",但另一方面有商业支持的好处。


    可以在运行时使用ICLRProfiling接口替换方法。

  • 调用AttachProfiler以附加到进程。
  • 调用setilFunctionBody替换方法代码。
  • 有关详细信息,请参阅此日志。


    我知道这不是你问题的确切答案,但通常的方法是使用工厂/代理方法。

    首先我们声明一个基类型。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class SimpleClass
    {
        public virtual DTask<bool> Solve(int n, DEvent<bool> callback)
        {
            for (int m = 2; m < n - 1; m += 1)
                if (m % n == 0)
                    return false;
            return true;
        }
    }

    然后我们可以声明一个派生类型(称之为代理)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class DistributedClass
    {
        public override DTask<bool> Solve(int n, DEvent<bool> callback)
        {
            CodeToExecuteBefore();
            return base.Slove(n, callback);
        }
    }

    // At runtime

    MyClass myInstance;

    if (distributed)
        myInstance = new DistributedClass();
    else
        myInstance = new SimpleClass();

    派生类型也可以在运行时生成。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    public static class Distributeds
    {
        private static readonly ConcurrentDictionary<Type, Type> pDistributedTypes = new ConcurrentDictionary<Type, Type>();

        public Type MakeDistributedType(Type type)
        {
            Type result;
            if (!pDistributedTypes.TryGetValue(type, out result))
            {
                if (there is at least one method that have [Distributed] attribute)
                {
                    result = create a new dynamic type that inherits the specified type;
                }
                else
                {
                    result = type;
                }

                pDistributedTypes[type] = result;
            }
            return result;
        }

        public T MakeDistributedInstance<T>()
            where T : class
        {
            Type type = MakeDistributedType(typeof(T));
            if (type != null)
            {
                // Instead of activator you can also register a constructor delegate generated at runtime if performances are important.
                return Activator.CreateInstance(type);
            }
            return null;
        }
    }

    // In your code...

    MyClass myclass = Distributeds.MakeDistributedInstance<MyClass>();
    myclass.Solve(...);

    唯一的性能损失是在构造派生对象的过程中,第一次非常慢,因为它将使用大量的反射和反射发射。其他时候,它是并发表查找和构造函数的成本。如前所述,可以优化施工使用

    1
    ConcurrentDictionary<Type, Func<object>>.


    根据这个问题和另一个问题的答案,我提出了这个整理过的版本:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    public static unsafe MethodReplacementState Replace(this MethodInfo methodToReplace, MethodInfo methodToInject)
            {
    //#if DEBUG
                RuntimeHelpers.PrepareMethod(methodToReplace.MethodHandle);
                RuntimeHelpers.PrepareMethod(methodToInject.MethodHandle);
    //#endif
                MethodReplacementState state;

                IntPtr tar = methodToReplace.MethodHandle.Value;
                if (!methodToReplace.IsVirtual)
                    tar += 8;
                else
                {
                    var index = (int)(((*(long*)tar) >> 32) & 0xFF);
                    var classStart = *(IntPtr*)(methodToReplace.DeclaringType.TypeHandle.Value + (IntPtr.Size == 4 ? 40 : 64));
                    tar = classStart + IntPtr.Size * index;
                }
                var inj = methodToInject.MethodHandle.Value + 8;
    #if DEBUG
                tar = *(IntPtr*)tar + 1;
                inj = *(IntPtr*)inj + 1;
                state.Location = tar;
                state.OriginalValue = new IntPtr(*(int*)tar);

                *(int*)tar = *(int*)inj + (int)(long)inj - (int)(long)tar;
                return state;

    #else
                state.Location = tar;
                state.OriginalValue = *(IntPtr*)tar;
                * (IntPtr*)tar = *(IntPtr*)inj;
                return state;
    #endif
            }
        }

        public struct MethodReplacementState : IDisposable
        {
            internal IntPtr Location;
            internal IntPtr OriginalValue;
            public void Dispose()
            {
                this.Restore();
            }

            public unsafe void Restore()
            {
    #if DEBUG
                *(int*)Location = (int)OriginalValue;
    #else
                *(IntPtr*)Location = OriginalValue;
    #endif
            }
        }