Динамически заменять содержимое метода C#?

что я хочу сделать, это изменить способ выполнения метода 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;
}

во время выполнения мне нужно иметь возможность анализировать методы, которые имеют распределенный атрибут (что я уже могу сделать), а затем вставлять код до выполнения тела функции и после возвращения функции. Что еще более важно, мне нужно иметь возможность делать это без изменения кода, где вызывается Solve или в начале функции (во время компиляции; цель состоит в том, чтобы сделать это во время выполнения).

На данный момент я попытался этот кусок кода (предположим, что t-это тип, в котором хранится решение, а m-Методинфо решения):

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, а также обновить все вызовы для решения (), чтобы они указывали на новую копию.

9 ответов


просто подумайте о последствиях, если бы это было возможно. Вы можете, например, заменить содержимое String класс и нанести ущерб. После загрузки метода средой CLR его нельзя изменить. Вы можете взглянуть на AOP и библиотеки, такие как Замок DynamicProxy которые используются насмешливыми фреймворками, такими как Rhino Mocks.


для .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");
        }
    }

}

гармония - это библиотека с открытым исходным кодом, предназначенная для замены, украшения или изменения существующих методов C# любого типа во время выполнения. Основное внимание уделяется играм и плагинам, написанным в моно но метод можно использовать с любой версией .NET. Он также заботится о нескольких изменениях одного и того же метода (они накапливаются вместо перезаписи).

Он создает методы типа DynamicMethod для каждого исходного метода и выдает ему код, который вызывает пользовательские методы в начале и в конце. Он также позволяет писать фильтры для обработки исходного кода IL, что позволяет более подробно манипулировать оригинальным методом.

чтобы завершить процесс, он записывает простой прыжок ассемблера в батут исходного метода, который указывает на ассемблер, сгенерированный из компиляции динамического метода. Это работает для 32 / 64Bit на Windows, macOS и любой Linux, который поддерживает Mono.


вы можете изменить содержимое метода во время выполнения. Но вы не должны, и настоятельно рекомендуется сохранить это для целей тестирования.

просто взгляните на:

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

в принципе, вы можете:

  1. получить содержимое метода IL через MethodInfo.GetMethodBody ().GetILAsByteArray ()
  2. возиться с этим байты.

    Если вы просто хотите добавить или добавить какой-то код, то просто preprend / append opcodes вы хотите (будьте осторожны, оставляя стек чистым, хотя)

    вот несколько советов по "uncompile" существующий IL:

    • возвращаемые байты представляют собой последовательность инструкций IL, за которыми следуют их аргументы(если они имеют некоторые - например,'.вызов "имеет один аргумент: токен вызываемого метода и".поп-нет)
    • соответствие между кодами IL и байты, которые вы найдете в возвращаемом массиве, можно найти с помощью OpCodes.YourOpCode.Значение (которое является реальным значением байта кода операции, сохраненным в вашей сборке)
    • Аргументы, добавленные после кодов IL, могут иметь разные размеры (от одного до нескольких байтов), в зависимости от кода операции
    • вы можете найти токены, на которые ссылаются аргументы тезисов с помощью соответствующих методов. Например, если ваш IL содержит ".вызов 354354 " (кодируется как 28 00 05 68 32 в hexa, 28h=40 being '.называть операции и 56832h=354354), соответствующий вызываемый метод можно найти с помощью MethodBase.GetMethodFromHandle (354354)
  3. после изменения, вы Il байтовый массив может быть повторно введен через InjectionHelper.UpdateILCodes (метод MethodInfo, byte[] ilCodes) - см. ссылку, указанную выше

    это "небезопасная" часть... Он работает хорошо, но это заключается в взломе внутренних механизмов CLR...


вы можете заменить его, если метод не является виртуальным, не общим, не в общем типе, не встроен и на x86 plateform:

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.

Логман по, но с интерфейсом для замены тела метода. Кроме того, более простой пример.

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

Я знаю, что это не точный ответ на ваш вопрос, но обычный способ сделать это, используя фабрик/ - прокси.

Сначала мы объявляем базовый тип.

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

вы можете заменить метод во время выполнения, используя Интерфейс ICLRPRofiling.

  1. вызов AttachProfiler присоединить к процессу.
  2. вызов SetILFunctionBody для замены кода метода.

посмотреть этот блог для более подробной информации.


существует несколько фреймворков, которые позволяют динамически изменять любой метод во время выполнения (они используют интерфейс ICLRProfiling, упомянутый user152949):

  • Prig: бесплатный и с открытым исходным кодом!
  • Microsoft Подделки: коммерческий, входит в Visual Studio Premium и Ultimate, но не сообщество и профессионал
  • JustMock Из Telerik: коммерчески, версия" lite" доступно
  • Typemock Изолятор: коммерческие

есть также несколько фреймворков, которые издеваются над внутренними элементами .NET, они, вероятно, более хрупкие и, вероятно, не могут изменять встроенный код, но, с другой стороны, они полностью автономны и не требуют использования пользовательского лаунчера.

  • гармония: лицензия MIT. Кажется, на самом деле успешно использовались в нескольких игровых модах, поддерживает оба .NET и Mono.
  • Deviare В Процессе Инструментирования Двигателя: GPLv3 и коммерческий. Поддержка .NET в настоящее время отмечена как экспериментальная, но, с другой стороны, имеет преимущество коммерческой поддержки.