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

问题描述 投票:75回答:9

我想要做的是改变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()的调用,以便它们会指向新副本。

c# methods assemblies cil swap
9个回答
150
投票

对于.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");
        }
    }

}

119
投票

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

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

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


25
投票

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

看看:

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机制......

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.

8
投票

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

还有一些框架可以使用.NET内部进行调整,这些框架可能更脆弱,并且可能无法更改内联代码,但另一方面它们完全是自包含的,并且不需要您使用自定义启动器。

  • qazxsw poi:麻省理工学院授权。似乎实际上已经在一些游戏mod中成功使用,支持.NET和Mono。
  • Harmony:GPLv3和商业。 .NET支持目前标记为实验性,但另一方面具有商业支持的好处。

7
投票

Deviare In Process Instrumentation Engine,但有一个交换方法体的接口。另外,一个更简单的例子。

Logman's solution

5
投票

您可以使用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; } } } 在运行时替换方法。

  1. 请致电ICLRPRofiling Interface以附加到流程。
  2. 调用AttachProfiler来替换方法代码。

SetILFunctionBody了解更多详情。


4
投票

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

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

See this blog

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

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

1
投票

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

ConcurrentDictionary<Type, Func<object>>.
© www.soinside.com 2019 - 2024. All rights reserved.