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.

10voto

poizan42 Points 108

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

  • Faux Microsoft : Commercial, inclus dans Visual Studio Premium et Ultimate mais pas dans Community et Professional
  • Telerik JustMock : Commercial, une version "lite" est disponible
  • Isolateur Typemock : Commercial
  • Prig : Libre et Open Source, mais non maintenu depuis 2017

Il existe également quelques frameworks qui se moquent des éléments 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 l'utilisation d'un lanceur personnalisé.

  • Harmonie : MIT sous licence. Semble avoir été utilisé avec succès dans quelques mods de jeux, supporte à la fois .NET et Mono.
  • [Pose][7] : Sous licence MIT mais non mis à jour depuis 2021.
  • Deviare In Process Instrumentation Engine : GPLv3 et Commercial. Le support de .NET est actuellement marqué comme expérimental, mais d'un autre côté il a l'avantage d'être soutenu commercialement. Malheureusement, il ne semble pas avoir été mis à jour depuis 2020.

8voto

C. McCoy IV Points 664

La solution de Logman mais avec une interface permettant d'échanger les corps des méthodes. Voici également 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;
        }
    }
}

8voto

Spinicoffee Points 41

Sur la base de Réponse de TakeMeAsAGuest Voici une extension similaire qui ne nécessite pas l'utilisation de dangereux blocs.

Voici le Extensions classe :

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

Voici un exemple d'utilisation simple :

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

Ce document est tiré d'un projet plus détaillé que j'ai mis à disposition sur Github ( MéthodeRedirect ).

Remarque : Le code a été mis en œuvre à l'aide de .NET Framework 4 et n'a pas été testé sur des versions plus récentes de .NET.

5voto

Vous pouvez remplacer une méthode au moment de l'exécution en utilisant la méthode ICLRPRInterface de dépôt .

  1. Appeler AttachProfiler à rattacher au processus.
  2. Appeler [SetILFunctionBody](https://msdn.microsoft.com/en-us/library/ms232096.aspx?tduid=(ec7e4e435737a22a6a8e0c94faeb1b4f)(256380)(2459594)(TnL5HPStwNw-0CP_VcVDY7nn568cA1F_ZQ)()) pour remplacer le code de la méthode.

Voir ce blog pour plus de détails.

3voto

Salvatore Previti Points 5842

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

Nous commençons par déclarer 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;
    }
}

Nous pouvons alors déclarer un type dérivé (appelé 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é au moment 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 performance est lors de la construction de l'objet dérivé, la première fois est assez lente parce qu'elle utilise beaucoup de réflexion et d'émission de réflexion. Toutes les autres fois, c'est le coût d'une recherche concurrente dans la table et d'un constructeur. Comme nous l'avons dit, vous pouvez optimiser la construction en utilisant

ConcurrentDictionary<Type, Func<object>>.

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