web-dev-qa-db-fra.com

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

Ce que je veux faire, c'est changer la façon dont une méthode C # s'exécute lorsqu'elle est appelée, afin 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 être en mesure d'analyser les méthodes qui ont l'attribut distribué (ce que je peux déjà faire), puis d'insérer du code avant que le corps de la fonction ne s'exécute et après le retour de la fonction. Plus important encore, je dois pouvoir le faire sans modifier le code où Solve est appelé ou au début de la fonction (au moment de la compilation; le faire au moment de l'exécution est l'objectif).

Pour le moment, j'ai essayé ce morceau de code (supposons que t est le type dans lequel Solve est stocké, et 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; pas ceux qui ont déjà été compilés et stockés dans l'Assemblée.

Je cherche donc un moyen de faire efficacement SwapMethodBody sur un méthode qui est déjà stockée dans un assembly chargé et en cours d'exécution.

Remarque, 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 à travers l'IL ainsi que de mettre à jour tous les appels à Solve () de sorte qu'ils pointerait vers la nouvelle copie.

77
June Rhodes

Pour .NET 4 et supérieur

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

}
150
Logman

Harmony est une bibliothèque open source conçue pour remplacer, décorer ou modifier les méthodes C # existantes de tout type pendant l'exécution. Son objectif principal est les jeux et plugins écrits en Mono mais la technique peut être utilisée avec n'importe quelle version de .NET. Il prend également en charge plusieurs modifications de la même méthode (elles s'accumulent au lieu d'être écrasées).

Il crée des méthodes de type DynamicMethod pour chaque méthode d'origine et lui envoie 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 d'origine, ce qui permet une manipulation plus détaillée de la méthode d'origine.

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

119
Andreas Pardeike

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

Jetez un œil à:

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

Fondamentalement, vous pouvez:

  1. Obtenez le contenu de la méthode IL via MethodInfo.GetMethodBody (). GetILAsByteArray ()
  2. Jouez avec ces octets.

    Si vous souhaitez simplement ajouter ou ajouter du code, alors ajoutez/ajoutez simplement les opcodes que vous souhaitez (faites attention à ne pas nettoyer la pile, cependant)

    Voici quelques conseils pour "décompiler" 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 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 renvoyé peut être trouvée à l'aide d'OpCodes.YourOpCode.Value (qui est la véritable valeur d'octet d'opcode enregistrée dans votre assembly)
    • Les arguments ajoutés après les codes IL peuvent avoir différentes tailles (de un à plusieurs octets), en fonction de l'opcode appelé
    • Vous pouvez trouver des jetons auxquels ces arguments font référence via des méthodes appropriées. Par exemple, si votre IL contient ".call 354354" (codé 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é, votre tableau d'octets IL peut être réinjecté via InjectionHelper.UpdateILCodes (méthode MethodInfo, octet [] ilCodes) - voir le lien mentionné ci-dessus

    C'est la partie "peu sûre" ... Ça marche bien, mais ça consiste à pirater des mécanismes CLR internes ...

25
Olivier

vous pouvez la remplacer si la méthode est non virtuelle, non générique, pas de type générique, non intégrée 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.
11
Teter28

Il existe quelques frameworks qui vous permettent de modifier dynamiquement n'importe quelle méthode au moment de l'exécution (ils utilisent l'interface ICLRProfiling mentionnée par user152949):

Il existe également quelques cadres qui se moquent des composants internes de .NET, ils sont probablement plus fragiles et ne peuvent probablement pas modifier le code intégré, mais d'un autre côté, ils sont entièrement autonomes et ne nécessitent pas que vous utilisiez un lanceur personnalisé.

  • Harmony : MIT sous licence. Semble avoir été utilisé avec succès dans quelques mods de jeu, prend en charge à la fois .NET et Mono.
  • Deviare In Process Instrumentation Engine : GPLv3 et Commercial. Le support .NET est actuellement marqué comme expérimental, mais a en revanche l'avantage d'être soutenu commercialement.
8
poizan42

solution de Logman , mais avec une interface pour permuter les corps des méthodes. Aussi, un exemple plus 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
    {
        /// <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
C. McCoy IV

Vous pouvez remplacer une méthode lors de l'exécution en utilisant ICLRPRofiling Interface .

  1. Appelez AttachProfiler pour vous attacher au processus.
  2. Appelez SetILFunctionBody pour remplacer le code de méthode.

Voir ce blog pour plus de détails.

5
user152949

Je sais que ce n'est pas la réponse exacte à votre question, mais la façon habituelle de le faire est d'utiliser l'approche usines/proxy.

Nous déclarons d'abord un type de base.

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

Ensuite, nous pouvons déclarer un type dérivé (appelez-le proxy).

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();

Le type dérivé peut également être généré lors de l'exécution.

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(...);

La seule perte de performances est lors de la construction de l'objet dérivé, la première fois est assez lente car elle utilisera beaucoup de réflexion et de réflexion émise. Dans tous les autres cas, c'est le coût d'une recherche de table simultanée et d'un constructeur. Comme dit, vous pouvez optimiser la construction en utilisant

ConcurrentDictionary<Type, Func<object>>.
4
Salvatore Previti

Sur la base de la réponse à cette question et à une autre, ive a proposé cette version bien rangée:

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
        }
    }
1
TakeMeAsAGuest