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


108

我想做的是更改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()的所有调用的方法。将指向新副本。


3
无法交换已加载的方法。否则,Spring.Net不必使用代理和接口来制造奇怪的东西:-)阅读此问题,它与您的问题相切:stackoverflow.com/questions/25803/…(如果您可以拦截它,则可以像-交换它...如果您不能1,那么显然您不能2)。
xanatos 2011年

在那种情况下,是否有办法将方法复制到动态模块中,并更新程序集的其余部分,以使对该方法的调用指向新副本?
June Rhodes

一样的老。如果可以轻松完成,那么所有各种IoC容器都可能做到这一点。他们不这么做-> 99%不可能做到:-)(没有可怕且无可辩驳的骇客)。有一个希望:他们承诺在C#5.0中进行元编程和异步处理。我们已经看到了异步...没有元编程...但是可能是这样!
xanatos 2011年

1
您确实没有解释为什么要让自己接受如此痛苦的事情。
DanielOfTaebl 2011年

6
请在下面查看我的答案。这完全有可能。在您不拥有的代码上以及在运行时。我不明白为什么这么多人认为这是不可能的。
安德烈亚斯·帕迪克

Answers:


201

披露:和谐是由我(本文的作者)编写并维护的一个库。

Harmony 2是一个开放源代码库(MIT许可证),旨在在运行时替换,修饰或修改任何类型的现有C#方法。它的主要重点是用Mono或.NET编写的游戏和插件。它需要对同一方法进行多次更改-它们会累积而不是彼此覆盖。

它为每个原始方法创建动态替换方法,并向它们发出代码,这些代码在开始和结束时调用自定义方法。它还允许您编写过滤器以处理原始IL代码和自定义异常处理程序,从而可以对原始方法进行更详细的操作。

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

文档可以在这里找到。

来源

原始码

public class SomeGameClass
{
    private bool isRunning;
    private int counter;

    private int DoSomething()
    {
        if (isRunning)
        {
            counter++;
            return counter * 10;
        }
    }
}

使用Harmony批注进行修补

using SomeGame;
using HarmonyLib;

public class MyPatcher
{
    // make sure DoPatching() is called at start either by
    // the mod loader or by your injector

    public static void DoPatching()
    {
        var harmony = new Harmony("com.example.patch");
        harmony.PatchAll();
    }
}

[HarmonyPatch(typeof(SomeGameClass))]
[HarmonyPatch("DoSomething")]
class Patch01
{
    static FieldRef<SomeGameClass,bool> isRunningRef =
        AccessTools.FieldRefAccess<SomeGameClass, bool>("isRunning");

    static bool Prefix(SomeGameClass __instance, ref int ___counter)
    {
        isRunningRef(__instance) = true;
        if (___counter > 100)
            return false;
        ___counter = 0;
        return true;
    }

    static void Postfix(ref int __result)
    {
        __result *= 2;
    }
}

或者,通过反射进行手动修补

using SomeGame;
using HarmonyLib;

public class MyPatcher
{
    // make sure DoPatching() is called at start either by
    // the mod loader or by your injector

    public static void DoPatching()
    {
        var harmony = new Harmony("com.example.patch");

        var mOriginal = typeof(SomeGameClass).GetMethod("DoSomething", BindingFlags.Instance | BindingFlags.NonPublic);
        var mPrefix = typeof(MyPatcher).GetMethod("MyPrefix", BindingFlags.Static | BindingFlags.Public);
        var mPostfix = typeof(MyPatcher).GetMethod("MyPostfix", BindingFlags.Static | BindingFlags.Public);
        // add null checks here

        harmony.Patch(mOriginal, new HarmonyMethod(mPrefix), new HarmonyMethod(mPostfix));
    }

    public static void MyPrefix()
    {
        // ...
    }

    public static void MyPostfix()
    {
        // ...
    }
}

看看源代码,很有趣!您能(在此处和/或在文档中)解释用于执行跳转的特定指令如何工作(在中Memory.WriteJump)?
汤姆(Tom),

要部分地回答我的评论:48 B8 <QWord>移动一个四字立即值rax,则FF E0jmp rax-所有清楚那里!我剩下的问题是关于E9 <DWord>情况(近距离跳转)的:在这种情况下,近距离跳转似乎保留了下来,修改是针对该跳转的目标的;Mono何时会生成此类代码,为什么会得到这种特殊待遇?
汤姆(Tom),

1
至于我可以告诉它不支持.NET睿2呢,让一些例外与AppDomain.CurrentDomain.DefineDynamicAssembly
马克斯

1
我的一个朋友0x0ade确实向我提到过,在.NET Core上还有一个不太成熟的替代方法,即NuGet上的MonoMod.RuntimeDetour。
Andreas Pardeike '19

1
更新:通过包含对System.Reflection.Emit的引用,Harmony现在可以使用.NET Core 3编译并测试OK
Andreas Pardeike,

181

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

}

14
这值得更多的赞扬。我有一个完全不同的方案,但是此代码段正是我需要设定的正确方向。谢谢。
SC 2002年

2
@Logman很好的答案。但是我的问题是:调试模式下发生了什么?并且可以仅替换一条指令吗?例如,如果要替换无条件跳转上的条件跳转?AFAIK您正在替换编译方法,因此要确定应替换哪种条件并不容易...
Alex Zhukovskiy

2
@AlexZhukovskiy,如果您喜欢将其张贴在堆栈上并发送给我链接。周末之后,我会调查一下并给您答复。机器,周末过后我也会调查您的问题。
Logman'8

2
在与MSTest进行集成测试时,我注意到两件事:(1)在this内部使用时injectionMethod*(),它将Injection编译时引用一个实例,但Target运行时引用一个实例(对于在注入对象内部使用的所有对实例成员的引用都是如此方法)。(2)由于某种原因,该#DEBUG部件仅在调试测试时有效,而在运行已调试编译的测试时则无效。我最终总是使用#else零件。我不明白为什么会这样,但确实可以。
晚安书呆子自豪

2
非常好。是时候打破一切了!@GoodNightNerdPride使用Debugger.IsAttached而不是#if 预处理程序
M.kazem Akhgary

25

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

看看:

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

基本上,您可以:

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

    如果您只是想添加或添加一些代码,则只需添加/添加所需的操作码即可(请注意保持堆栈清洁)

    以下是“反编译”现有IL的一些技巧:

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

    这是“不安全的”部分...运作良好,但这包括破解内部CLR机制...


7
出于学问的考虑,354354(0x00056832)不是有效的元数据令牌,高位字节应为0x06(MethodDef),0x0A(MemberRef)或0x2B(MethodSpec)。同样,元数据令牌应以小端字节顺序写入。最后,元数据令牌是特定于模块的,MethodInfo.MetadataToken将从声明模块返回令牌,如果您要调用的方法与正在修改的方法不在同一模块中,则使其无法使用。
Brian Reichle,2015年

13

如果方法是非虚拟的,非通用的,非通用类型,非内联的并且在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.

看起来很危险。我真的希望没有人在生产代码中使用它。
Brian Reichle

2
应用程序性能监视(APM)工具使用了这种方法,并且在生产中也使用了这种方法。
Martin Kersten 2015年

1
感谢您的答复,我正在一个项目中提供这种功能,例如,面向方面的编程API。我解决了在x86和x64上同时管理虚拟方法和通用方法的限制。让我知道您是否需要更多详细信息。
Teter28年

6
什么是元数据类?
塞巴斯蒂安

这个答案是伪代码,已经过时。许多方法不再存在。
N-ate

9

有两个框架可让您在运行时动态更改任何方法(它们使用user152949提到的ICLRProfiling接口):

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

  • 和谐:麻省理工学院许可。似乎实际上已经在一些游戏mod中成功使用,同时支持.NET和Mono。
  • 过程仪表引擎中的Deviare:GPLv3和商业版。.NET支持目前标记为实验性的,但另一方面却具有商业支持的好处。

8

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

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
这给了我:MA.ELCalc.FunctionalTests.dll中发生了'System.AccessViolationException'类型的异常,但未在用户代码中处理。其他信息:尝试读取或写入受保护的内存。这通常表示在更换吸气剂时其他内存已损坏。
N-ate

我收到异常消息“ wapMethodBodies尚未处理8的IntPtr大小”
Phong Dao

6

根据对这个问题的答案以及另一个问题,香港专业教育学院提出了这个整理好的版本:

// Note: This method replaces methodToReplace with methodToInject
// Note: methodToInject will still remain pointing to the same location
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
        }
    }

目前,这是最好的答案
尤金·戈博沃

添加一个使用示例将很有帮助
kofifus


3

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

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

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

1
嗯..仍然需要代表程序员进行工作才能主动了解分布式处理;我一直在寻找一种解决方案,该解决方案仅依赖于它们在方法上设置[Distributed]属性(而不是子类化或从ContextBoundObject继承)。看起来我可能需要使用Mono.Cecil或类似的东西对程序集进行一些后期编译修改。
June Rhodes

我不会说,这是通常的方式。就所需的技能而言,这种方法很简单(无需了解CLR),但是对于每个替换的方法/类,都需要重复相同的步骤。如果以后要更改某些内容(例如,在之后而不是之前执行一些代码),则必须执行N次(与不安全的代码相反,该操作需要执行一次)。因此,这是N个小时的工作vs 1个小时的工作)
尤金·戈博沃
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.