Reemplazar dinámicamente el contenido de un método de C#?


Lo que quiero hacer es cambiar cómo se ejecuta un método C# cuando se llama, para que pueda escribir algo como esto:

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

En tiempo de ejecución, necesito poder analizar los métodos que tienen el atributo Distribuido (lo que ya puedo hacer) y luego insertar código antes de que se ejecute el cuerpo de la función y después de que la función regrese. Más importante aún, necesito poder hacerlo sin modificar el código donde se llama a Solve o al inicio de la función( en tiempo de compilación; hacerlo en tiempo de ejecución es el objetivo).

En este momento he intentado este bit de código (supongamos que t es el tipo en el que se almacena Solve, y m es un MethodInfo de 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;
}

Sin embargo, metodológica.SwapMethodBody solo funciona en módulos dinámicos; no en aquellos que ya han sido compilados y almacenados en el ensamblado.

Así que estoy buscando una manera de hacer de manera efectiva SwapMethodBody en un método que ya está almacenado en una carga y ejecución asamblea.

Tenga en cuenta que no es un problema si tengo que copiar completamente el método en un módulo dinámico, pero en este caso necesito encontrar una manera de copiar a través de la IL, así como actualizar todas las llamadas a Solve() de modo que apunten a la nueva copia.

Author: starblue, 2011-09-04

9 answers

Basta con pensar en las implicaciones si esto fuera posible. Por ejemplo, podría reemplazar el contenido de la clase String y causar estragos. Una vez que un método es cargado por el CLR no puede ser modificado. Puede echar un vistazo a AOP y bibliotecas como Castle DynamicProxy que se utilizan para burlarse de frameworks como Rhino Mocks.

 -21
Author: Darin Dimitrov,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-09-18 00:18:30

Para. NET 4 y superiores

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

}
 111
Author: Logman,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-01-06 21:25:08

Harmony es una biblioteca de código abierto diseñada para reemplazar, decorar o modificar métodos C# existentes de cualquier tipo durante el tiempo de ejecución. Su enfoque principal son los juegos y plugins escritos en Mono pero la técnica se puede utilizar con cualquier versión.NET. También se encarga de múltiples cambios en el mismo método (se acumulan en lugar de sobrescribir).

Crea métodos de tipo DynamicMethod para cada método original y le emite código que llama a métodos personalizados al principio y al final. También le permite escribir filtros para procesar el código IL original, lo que permite una manipulación más detallada del método original.

Para completar el proceso, escribe un simple salto ensamblador en el trampolín del método original que apunta al ensamblador generado a partir de la compilación del método dinámico. Esto funciona para 32 / 64Bit en Windows, macOS y cualquier Linux que Mono soporte.

 71
Author: Andreas Pardeike,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-02-04 16:56:34

PUEDE modificar el contenido de un método en tiempo de ejecución. Pero se supone que no debes, y se recomienda encarecidamente conservarlo para fines de prueba.

Solo echa un vistazo a:

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

Básicamente, puedes:

  1. Obtenga el contenido del método IL a través de MethodInfo.GetMethodBody().GetILAsByteArray ()
  2. Mezcle estos bytes.

    Si solo desea anteponer o agregue algún código, luego solo preprenda/anexe los opcodes que desee (tenga cuidado al dejar la pila limpia, sin embargo)

    Aquí hay algunos consejos para" descompilar " IL existente:

    • Los bytes devueltos son una secuencia de instrucciones IL, seguidas de sus argumentos (si tienen algunos - por ejemplo, '.call 'tiene un argumento: el token del método llamado, y'.pop' no tiene ninguna)
    • La correspondencia entre los códigos IL y los bytes que se encuentran en la matriz devuelta se puede encontrar utilizando OpCodes.YourOpCode.Valor (que es el valor real del byte del opcode guardado en su ensamblado)
    • Los argumentos que se añaden después de los códigos IL pueden tener diferentes tamaños (de uno a varios bytes), dependiendo del opcode llamado
    • Puede encontrar tokens a los que estos argumentos se refieren a través de métodos apropiados. Por ejemplo, si su IL contiene ".llame al 354354" (codificado como 28 00 05 68 32 en hexa, 28h=40 siendo ".call ' opcode y 56832h=354354), el método llamado correspondiente se puede encontrar usando MethodBase.GetMethodFromHandle (354354)
  3. Una vez modificado, su matriz de bytes puede ser reinyectada a través de InjectionHelper.UpdateILCodes (MethodInfo method, byte[] ilCodes) - ver enlace mencionado anteriormente

    Esta es la parte "insegura"... Funciona bien, pero esto consiste en hackear mecanismos internos de CLR...

 23
Author: Olivier,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-10-16 12:54:15

Puede reemplazarlo si el método no es virtual, no genérico, no en tipo genérico, no en línea y en plataforma 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.
 10
Author: Teter28,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-12-30 14:25:00

Solución de Logman, pero con una interfaz para intercambiar cuerpos de métodos. Además, un ejemplo más simple.

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;
        }
    }
}
 6
Author: C. McCoy IV,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-08-30 03:48:20

Sé que no es la respuesta exacta a tu pregunta, pero la forma habitual de hacerlo es usando el enfoque factories/proxy.

Primero declaramos un tipo base.

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

Entonces podemos declarar un tipo derivado (llamarlo proxy).

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

El tipo derivado también se puede generar en tiempo de ejecución.

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

La única pérdida de rendimiento es durante la construcción del objeto derivado, la primera vez es bastante lenta porque utilizará una gran cantidad de reflexión y reflexión emiten. Todas las demás veces, es el costo de una búsqueda de tabla simultánea y un constructor. Como se ha dicho, puede optimizar la construcción utilizando

ConcurrentDictionary<Type, Func<object>>.
 4
Author: Salvatore Previti,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-09-04 13:21:48

Puede reemplazar un método en tiempo de ejecución utilizando la interfaz ICLRPRofiling.

  1. Llama a AttachProfiler para adjuntar al proceso.
  2. Llama a SetILFunctionBody para reemplazar el código del método.

Ver este blog para más detalles.

 4
Author: ,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-06-27 10:50:14

Existen un par de frameworks que permiten cambiar dinámicamente cualquier método en tiempo de ejecución (utilizan la interfaz ICLRProfiling mencionada por user152949):

También hay algunos frameworks que se burlan de los internos de.NET, estos son probablemente más frágiles, y probablemente no pueden cambiar el código en línea, pero por otro lado son completamente autónomos y no requieren que use un lanzador personalizado.

  • Harmony : Licencia MIT. Parece que en realidad se han utilizado con éxito en algunos mods de juegos, soporta tanto. NET y Mono.
  • Desviación En El Motor De Instrumentación De Proceso: GPLv3 y Comercial. Soporte. NET actualmente marcado como experimental, pero por otro lado tiene el beneficio de ser respaldado comercialmente.
 2
Author: poizan42,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-06-19 12:00:28