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

ce que je veux faire est de changer la façon dont une méthode C# s'exécute quand elle est appelée, pour 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;
}

à l'exécution, je dois être capable d'analyser des méthodes qui ont L'attribut distribué (ce que je peux déjà faire) et ensuite insérer du code avant que le corps de la fonction n'exécute et après que la fonction retourne. Plus important encore, je dois être capable de le faire sans modifier le code où Solve est appelé ou au début de la fonction (au moment de la compilation; de le faire au moment de l'exécution est l'objectif).

pour le moment, j'ai essayé ce bout de code (à supposer que t est le type qui permettent de Résoudre des sont stockées, et m est un MethodInfo de Résoudre) :

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

However, MethodRental.SwapMethodBody ne fonctionne que sur des modules dynamiques; pas ceux qui ont déjà été compilés et stockés dans l'assemblage.

donc je cherche un moyen de faire efficacement SwapMethodBody sur une méthode qui est déjà stocké dans un assemblage chargé et d'exécution .

Note, 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 mettre à jour tous les appels à résoudre() de sorte qu'ils pointent vers la nouvelle copie.

58
demandé sur starblue 2011-09-04 16:07:51

9 réponses

pensez aux implications si cela était possible. Vous pouvez par exemple remplacer le contenu de la classe String par wreak havock. Une fois qu'une méthode est chargée par le CLR, elle ne peut pas être modifiée. Vous pouvez jeter un oeil à AOP et les bibliothèques telles que Château DynamicProxy qui sont utilisés par des cadres moqueurs tels que des moqueries de rhinocéros.

-21
répondu Darin Dimitrov 2015-09-18 00:18:30

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

}
111
répondu Logman 2017-01-06 21:25:08

Harmony est une bibliothèque libre conçue pour remplacer, décorer ou modifier les méthodes C# existantes de toute sorte pendant l'exécution. Il se concentre principalement sur les jeux et les plugins écrits en Mono mais la technique peut être utilisée avec n'importe quelle version .NET. Il s'occupe aussi de multiples changements à la même méthode (ils s'accumulent au lieu de les écraser).

il crée des méthodes de type DynamicMethod pour chaque méthode originale et émet code à lui 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 qui permet une manipulation plus détaillée de la méthode originale.

pour compléter le processus, il écrit un simple assembleur sauter dans le trampoline de la méthode originale qui pointe à l'assembleur généré par la compilation de la méthode dynamique. Cela fonctionne pour 32 / 64Bit sur Windows, macOS et tout Linux qui prend en charge Mono.

71
répondu Andreas Pardeike 2017-02-04 16:56:34

vous pouvez modifier le contenu d'une méthode à l'exécution. Mais vous n'êtes pas censé le faire, et il est fortement recommandé de le garder pour les tests.

Juste un coup d'oeil:

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

en gros, vous pouvez:

  1. Obtenir IL de la méthode du contenu via MethodInfo.GetMethodBody ().GetILAsByteArray ()
  2. Mess avec ces octets.

    si vous souhaitez juste ajouter ou ajouter du code, alors juste ajouter / ajouter des opcodes que vous voulez (faites attention en laissant la pile propre, cependant)

    voici quelques conseils pour "décompiler" il existant:

    • Octets retournés sont une séquence d'instructions IL, suivis par leurs arguments (si elles n'ont pour instance.' ,call ' a un argument: le token de méthode appelé, et '.pop' en a aucun)
    • la Correspondance entre IL des codes et des octets que vous trouverez dans le tableau retourné peut être trouvée à l'aide des OpCodes.YourOpCode.Valeur (qui est la valeur réelle du byte opcode enregistré dans votre assemblage)
    • Arguments ajoutés après il codes peuvent avoir des tailles différentes (d'un à plusieurs octets), en fonction de opcode appelé
    • vous pouvez trouver des signes que les thèses les arguments font référence à des méthodes appropriées. Par exemple, si votre IL contient ".appel 354354" (codé 28 00 05 68 32 dans hexa, 28h=40 étant'.call' opcode 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 peut être réinjecté via un Helper D'injection.UpdateILCodes (MethodInfo method, byte[] ilCodes) - voir le lien mentionné ci-dessus

    C'est "dangereux"... Cela fonctionne bien, mais cela consiste à hacker les mécanismes CLR internes...

23
répondu Olivier 2013-10-16 12:54:15

vous pouvez le remplacer si la méthode n'est pas virtuelle, Non Générique, Pas de type générique, non inlined et sur la 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.
10
répondu Teter28 2014-12-30 14:25:00

Logman's solution , mais avec une interface pour échanger des corps de méthode. 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;
        }
    }
}
6
répondu C. McCoy IV 2017-08-30 03:48:20

je sais que ce n'est pas la réponse exacte à votre question, mais la manière habituelle de le faire est d'utiliser usines/proxy approche.

tout d'abord, nous déclarons 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;
    }
}

alors nous pouvons déclarer un type dérivé (l'appeler 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 aussi être généré à 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 performance est lors de la construction de l'Objet dérivé, le premier le temps est plutôt lent car il va utiliser beaucoup de réflexion et de réflexion émettent. Tous les autres temps, il est le coût d'une recherche de table concurrente et un constructeur. Comme dit, Vous pouvez optimiser la construction en utilisant

ConcurrentDictionary<Type, Func<object>>.
4
répondu Salvatore Previti 2011-09-04 13:21:48

vous pouvez remplacer une méthode à l'exécution en utilisant l'Interface ICLRPRofiling .

  1. Appel AttachProfiler à joindre au processus.
  2. appeler SetILFunctionBody pour remplacer le code de la méthode.

voir ce blog pour plus de détails.

4
répondu 2016-06-27 10:50:14

il existe quelques cadres qui vous permettent de changer dynamiquement n'importe quelle méthode à l'exécution (ils utilisent l'interface ICLRProfiling mentionnée par user152949):

Il ya aussi quelques cadres qui se moque autour avec les internes de .NET, ceux-ci sont probablement plus fragile, et ne peut probablement pas changer de code inlined, mais d'un autre côté, ils sont entièrement autonomes et ne vous oblige pas à utiliser un lanceur personnalisé.

  • Harmonie : sous licence MIT. Semble avoir été utilisé avec succès dans quelques mods de jeu, supporte à la fois .NET et Mono.
  • Déviare in Process Instrumentation Engine : GPLv3 et Commercial. Le support. net est actuellement qualifié d'expérimental, mais il présente l'avantage d'être soutenu commercialement.
2
répondu poizan42 2018-06-19 12:00:28