动态替换 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;
}

在运行时,我需要能够分析具有分布式属性的方法(我已经可以这样做了) ,然后在函数体执行之前和函数返回之后插入代码。更重要的是,我需要能够在不修改调用 Solve 的代码或函数开始时(在编译时; 在运行时这样做是目标)的情况下完成这项工作。

目前,我已经尝试了这段代码 (假设 t 是 Solve 存储的类型,m 是 Solve 的 MethodInfo):

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

但是,MethodRent.SwapMethodBody 只能在动态模块上工作,而不能在已经编译并存储在程序集中的模块上工作。

因此,我正在寻找一种有效地在 已存储在已加载并正在执行的程序集中的上执行 SwapMethodBody 的方法。

注意,如果我必须将方法完全复制到一个动态模块中,这不是问题,但是在这种情况下,我需要找到一种方法来跨 IL 进行复制,并更新 Solve ()的所有调用,以便它们指向新的副本。

87393 次浏览

我知道这不是你问题的确切答案,但通常的方法是使用工厂/代理方法。

首先,我们声明一个基类型。

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

您可以在运行时修改方法的内容。但是您不应该这样做,并且强烈建议您将其保留用于测试目的。

看看这个:

Http://www.codeproject.com/articles/463508/net-clr-injection-modify-il-code-during-run-time

基本上,你可以:

  1. 通过 MethodInfo.GetMethodBody ()获取 IL 方法内容
  2. 搞乱这些字节。

    如果您只是希望预先添加或附加一些代码,那么只需要预先添加/附加您想要的操作码(但是要注意保持堆栈清洁)

    下面是一些“解编译”现有 IL 的技巧:

    • 返回的字节是一系列 IL 指令,后面跟着它们的参数(如果它们有一些参数——例如,’。Call’有一个参数: 被调用的方法标记和’。爸爸没有)
    • 使用 OpCodes 可以找到在返回数组中找到的 IL 代码和字节之间的对应关系。你的操作码。值(保存在程序集中的实际操作码字节值)
    • 在 IL 代码之后附加的参数可能有不同的大小(从一个字节到几个字节) ,这取决于调用的操作码
    • 您可能会发现这些参数通过适当的方法引用的标记。例如,如果您的 IL 包含”。“ call 354354”(编码为2800056832,以六进制表示,28小时 = 40是’。调用’操作码和56832h = 354354) ,可以使用 MethodBase 找到相应的调用方法。GetMethodFromHandle (354354)
  3. 修改后,可以通过 InjectionHelper.UpdateILCodes (MethodInfo 方法,byte [] ilCodes)重新注入 IL 字节数组-参见上面提到的链接

    这是“不安全”的部分... 它工作得很好,但这包括黑客内部 CLR 机制..。

如果该方法是非虚拟的、非泛型的、非泛型类型的、非内联的并且在 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.

适用于.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");
}
}


}

可以使用 分析界面在运行时替换方法。

  1. 调用 附件分析器附加到进程。
  2. 调用 SetILFunctionBody来替换方法代码。

有关更多细节,请参见此博客

有几个框架允许您在运行时动态更改任何方法(它们使用 user152949提到的 ICLRProfiles 接口) :

还有一些框架使用。NET,这些可能更脆弱,可能不能更改内联代码,但另一方面,它们是完全自包含的,不需要您使用自定义启动程序。

  • 和谐 : 麻省理工授权。看起来实际上已经在一些游戏中成功使用,支持.NET 和 Mono。
  • Deviare In Process Instruments Engine : GPLv3 and Commercial..NET 支持目前标记为实验性的,但另一方面有商业支持的好处。

披露: Harmony 是一个图书馆,由本文作者我撰写和维护。

Harmony 2 是一个开源库(MIT 许可证) ,用于在运行时替换、修饰或修改任何类型的现有 C # 方法。它的主要重点是游戏和插件写在 Mono 或。NET.它处理对同一方法的多个更改——它们累积而不是相互覆盖。

它为每个原始方法创建动态替换方法,并向它们发出代码,这些代码在开始和结束时调用自定义方法。它还允许您编写过滤器来处理原始 IL 代码和自定义异常处理程序,这允许对原始方法进行更详细的操作。

为了完成这个过程,它将一个简单的汇编程序跳转写入原始方法的蹦床,该蹦床指向编译动态方法生成的汇编程序。这适用于 Windows、 macOS 和任何 Mono 支持的 Linux 上的32/64位。

文档可以找到 给你

例子

(来源)

原始密码

public class SomeGameClass
{
private bool isRunning;
private int counter;


private int DoSomething()
{
if (isRunning)
{
counter++;
return counter * 10;
}
}
}

使用 Harmony 注释修补程序

using SomeGame;
using HarmonyLib;


public class MyPatcher
{
// make sure DoPatching() is called at start either by
// the mod loader or by your injector


public static void DoPatching()
{
var harmony = new Harmony("com.example.patch");
harmony.PatchAll();
}
}


[HarmonyPatch(typeof(SomeGameClass))]
[HarmonyPatch("DoSomething")]
class Patch01
{
static FieldRef<SomeGameClass,bool> isRunningRef =
AccessTools.FieldRefAccess<SomeGameClass, bool>("isRunning");


static bool Prefix(SomeGameClass __instance, ref int ___counter)
{
isRunningRef(__instance) = true;
if (___counter > 100)
return false;
___counter = 0;
return true;
}


static void Postfix(ref int __result)
{
__result *= 2;
}
}

或者,手动反射修补

using SomeGame;
using System.Reflection;
using HarmonyLib;


public class MyPatcher
{
// make sure DoPatching() is called at start either by
// the mod loader or by your injector


public static void DoPatching()
{
var harmony = new Harmony("com.example.patch");


var mOriginal = typeof(SomeGameClass).GetMethod("DoSomething", BindingFlags.Instance | BindingFlags.NonPublic);
var mPrefix = typeof(MyPatcher).GetMethod("MyPrefix", BindingFlags.Static | BindingFlags.Public);
var mPostfix = typeof(MyPatcher).GetMethod("MyPostfix", BindingFlags.Static | BindingFlags.Public);
// add null checks here


harmony.Patch(mOriginal, new HarmonyMethod(mPrefix), new HarmonyMethod(mPostfix));
}


public static void MyPrefix()
{
// ...
}


public static void MyPostfix()
{
// ...
}
}

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

基于这个问题和另一个问题的答案,我想出了这个经过整理的版本:

// Note: This method replaces methodToReplace with methodToInject
// Note: methodToInject will still remain pointing to the same location
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
}
}

看看 Mono:

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

基于 接受我的回答,这里有一个类似的扩展,它不需要使用 不安全块。

下面是 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 = 0xffff; // 2 bytes mask
const int MT_OFFSET_32BIT = 0x28;       // 40 bytes offset
const int MT_OFFSET_64BIT = 0x40;       // 64 bytes offset


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
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 (方法重定向)上提供的一个更详细的项目中提炼出来的。

备注 : 代码是使用。NETFramework4,并且尚未在更新版本的。NET.