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

Lo que quiero hacer es cambiar la forma en que un método C # se ejecuta cuando se llama, de modo que pueda escribir algo como esto:

[Distributed] public DTask Solve(int n, DEvent 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 (que ya puedo) y luego insertar el 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 comienzo de la función (en tiempo de comstackción, hacerlo en tiempo de ejecución es el objective).

Por el momento, he intentado este bit de código (suponiendo que t es el tipo en el que se almacena Solve, y m es un MethodInfo of 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 ReplacedSolve(int n, DEvent callback) { Console.WriteLine("This was executed instead!"); return true; } 

Sin embargo, MethodRental.SwapMethodBody solo funciona en módulos dynamics; no aquellos que ya han sido comstackdos y almacenados en el ensamblado.

Así que estoy buscando una forma de hacer SwapMethodBody con eficacia en un método que ya está almacenado en un ensamblaje cargado y en ejecución .

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

Solo piense en las implicaciones si esto fuera posible. Podría, por ejemplo, reemplazar el contenido de la clase String y causar esgulps. Una vez que el CLR carga un método, no se puede modificar. Puede echar un vistazo a AOP y bibliotecas como Castle DynamicProxy que se utilizan en marcos de burla como Rhino Mocks.

Para .NET 4 y superior

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

Harmony es una biblioteca de código abierto diseñada para reemplazar, decorar o modificar los métodos existentes de C # de cualquier tipo durante el tiempo de ejecución. Su foco principal son los juegos y complementos escritos en Mono, pero la técnica se puede usar con cualquier versión de .NET. También se ocupa 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 que permite una manipulación más detallada del método original.

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

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

Solo eche 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. Lío con estos bytes.

    Si solo desea anteponer o anexar algún código, simplemente preprend / anexe los códigos de operación que desee (tenga cuidado al dejar la stack limpia, sin embargo)

    Aquí hay algunos consejos para “eliminar” el IL existente:

    • Los bytes devueltos son una secuencia de instrucciones IL, seguidas por sus argumentos (si tienen algunos, por ejemplo, ‘.call’ tiene un argumento: el token del método llamado, y ‘.pop’ no tiene ninguno)
    • La correspondencia entre códigos IL y bytes que encuentre en la matriz devuelta se puede encontrar utilizando OpCodes.YourOpCode.Value (que es el valor real de byte de código de operación tal como se guardó en su conjunto)
    • Los argumentos anexados después de los códigos IL pueden tener diferentes tamaños (de uno a varios bytes), dependiendo del código de operación llamado
    • Puede encontrar tokens a los que se refieren estos argumentos a través de métodos apropiados. Por ejemplo, si su IL contiene “.call 354354” (codificado como 28 00 05 68 32 en hexa, 28h = 40 es código de operación ‘.call’ y 56832h = 354354), el método correspondiente se puede encontrar usando MethodBase.GetMethodFromHandle (354354) )
  3. Una vez modificada, su array de bytes IL puede reinyectarse a través de InjectionHelper.UpdateILCodes (método MethodInfo, byte [] ilCodes) – ver el enlace mencionado anteriormente

    Esta es la parte “insegura” … Funciona bien, pero esto consiste en piratear los mecanismos internos de CLR …

puede reemplazarlo si el método no es virtual, no es genérico, no está en tipo genérico, no está en línea y está en formato de placa x86:

 MethodInfo methodToReplace = ... RuntimeHelpers.PrepareMetod(methodToReplace.MethodHandle); var getDynamicHandle = Delegate.CreateDelegate(Metadata>.Type, Metadata.Type.GetMethod("GetMethodDescriptor", BindingFlags.Instance | BindingFlags.NonPublic)) as Func; 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. 

La 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 { ///  /// Swaps the function pointers for a and b, effectively swapping the method bodies. ///  ///  /// a and b must have same signature ///  /// Method to swap /// Method to swap 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; } } } 

Sé que no es la respuesta exacta a su pregunta, pero la forma habitual de hacerlo es utilizar fábricas / enfoque de proxy.

Primero declaramos un tipo base.

 public class SimpleClass { public virtual DTask Solve(int n, DEvent callback) { for (int m = 2; m < n - 1; m += 1) if (m % n == 0) return false; return true; } } 

Entonces podemos declarar un tipo derivado (llámalo proxy).

 public class DistributedClass { public override DTask Solve(int n, DEvent 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 pDistributedTypes = new ConcurrentDictionary(); 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() 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.Solve(...); 

La única pérdida de rendimiento es durante la construcción del objeto derivado, la primera vez es bastante lenta porque usará mucha reflexión y emisión de reflexión. El rest del tiempo, es el costo de una búsqueda simultánea de tabla y un constructor. Como se dijo, puede optimizar la construcción usando

 ConcurrentDictionary>. 

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

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

Mira este blog para más detalles.

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

  • Prig : ¡Gratis y de código abierto!
  • Microsoft Fakes : Commercial, incluido en Visual Studio Premium y Ultimate pero no en Community y Professional
  • Telerik JustMock : comercial, una versión “lite” está disponible
  • Aislante tipo pistola : comercial

También hay unos pocos frameworks que se burlan con las partes internas 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 uses un lanzador personalizado

  • Harmony : licencia MIT. Parece que realmente se ha utilizado con éxito en algunos modos de juego, es compatible con .NET y Mono.
  • Deviare en el motor de instrumentación de proceso : GPLv3 y comercial. El soporte de .NET actualmente marcado como experimental, pero por otro lado tiene el beneficio de ser respaldado comercialmente.