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

时间:2011-09-04 12:07:51

标签: c# methods assemblies cil swap

我想要做的是改变C#方法在调用时的执行方式,这样我就可以编写如下内容:

[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;
}

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

目前我尝试了这段代码(假设t是Solve存储的类型,m是Solve的MethodInfo)

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并更新所有对Solve()的调用这样他们就会指向新副本。

11 个答案:

答案 0 :(得分:139)

对于.NET 4及更高版本

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("\nVersion x86 Debug\n");

                    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("\nVersion x86 Release\n");
                    *tar = *inj;
#endif
                }
                else
                {

                    long* inj = (long*)methodToInject.MethodHandle.Value.ToPointer()+1;
                    long* tar = (long*)methodToReplace.MethodHandle.Value.ToPointer()+1;
#if DEBUG
                    Console.WriteLine("\nVersion x64 Debug\n");
                    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("\nVersion x64 Release\n");
                    *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");
        }
    }

}

答案 1 :(得分:112)

Harmony是一个开源库,旨在在运行时替换,修饰或修改任何类型的现有C#方法。主要关注的是用 Mono 编写的游戏和插件,但该技术可以与任何.NET版本一起使用。它还负责对同一方法进行多次更改(它们累积而不是覆盖)。

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

为了完成这个过程,它将一个简单的汇编程序跳转写入原始方法的trampoline,指向通过编译动态方法生成的汇编程序。这适用于Windows,MacOS和Mono支持的任何Linux上的32 / 64Bit。

答案 2 :(得分:24)

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

看看:

http://www.codeproject.com/Articles/463508/NET-CLR-Injection-Modify-IL-Code-during-Run-time

基本上,你可以:

  1. 通过MethodInfo.GetMethodBody()获取IL方法内容.GetILAsByteArray()
  2. 混淆这些字节。

    如果您只是希望预先添加或附加一些代码,那么只需预先发布/附加您想要的操作码(但要注意保持堆栈清洁)

    以下是“解编”现有IL的一些提示:

    • 返回的字节是一系列IL指令,后跟它们的参数(如果它们有一些 - 例如,'。call'有一个参数:被调用的方法标记,'。pop'没有)
    • 可以使用OpCodes.YourOpCode.Value(这是保存在程序集中的真实操作码字节值)找到IL代码与返回数组中找到的字节之间的对应关系。
    • IL代码后附加的参数可能有不同的大小(从一个到几个字节),具体取决于名为
    • 的操作码
    • 您可以通过适当的方法找到这些参数所指的标记。例如,如果你的IL包含“.call 354354”(在hexa中编码为28 00 05 68 32,28h = 40为'.call'操作码和56832h = 354354),则可以使用MethodBase.GetMethodFromHandle找到相应的被调用方法(354354) )
  3. 修改后,您可以通过InjectionHelper.UpdateILCodes(MethodInfo方法,byte [] ilCodes)重新注入IL字节数组 - 请参阅上面提到的链接

    这是“不安全”的部分......它运作良好,但这包括黑客攻击内部CLR机制......

答案 3 :(得分:11)

如果方法是非虚拟的,非泛型的,非泛型的,非内联的,并且在x86平台上,你可以替换它:

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.

答案 4 :(得分:7)

Logman's solution,但有一个交换方法体的接口。另外,一个更简单的例子。

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;
        }
    }
}

答案 5 :(得分:4)

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

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

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;
    }
}

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

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();

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

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(...);

唯一的性能损失是在构造派生对象期间,第一次很慢,因为它会使用大量的反射和反射发射。 所有其他时间,它是并发表查找和构造函数的成本。 如上所述,您可以使用

优化构造
ConcurrentDictionary<Type, Func<object>>.

答案 6 :(得分:4)

您可以使用ICLRPRofiling Interface在运行时替换方法。

  1. 致电AttachProfiler以附加到流程。
  2. 致电SetILFunctionBody以替换方法代码。
  3. See this blog了解详情。

答案 7 :(得分:4)

有一些框架允许您在运行时动态更改任何方法(它们使用user152949提到的ICLRProfiling接口):

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

  • Harmony:麻省理工学院获得许可。似乎实际上已在几个游戏模块中成功使用,支持.NET和Mono。
  • Deviare In Process Instrumentation Engine:GPLv3和商业广告。 .NET支持目前标记为实验性,但另一方面具有商业支持的好处。

答案 8 :(得分:0)

基于对这个问题的回答以及另一个,IVE提出了这个经过整理的版本:

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
        }
    }

答案 9 :(得分:0)

看看Mono.Cecil:

self.val

答案 10 :(得分:0)

基于TakeMeAsAGuest's answer,这是一个类似的扩展名,不需要使用不安全块。

这是Extensions类:

using System;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace MethodRedirect
{
    static class Extensions
    { 
        public static void RedirectTo(this MethodInfo origin, MethodInfo target)
        {
            IntPtr ori = GetMethodAddress(origin);
            IntPtr tar = GetMethodAddress(target);
         
            Marshal.Copy(new IntPtr[] { Marshal.ReadIntPtr(tar) }, 0, ori, 1);
        }

        private static IntPtr GetMethodAddress(MethodInfo mi)
        {
            const ushort SLOT_NUMBER_MASK = 0xfff; // 3 bytes
            const int MT_OFFSET_32BIT = 0x28;      // 40 bytes
            const int MT_OFFSET_64BIT = 0x40;      // 64 bytes

            IntPtr address;

            // JIT compilation of the method
            RuntimeHelpers.PrepareMethod(mi.MethodHandle);

            IntPtr md = mi.MethodHandle.Value;             // MethodDescriptor address
            IntPtr mt = mi.DeclaringType.TypeHandle.Value; // MethodTable address

            if (mi.IsVirtual)
            {
                // The fixed-size portion of the MethodTable structure depends on the process type
                int offset = IntPtr.Size == 4 ? MT_OFFSET_32BIT : MT_OFFSET_64BIT;

                // First method slot = MethodTable address + fixed-size offset
                // This is the address of the first method of any type (i.e. ToString)
                IntPtr ms = Marshal.ReadIntPtr(mt + offset);

                // Get the slot number of the virtual method entry from the MethodDesc data structure
                // Remark: the slot number is represented on 3 bytes
                long shift = Marshal.ReadInt64(md) >> 32;
                int slot = (int)(shift & SLOT_NUMBER_MASK);
                
                // Get the virtual method address relative to the first method slot
                address = ms + (slot * IntPtr.Size);                                
            }
            else
            {
                // Bypass default MethodDescriptor padding (8 bytes) 
                // Reach the CodeOrIL field which contains the address of the JIT-compiled code
                address = md + 8;
            }

            return address;
        }
    }
}

这是一个简单的用法示例:

using System;
using System.Reflection;

namespace MethodRedirect
{
    class Scenario
    {    
      static void Main(string[] args)
      {
          Assembly assembly = Assembly.GetAssembly(typeof(Scenario));
          Type Scenario_Type = assembly.GetType("MethodRedirect.Scenario");

          MethodInfo Scenario_InternalInstanceMethod = Scenario_Type.GetMethod("InternalInstanceMethod", BindingFlags.Instance | BindingFlags.NonPublic);
          MethodInfo Scenario_PrivateInstanceMethod = Scenario_Type.GetMethod("PrivateInstanceMethod", BindingFlags.Instance | BindingFlags.NonPublic);

          Scenario_InternalInstanceMethod.RedirectTo(Scenario_PrivateInstanceMethod);

          // Using dynamic type to prevent method string caching
          dynamic scenario = (Scenario)Activator.CreateInstance(Scenario_Type);

          bool result = scenario.InternalInstanceMethod() == "PrivateInstanceMethod";

          Console.WriteLine("\nRedirection {0}", result ? "SUCCESS" : "FAILED");

          Console.ReadKey();
      }

      internal string InternalInstanceMethod()
      {
          return "InternalInstanceMethod";
      }

      private string PrivateInstanceMethod()
      {
          return "PrivateInstanceMethod";
      }
    }
}

这摘自我在Github(MethodRedirect)上提供的更详细的项目。