Динамически заменять содержимое метода 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;
}

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

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

9 58

9 ответов:

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

для .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.Ваш код.Значение (которое является реальным значением байта кода операции, сохраненным в вашей сборке)
    • Аргументы, добавляемые после кодов 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):

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

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

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