Динамически заменить содержимое метода С#?
Что я хочу сделать, это изменить способ выполнения С# метода при его вызове, чтобы я мог написать что-то вроде этого:
[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 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<bool> ReplacedSolve(int n, DEvent<bool> callback)
{
Console.WriteLine("This was executed instead!");
return true;
}
Однако MethodRental.SwapMethodBody работает только с динамическими модулями; а не те, которые уже были скомпилированы и сохранены в сборке.
Итак, я ищу способ эффективно выполнить SwapMethodBody на методе, который уже сохранен в загруженной и исполняемой сборке.
Обратите внимание: это не проблема, если мне нужно полностью копировать этот метод в динамический модуль, но в этом случае мне нужно найти способ копирования через IL, а также обновить все вызовы Solve() чтобы они указывали на новую копию.
Ответы
Ответ 1
Подумайте о последствиях, если это возможно. Вы можете, например, заменить содержимое класса String
и нанести havock. Когда метод загружается CLR, он не может быть изменен. Вы можете взглянуть на AOP и библиотеки, такие как Castle DynamicProxy, которые используются в насмешливых фреймворках, таких как Rhino Mocks.
Ответ 2
Для .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");
}
}
}
Ответ 3
Harmony - это библиотека с открытым исходным кодом, предназначенная для замены, декорирования или модификации существующих методов С# любого вида во время выполнения. Основное внимание уделяется играм и плагинам, написанным в Моно, но этот метод можно использовать с любой версией .NET. Он также заботится о нескольких изменениях того же метода (они накапливаются вместо перезаписи).
Он создает методы типа DynamicMethod для каждого исходного метода и испускает для него код, который вызывает пользовательские методы в начале и в конце. Он также позволяет вам писать фильтры для обработки исходного кода IL, который позволяет более детально манипулировать исходным методом.
Чтобы завершить процесс, он записывает простой ассемблер в батуте исходного метода, который указывает на сборщик, сгенерированный при компиляции динамического метода. Это работает для 32/64Bit для Windows, MacOS и любого Linux, поддерживаемого Mono.
Ответ 4
Вы можете изменить содержимое метода во время выполнения. Но вы не должны этого делать, и настоятельно рекомендуется сохранить это для целей тестирования.
Просто взгляните на:
http://www.codeproject.com/Articles/463508/NET-CLR-Injection-Modify-IL-Code-during-Run-time
В принципе, вы можете:
- Получить контент IL-метода через MethodInfo.GetMethodBody(). GetILAsByteArray()
-
Беспокойство с этими байтами.
Если вы просто хотите добавить или добавить какой-то код, просто представьте/добавьте коды операций, которые вы хотите (будьте осторожны, если вы оставите стек чистым, хотя)
Вот несколько советов, чтобы "скомпилировать" существующий 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) )
-
После изменения вы можете переустановить массив байтов IL через InjectionHelper.UpdateILCodes(метод MethodInfo, byte [] ilCodes) - см. ссылку, упомянутую выше
Это "небезопасная" часть... Это хорошо работает, но это связано с взломом внутренних механизмов CLR...
Ответ 5
вы можете его заменить, если этот метод не является виртуальным, не общим, а не общим типом, а не встроенным и на табличке 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.
Ответ 6
Логарифмическое решение, но с интерфейсом для тел методов обмена. Кроме того, более простой пример.
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;
}
}
}
Ответ 7
Я знаю, что это не точный ответ на ваш вопрос, но обычный способ сделать это - использовать заводы/прокси-подход.
Сначала мы объявляем базовый тип.
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>>.
Ответ 8
Вы можете заменить метод во время выполнения, используя ICLRPRofiling Interface.
Подробнее см. в этом блоге.
Ответ 9
Существует несколько фреймворков, которые позволяют динамически изменять любой метод во время выполнения (они используют интерфейс ICLRProfiling, упомянутый пользователем152949):
Есть также несколько фреймворков, которые издеваются над внутренними компонентами.NET, они, скорее всего, более хрупкие и, вероятно, не могут изменить встроенный код, но, с другой стороны, они полностью автономны и не требуют от вас использования обычная пусковая установка.
- Гармония: лицензия MIT. Кажется, на самом деле он был успешно использован в нескольких игровых моделях, поддерживает как.NET, так и Mono.
- Deviare In Process Instrumentation Двигатель: GPLv3 и коммерческий. Поддержка.NET в настоящее время отмечена как экспериментальная, но, с другой стороны, имеет преимущество от коммерческой поддержки.
Ответ 10
Основываясь на ответе на этот и другой вопрос, я придумал эту приведенную в порядок версию:
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
}
}