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

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

В данный момент я попытался выполнить этот бит кода (предположим, что t - это тип, в котором хранится Solve, а m - это MethodInfo из Solve):

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

9 ответов

Решение

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

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

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

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

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

}

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

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

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

По сути, вы можете:

  1. Получить содержимое метода IL с помощью MethodInfo.GetMethodBody().GetILAsByteArray()
  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. После изменения ваш байтовый массив IL можно повторно внедрить с помощью InjectionHelper.UpdateILCodes(метод MethodInfo, byte[] ilCodes) - см. Ссылку, указанную выше.

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

Основываясь на ответе на этот и другой вопрос, я придумал эту приведенную в порядок версию:

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

Вы можете заменить его, если метод не виртуальный, не универсальный, не универсального типа, не встроенный и не на платформе 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.

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

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

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

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

Основываясь на ответе TakeMeAsAGuest, вот аналогичное расширение, которое не требует использования небезопасных блоков.

Вот 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).

Решение 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;
        }
    }
}

Вы можете заменить метод во время выполнения с помощью интерфейса ICLRPRofiling.

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

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

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

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

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

Загляните в Mono.Cecil:

using Mono.Cecil;
using Mono.Cecil.Inject;

public class Patcher
{    
   public void Patch()
   {
    // Load the assembly that contains the hook method
    AssemblyDefinition hookAssembly = AssemblyLoader.LoadAssembly("MyHookAssembly.dll");
    // Load the assembly
    AssemblyDefinition targetAssembly = AssemblyLoader.LoadAssembly("TargetAssembly.dll");

    // Get the method definition for the injection definition
    MethodDefinition myHook = hookAssembly.MainModule.GetType("HookNamespace.MyHookClass").GetMethod("MyHook");
    // Get the method definition for the injection target. 
    // Note that in this example class Bar is in the global namespace (no namespace), which is why we don't specify the namespace.
    MethodDefinition foo = targetAssembly.MainModule.GetType("Bar").GetMethod("Foo");

    // Create the injector
    InjectionDefinition injector = new InjectionDefinition(foo, myHook, InjectFlags.PassInvokingInstance | InjectFlags.passParametersVal);

    // Perform the injection with default settings (inject into the beginning before the first instruction)
    injector.Inject();

    // More injections or saving the target assembly...
   }
}
Другие вопросы по тегам