149 votes

Remplacer dynamiquement le contenu d'une méthode C# ?

Ce que je veux faire, c'est modifier la façon dont une méthode C# s'exécute lorsqu'elle est appelée, de sorte que je puisse écrire quelque chose comme ceci :

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

Au moment de l'exécution, je dois pouvoir analyser les méthodes qui ont l'attribut Distribué (ce que je peux déjà faire) et insérer du code avant l'exécution du corps de la fonction et après le retour de la fonction. Plus important encore, je dois pouvoir le faire sans modifier le code à l'endroit où Solve est appelé ou au début de la fonction (au moment de la compilation ; l'objectif est de le faire au moment de l'exécution).

Pour l'instant, j'ai essayé ce bout de code (en supposant que t est le type dans lequel Solve est stocké, et que m est 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;
}

Cependant, MethodRental.SwapMethodBody ne fonctionne que sur les modules dynamiques, et non sur ceux qui ont déjà été compilés et stockés dans l'assemblage.

Je cherche donc un moyen d'effectuer efficacement l'opération SwapMethodBody sur un fichier qui est déjà stockée dans un assemblage chargé et en cours d'exécution .

Notez que ce n'est pas un problème si je dois copier complètement la méthode dans un module dynamique, mais dans ce cas, je dois trouver un moyen de copier l'IL ainsi que de mettre à jour tous les appels à Solve() afin qu'ils pointent vers la nouvelle copie.

394voto

Andreas Pardeike Points 1211

Divulgation : Harmony est une bibliothèque écrite et maintenue par moi, l'auteur de ce billet.

Harmonie 2 est une bibliothèque open source (licence MIT) conçue pour remplacer, décorer ou modifier les méthodes C# existantes de tout type pendant l'exécution. Elle est principalement destinée aux jeux et aux plugins écrits en Mono ou en .NET. Elle prend en charge les modifications multiples apportées à une même méthode - elles s'accumulent au lieu de s'écraser les unes les autres.

Il crée des méthodes de remplacement dynamiques pour chaque méthode originale et émet du code qui appelle des méthodes personnalisées au début et à la fin. Il vous permet également d'écrire des filtres pour traiter le code IL original et des gestionnaires d'exception personnalisés qui permettent une manipulation plus détaillée de la méthode originale.

Pour terminer le processus, il écrit un simple saut d'assembleur dans le trampoline de la méthode originale qui pointe vers l'assembleur généré par la compilation de la méthode dynamique. Cela fonctionne pour 32/64 bits sur Windows, macOS et tout Linux pris en charge par Mono.

La documentation est disponible aquí .

Exemple

( Source )

Code original

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

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

Parcheando avec annotations 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;
    }
}

Alternativement, le manuel Parcheando avec la réflexion

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()
    {
        // ...
    }
}

221voto

Logman Points 69

Pour .NET 4 et plus

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

}

27voto

Olivier Points 2277

Il est possible de modifier le contenu d'une méthode au moment de l'exécution. Mais vous n'êtes pas censé le faire, et il est fortement recommandé de le conserver à des fins de test.

Il suffit de jeter un coup d'œil :

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

En principe, vous pouvez :

  1. Obtenir le contenu de la méthode IL via MethodInfo.GetMethodBody().GetILAsByteArray()
  2. Ne touchez pas à ces octets.

    Si vous souhaitez simplement prépendre ou ajouter du code, il suffit de prépendre/appliquer les opcodes que vous souhaitez (attention à ne pas laisser la pile vide).

    Voici quelques conseils pour "décompiler" un IL existant :

    • Les octets renvoyés sont une séquence d'instructions IL, suivie de leurs arguments (s'ils en ont - par exemple, '.call' a un argument : le jeton de la méthode appelée, et '.pop' n'en a pas).
    • La correspondance entre les codes IL et les octets que vous trouvez dans le tableau retourné peut être trouvée en utilisant OpCodes.YourOpCode.Value (qui est la valeur réelle de l'octet du code op tel qu'il est enregistré dans votre assemblage).
    • Les arguments ajoutés après les codes IL peuvent avoir des tailles différentes (d'un à plusieurs octets), en fonction de l'opcode appelé.
    • Vous pouvez trouver les jetons auxquels ces arguments font référence par le biais de méthodes appropriées. Par exemple, si votre IL contient ".call 354354" (codé en 28 00 05 68 32 en hexa, 28h=40 étant l'opcode '.call' et 56832h=354354), la méthode appelée correspondante peut être trouvée en utilisant MethodBase.GetMethodFromHandle(354354)
  3. Une fois modifié, le tableau d'octets d'IL peut être réinjecté via InjectionHelper.UpdateILCodes(MethodInfo method, byte[] ilCodes) - voir le lien mentionné ci-dessus.

    C'est la partie "dangereuse"... Cela fonctionne bien, mais cela consiste à pirater les mécanismes internes du CLR...

14voto

Teter28 Points 484

Vous pouvez le remplacer si la méthode n'est pas virtuelle, pas générique, pas dans un type générique, pas inline et sur une plateforme 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.

14voto

TakeMeAsAGuest Points 680

Sur la base de la réponse à cette question et à une autre, j'ai élaboré cette version simplifiée :

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X