Lo que quiero hacer es cambiar la forma en que se ejecuta un método de C # cuando se llama, para poder 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 métodos que tienen el atributo Distribuido (lo cual 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 comienzo de la función (en tiempo de compilación; hacerlo en tiempo de ejecución es el objetivo).
Por el momento, he intentado este bit de código (suponga que t es el tipo en el que está almacenado 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, MethodRental.SwapMethodBody solo funciona en módulos dinámicos; no los que ya se han compilado y almacenado en el ensamblaje.
Así que estoy buscando una manera de hacer SwapMethodBody de manera efectiva en un método que ya está almacenado en un ensamblado 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 dinámico, pero en este caso necesito encontrar una manera de copiar a través de IL, así como actualizar todas las llamadas a Solve () de modo que apuntaría a la nueva copia.
Respuestas:
Harmony 2 es una biblioteca de código abierto (licencia MIT) diseñada para reemplazar, decorar o modificar métodos de C # existentes de cualquier tipo durante el tiempo de ejecución. Su enfoque principal son los juegos y complementos escritos en Mono o .NET. Se encarga de varios cambios en el mismo método: se acumulan en lugar de sobrescribirse entre sí.
Crea métodos de reemplazo dinámicos para cada método original y les emite un 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 y los manejadores de excepciones personalizados, lo 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 compilación del método dinámico. Esto funciona para 32 / 64Bit en Windows, macOS y cualquier Linux compatible con Mono.
La documentación se puede encontrar aquí .
Ejemplo
( Fuente )
Codigo original
Parcheo con anotaciones Harmony
Alternativamente, parcheo manual con reflexión
fuente
Memory.WriteJump
)?48 B8 <QWord>
mueve un valor inmediato de QWord arax
, luegoFF E0
estájmp rax
: ¡todo claro allí! Mi pregunta restante es sobre elE9 <DWord>
caso (un salto cercano): parece que en este caso el salto cercano se conserva y la modificación está en el objetivo del salto; ¿Cuándo genera Mono tal código en primer lugar y por qué recibe este tratamiento especial?Para .NET 4 y superior
fuente
this
dentroinjectionMethod*()
, hará referencia a unaInjection
instancia durante el tiempo de compilación , pero unaTarget
instancia durante el tiempo de ejecución (esto es cierto para todas las referencias a miembros de instancia que usa dentro de un inyectado método). (2) Por alguna razón, la#DEBUG
pieza solo funcionaba cuando se depuraba una prueba, pero no cuando se ejecutaba una prueba que había sido depurada y compilada. Terminé siempre utilizando la#else
parte. No entiendo por qué esto funciona, pero funciona.Debugger.IsAttached
lugar de#if
preprocesadorPUEDE modificar el contenido de un método en tiempo de ejecución. Pero se supone que no debe hacerlo, y se recomienda encarecidamente que lo guarde para fines de prueba.
Eche un vistazo a:
http://www.codeproject.com/Articles/463508/NET-CLR-Injection-Modify-IL-Code-during-Run-time
Básicamente, puedes:
Ensucia estos bytes.
Si solo desea anteponer o agregar algún código, simplemente anteponga / agregue los códigos de operación que desee (tenga cuidado de dejar la pila limpia)
A continuación, se ofrecen algunos consejos para "descompilar" IL existente:
Una vez modificada, su matriz de bytes IL se puede reinyectar a través de InjectionHelper.UpdateILCodes (método MethodInfo, byte [] ilCodes) - vea el enlace mencionado anteriormente
Esta es la parte "insegura" ... Funciona bien, pero esto consiste en hackear los mecanismos internos de CLR ...
fuente
puede reemplazarlo si el método no es virtual, no genérico, no es de tipo genérico, no está en línea y en plataforma x86:
fuente
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):
También hay algunos marcos que se burlan de los componentes internos de .NET, estos probablemente sean más frágiles y probablemente no puedan cambiar el código en línea, pero por otro lado son completamente autónomos y no requieren que use un lanzador personalizado.
fuente
La solución de Logman , pero con una interfaz para intercambiar cuerpos de métodos. Además, un ejemplo más simple.
fuente
Basándome en la respuesta a esta pregunta y a otra, se me ocurrió esta versión ordenada:
fuente
Puede reemplazar un método en tiempo de ejecución utilizando la interfaz ICLRPRofiling .
Consulte este blog para obtener más detalles.
fuente
Sé que no es la respuesta exacta a su pregunta, pero la forma habitual de hacerlo es utilizando el enfoque de fábricas / proxy.
Primero declaramos un tipo base.
Entonces podemos declarar un tipo derivado (llamarlo proxy).
El tipo derivado también se puede generar en tiempo de ejecución.
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 emisión de reflexión. Todas las demás veces, es el costo de una consulta de tabla simultánea y un constructor. Como se dijo, puede optimizar la construcción usando
fuente