Динамически заменять содержимое метода 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;
}

Во время выполнения мне нужно иметь возможность анализировать методы с атрибутом Distributed (что я уже могу сделать), а затем вставлять код до выполнения тела функции и после ее возврата. Что еще более важно, мне нужно иметь возможность делать это без изменения кода, в котором вызывается Solve, или в начале функции (во время компиляции; цель - сделать это во время выполнения).

На данный момент я попытался использовать этот фрагмент кода (предположим, что t - это тип, в котором хранится Solve, а m - это MethodInfo для 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;
}

Однако MethodRental.SwapMethodBody работает только с динамическими модулями; не те, что уже были скомпилированы и сохранены в сборке.

Итак, я ищу способ эффективно использовать SwapMethodBody для метода, который уже хранится в загруженной и выполняющейся сборке.

Обратите внимание: это не проблема, если мне нужно полностью скопировать метод в динамический модуль, но в этом случае мне нужно найти способ скопировать через IL, а также обновить все вызовы Solve (), чтобы они укажет на новую копию.


person June Rhodes    schedule 04.09.2011    source источник
comment
Невозможно поменять местами уже загруженные методы. В противном случае Spring.Net не пришлось бы делать странные вещи с прокси и интерфейсами :-) Прочтите этот вопрос, он касается вашей проблемы: stackoverflow.com/questions/25803/ (если вы можете его перехватить, вы можете кое-что -как-поменять местами ... Если вы не можете 1, то явно не можете 2).   -  person xanatos    schedule 04.09.2011
comment
В этом случае есть ли способ скопировать метод в динамический модуль и обновить остальную часть сборки таким образом, чтобы вызовы этого метода указывали на новую копию?   -  person June Rhodes    schedule 04.09.2011
comment
Такой же старый-такой же старый. Если бы это можно было сделать легко, все различные контейнеры IoC, вероятно, сделали бы это. Они этого не делают - ›99% это невозможно :-) (без ужасных и бесчисленных хаков). Есть одна надежда: в C # 5.0 обещали метапрограммирование и асинхронность. Async мы видели ... Ничего не запрограммировать ... НО это могло быть!   -  person xanatos    schedule 04.09.2011
comment
Не могли бы вы сделать Solve() виртуальным и создать класс во время выполнения, который наследуется от класса, в котором находится Solve()?   -  person svick    schedule 04.09.2011
comment
Почему вам нравится менять корпус? Вы знаете, на что вы меняете местами? Если да, то почему бы не заставить метод принимать тело как Func?   -  person Tomas Jansson    schedule 04.09.2011
comment
Если вы не можете каким-либо образом переписать решение, я не думаю, что в C # есть что-то, что вы можете сделать ... Ну, в C ++ вы можете внедрить dll и Proxy для всех функций, но я не думаю, что это хорошая идея   -  person Francesco Belladonna    schedule 04.09.2011
comment
Вы действительно не объяснили, почему хотите позволить себе что-то настолько болезненное.   -  person DanielOfTaebl    schedule 04.09.2011
comment
Пожалуйста, посмотрите мой ответ ниже. Это вполне возможно. В коде, которым вы не владеете, и во время выполнения. Я не понимаю, почему многие думают, что это невозможно.   -  person Andreas Pardeike    schedule 14.03.2017


Ответы (11)


Раскрытие информации: Harmony - это библиотека, которая была написана и поддерживается мной, автором этой публикации.

Harmony 2 - это библиотека с открытым исходным кодом (лицензия MIT), предназначенная для замены, украшения или изменения существующих методов C # любых вид во время выполнения. Основное внимание уделяется играм и плагинам, написанным на Mono или .NET. Он заботится о нескольких изменениях одного и того же метода - они накапливаются, а не перезаписывают друг друга.

Он создает динамические методы замены для каждого исходного метода и передает им код, который вызывает пользовательские методы в начале и в конце. Он также позволяет вам писать фильтры для обработки исходного кода IL и настраиваемые обработчики исключений, что позволяет более детально манипулировать исходным методом.

Чтобы завершить процесс, он записывает простой ассемблерный переход в трамплин исходного метода, который указывает на ассемблер, созданный при компиляции динамического метода. Это работает для 32/64-битных версий Windows, macOS и любых Linux, поддерживаемых Mono.

Документацию можно найти здесь.

Пример

(Источник)

Исходный код

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 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()
    {
        // ...
    }
}
person Andreas Pardeike    schedule 04.02.2017
comment
Посмотрел исходники, очень интересно! Можете ли вы объяснить (здесь и / или в документации), как работают конкретные инструкции, которые используются для выполнения прыжка (в Memory.WriteJump)? - person Tom; 29.10.2018
comment
Чтобы частично ответить на мой собственный комментарий: 48 B8 <QWord> перемещает непосредственное значение QWord в rax, тогда FF E0 становится jmp rax - все ясно! Мой оставшийся вопрос касается случая E9 <DWord> (ближний прыжок): кажется, в этом случае ближний прыжок сохраняется, и модификация относится к цели прыжка; когда Mono вообще генерирует такой код и почему он получает особую обработку? - person Tom; 30.10.2018
comment
Не смотрите только на моно. Многие люди используют Harmony в разных версиях .Net, и я заметил это косвенное обращение в коде батутов во время тестирования. Есть еще много неохваченных случаев, но мое время ограничено, и на данный момент он работает достаточно хорошо. - person Andreas Pardeike; 30.10.2018
comment
Насколько я могу судить, он еще не поддерживает .NET Core 2, и возникают некоторые исключения с AppDomain.CurrentDomain.DefineDynamicAssembly. - person Max; 18.11.2018
comment
Да, Макс, у меня еще не было времени на сборку и тестирование Harmony с .NET Core 2. - person Andreas Pardeike; 18.11.2018
comment
Мой друг 0x0ade упомянул мне, что есть менее зрелая альтернатива, которая работает на .NET Core, а именно MonoMod.RuntimeDetour на NuGet. - person Andreas Pardeike; 08.02.2019
comment
Обновление: включив ссылку на System.Reflection.Emit, Harmony теперь компилируется и тестирует нормально с .NET Core 3. - person Andreas Pardeike; 02.05.2019
comment
Harmony 2 теперь использует MonoMod.Common, чтобы избежать Reflection.Emit, и использует Cecil в памяти для генерации нового метода. Работает со всеми версиями .NET. - person Andreas Pardeike; 28.01.2020
comment
У вас есть и пример патча, не префикса, постфикса или транспилятора, а просто метод, заменяющий оригинал. Вроде как обратный патч. - person rraallvv; 25.08.2020
comment
@rraallvv Чтобы заменить метод, вы должны создать префикс, который имеет все аргументы оригинала плюс __instance (если не статический) и ref __result, и пусть он возвращает false, чтобы пропустить оригинал. В нем вы используете __instance и присваиваете результат __result, а затем возвращаете false. - person Andreas Pardeike; 26.08.2020

Для .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");
        }
    }

}
person Logman    schedule 05.04.2016
comment
Это заслуживает еще большего количества голосов. У меня совершенно другой сценарий, но этот фрагмент - именно то, что мне нужно, чтобы направить меня в правильном направлении. Спасибо. - person S.C.; 15.04.2016
comment
Спасибо за отличный ответ. Есть ли способ заставить это работать в режиме полной версии с оптимизацией (т.е. запускать без отладки), когда методы встроены? (Когда я добавляю [MethodImpl(MethodImplOptions.NoInlining)], он работает, но есть ли способ заменить его без этого?) - person Mr Anderson; 07.07.2016
comment
@MrAnderson Да, но это не так просто. Это больше похоже на программирование на ассемблере, чем на C #, поскольку вам нужно получить машинный код встроенной функции, а затем просканировать приложение на предмет этого кода. После этого замените каждое место, где вы нашли свою функцию, новым кодом. Это непросто и подвержено ошибкам, но вы можете это сделать. - person Logman; 07.07.2016
comment
@Logman Я скачал и изучаю исходный код CLI, я понял, как предотвратить встраивание метода. Пока хорошие результаты, но все еще работает. Другой вопрос - когда methodToInject является DynamicMethod, ваш процесс не работает. Есть идеи, как заставить это работать? - person Mr Anderson; 08.07.2016
comment
@Logman отличный ответ. Но у меня вопрос: что происходит в режиме отладки? А можно ли заменить только одну инструкцию? Например, если я хочу заменить условный переход на безусловный? AFAIK вы заменяете скомпилированный метод, поэтому нелегко определить, какое условие мы должны заменить ... - person Alex Zhukovskiy; 04.08.2016
comment
@AlexZhukovskiy в отладочном компиляторе добавляет некоторый промежуточный код, и для внедрения вашего метода вам необходимо пересчитать адрес вашего метода. Что касается второго вопроса, да, вы можете изменить ассемблерный код вашего приложения и поменять одну инструкцию на другую. Уловка состоит в том, чтобы найти правильный адрес памяти, который нужно переопределить. - person Logman; 04.08.2016
comment
Проблема @Logman в том, что я не смог распознать инструкции по сборке. Например, для метода return 15 это должно быть mov и ret, но я получил нераспознанную последовательность байтов. Я просто добавил указатель на (byte *) и переместился до 0xc3 (ret), но не нашел его. Я что делаю неправильно? Как я могу получить представление asselbmy из указателя? - person Alex Zhukovskiy; 04.08.2016
comment
@AlexZhukovskiy попробуйте отобразить часть вашего кода (преобразовать его в строку) и вставить его в это . Может быть несколько причин, по которым вы не сталкиваетесь с кодом операции 0xc3. - person Logman; 05.08.2016
comment
fyi Существует вопрос, указывающий сюда ... - person ; 05.08.2016
comment
@Logman Я знаю этот очень полезный сайт. Но я не смог кое-что наметить. Я отправлю пример позже, если хотите. - person Alex Zhukovskiy; 05.08.2016
comment
@AlexZhukovskiy, если хотите, разместите его в стеке и пришлите мне ссылку. Я изучу это и дам вам ответ после выходных. Машина Я также рассмотрю ваш вопрос после выходных. - person Logman; 05.08.2016
comment
Думаю, я только что нашел решение для вышеупомянутого вопроса: я опубликовал свой ответ там, ИМХО, он работает отлично. Спасибо, в любом случае. - person ; 05.08.2016
comment
Когда я делал это для интеграционного теста с MSTest, я заметил две вещи: (1) Когда вы используете this внутри injectionMethod*(), он будет ссылаться на экземпляр Injection во время компиляции, но на экземпляр Target во время времени выполнения < / i> (это верно для всех ссылок на члены экземпляра, которые вы используете внутри внедренного метода). (2) По какой-то причине часть #DEBUG работала только при отладке теста, но не при запуске теста, который был скомпилирован отладкой. В итоге я всегда использовал часть #else. Я не понимаю, почему это работает, но это так. - person Good Night Nerd Pride; 07.09.2016
comment
this сложно во время выполнения, поскольку метод не знает, для какого объекта он был вызван, и this необходимо передать методу из внешнего метода. Но во время компиляции компилятор знает, что где и что называть. - person Logman; 09.09.2016
comment
очень хорошо. пора все сломать! @GoodNightNerdPride использовать Debugger.IsAttached вместо #if препроцессора - person M.kazem Akhgary; 25.12.2016
comment
@Logman При внедрении методов, созданных с помощью Reflection.Emit.MethodBuilder, выбрасывает AccessViolationException в режиме отладки (работает в выпуске). Есть предположения? - person Mr Anderson; 19.01.2017
comment
@MrAnderson Вероятно, в динамически сгенерированном методе нет отладочного кода. Попробуйте использовать в отладке релизную версию для таких методов. Я внимательно рассмотрю это в свободное время. - person Logman; 20.01.2017
comment
Есть ли способ заставить это работать с виртуальными методами? Я получаю исключение AccessViolationException для методов, которые являются реализациями интерфейса, я полагаю, это потому, что они виртуальные? - person Red Taz; 22.05.2017
comment
@RedTaz да. Проверьте ответ User1892538. - person Logman; 22.05.2017
comment
@nrofis, можете ли вы объяснить, что имеете в виду, говоря "нет" и имея в виду DynamicMethod? - person Logman; 07.06.2017
comment
@Logman об ответе, который вы написали мистеру Андерсону. Если я пытаюсь использовать DynamicMethod в качестве внедренного метода, DynamicMethod.MethodHandle выдает исключение. Не работает даже в релизной версии - person nrofis; 07.06.2017
comment
@nrofis Пожалуйста, задайте новый вопрос о внедрении динамических методов, так как проблема еще не решена в комментариях. - person Logman; 07.06.2017
comment
Любая идея, как я могу заставить его работать в .Net 3.5, работающем в Mono CLI? - person djk; 09.11.2017
comment
Неа. Я пробую его на моно внутри Unity, но у меня есть только частичные результаты, и у меня сейчас нет времени, чтобы заставить его работать. Вы всегда можете проанализировать машинный код и попытаться разобраться самостоятельно. Вам просто нужно иметь базовые знания о том, как работает процессор, и потратить некоторое время на анализ памяти, как это делаю я. Это трудоемкая, но не слишком сложная задача. - person Logman; 09.11.2017
comment
В моем случае, когда я нажимаю строку прямо перед #else, я получаю: Exception thrown: 'System.AccessViolationException' in CustomActionsTest.dll An exception of type 'System.AccessViolationException' occurred in CustomActionsTest.dll but was not handled in user code Additional information: Attempted to read or write protected memory. This is often an indication that other memory is corrupt. - person Jon; 20.03.2018
comment
При запуске этого кода в .NET Core 2.0 и вызове внедренных методов возникает следующая ошибка: An unhandled exception of type 'System.ExecutionEngineException' occurred in Unknown Module. - person aelbatal; 09.04.2018
comment
@Jon У меня такой же результат, вы нашли решение? Спасибо - person Sebastien DErrico; 11.04.2018
comment
В чем причина +2? (int*)methodToInject.MethodHandle.Value.ToPointer() + 2; - person P.Brian.Mackey; 11.04.2018
comment
@jon - Это произошло, когда я изменил значение +2 в области указателя. Кроме того, я получаю действительную инъекцию, только когда нажимаю F5. По какой-то причине CTRL + F5 не работает должным образом. В этом случае он не вызывает всех вызовов методов. Я также использовал консольное приложение. LINQPad плохо справлялся с этим кодом. - person P.Brian.Mackey; 11.04.2018
comment
Может кто-нибудь объяснить части этого? Для чего нужны зачеты? Мне нужно изменить этот метод, чтобы он мог исправлять код, который был скомпилирован в выпуске, с кодом, который был скомпилирован при отладке. В неизменном виде текущая реализация приводит к аварийному завершению работы приложения. Я не уверен, почему, например, в DEBUG с IntPrt.Size == 8 код имеет дело с int * вместо long *. Это ошибка или намеренно? - person Zachary Burns; 17.04.2018
comment
@ZacharyBurns Возможно, вам стоит создать новый вопрос, поскольку ваша проблема намного сложнее этой. Чтобы ответить на ваш второй вопрос, смещение необходимо для доступа к реальному указателю, но в некоторых случаях оно может отличаться. Проверьте этот ответ, например. - person Logman; 17.04.2018
comment
Ответил на свой вопрос. +2 - это арифметика указателя. В x86 код работает с int (System.Int32) в 64-битной системе (8-байтовые слова). В сборках x64 используется long (System.Int64), и поскольку такие выражения, как longVar + 1, производят long типы, мы добавляем только 1 в последнем случае. productutorialspoint.com/cprogramming/c_pointer_arithmetic.htm - person P.Brian.Mackey; 03.05.2018
comment
Это здорово, спасибо. Одна проблема: код отладки, похоже, не работает с ядром .NET (получение NRE при вызове пропатченного метода). Удаление этого делает эту функцию для меня - person Max; 18.11.2018
comment
Это вызывает System.ExecutionEngineException в методах экземпляра с возвращаемым значением и параметрами. Не работает и для функций без параметров, но возвращает неинициализированную память, если вы выполняете отладку через нее. (ведет себя иначе с vs без подключенного отладчика). - person Sellorio; 26.07.2019
comment
В .NETCore 3.X он работает частично, но ненадолго. Похоже, указатели что-то отменяют. Это статические методы, которые я пытаюсь поменять местами. - person Dealdiane; 13.05.2020
comment
Кто-нибудь уже знает, как запустить код, когда отладчик не подключен во время модульного теста? Если отладчик прикреплен, код работает нормально. - person Dragonblf; 24.05.2020
comment
Для этого я отменил #IF DEBUG. Но для меня это не работает, когда у меня прикреплен отладчик @Dragonblf - person Evgeny Gorbovoy; 09.11.2020

Вы МОЖЕТЕ изменить содержимое метода во время выполнения. Но вы не должны этого делать, и настоятельно рекомендуется сохранить это в тестовых целях.

Просто взгляните на:

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

В принципе, вы можете:

  1. Получить содержимое метода IL через MethodInfo.GetMethodBody (). GetILAsByteArray ()
  2. Возьми эти байты.

    Если вы просто хотите добавить или добавить какой-то код, просто добавьте коды операций preprend / append, которые вы хотите (однако будьте осторожны, оставляя стек чистым)

    Вот несколько советов по "распаковке" существующего IL:

    • Bytes returned are a sequence of IL instructions, followed by their arguments (if they have some - for instance, '.call' has one argument: the called method token, and '.pop' has none)
    • Соответствие между кодами IL и байтами, которые вы найдете в возвращаемом массиве, можно найти с помощью OpCodes.YourOpCode.Value (который является реальным значением байта кода операции, сохраненным в вашей сборке)
    • Аргументы, добавленные после кодов IL, могут иметь разный размер (от одного до нескольких байтов), в зависимости от вызываемого кода операции.
    • Вы можете найти токены, на которые ссылаются эти аргументы, с помощью соответствующих методов. Например, если ваш IL содержит ".call 354354" (кодируется как 28 00 05 68 32 в шестнадцатеричной системе, 28h = 40 - это код операции '.call' и 56832h = 354354), соответствующий вызываемый метод можно найти с помощью MethodBase.GetMethodFromHandle (354354 )
  3. После изменения ваш байтовый массив IL можно повторно ввести через InjectionHelper.UpdateILCodes (метод MethodInfo, byte [] ilCodes) - см. Ссылку, указанную выше

    Это «небезопасная» часть ... Работает хорошо, но заключается во взломе внутренних механизмов CLR ...

person Olivier    schedule 16.10.2013
comment
Чтобы быть педантичным, 354354 (0x00056832) не является допустимым токеном метаданных, старший байт должен быть 0x06 (MethodDef), 0x0A (MemberRef) или 0x2B (MethodSpec). Кроме того, токен метаданных должен быть записан в порядке байтов с прямым порядком байтов. Наконец, токен метаданных зависит от модуля, и MethodInfo.MetadataToken вернет токен из объявляющего модуля, что сделает его непригодным для использования, если вы хотите вызвать метод, не определенный в том же модуле, что и метод, который вы изменяете. - person Brian Reichle; 02.01.2015

вы можете заменить его, если метод не виртуальный, не общий, не универсального типа, не встроен и находится на платформе 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.
person Teter28    schedule 30.12.2014
comment
Это выглядит безумно опасным. Я очень надеюсь, что никто не использует его в производственном коде. - person Brian Reichle; 02.01.2015
comment
Это используется инструментами мониторинга производительности приложений (APM), а также используется в производственной среде. - person Martin Kersten; 18.09.2015
comment
Спасибо за ответ. Я работаю над проектом, предлагающим такую ​​возможность, как API-интерфейс ориентированного на аспекты программирования. Я устранил свое ограничение на управление виртуальным методом и универсальным методом как на x86, так и на x64. Дайте мне знать, если вам понадобятся подробности. - person Teter28; 18.09.2015
comment
Что такое метаданные класса? - person Sebastian; 20.09.2016
comment
Этот ответ является псевдокодом и устарел. Многие методы больше не существуют. - person N-ate; 12.12.2017
comment
@ Teter28, я пытаюсь понять, как можно заменить метод внутри дженерика. Я знаю, что это не работает с дженериками, но знаете почему? - person johnny 5; 17.01.2020

Основываясь на ответах на этот и еще один вопрос, ive придумала эту упрощенную версию:

// 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
        }
    }
person TakeMeAsAGuest    schedule 06.03.2019
comment
На данный момент это лучший ответ - person Evgeny Gorbovoy; 09.05.2020
comment
было бы полезно добавить пример использования - person kofifus; 15.06.2020
comment
Удивительный! Я просто попробовал, и это работает ~ 0 ~ Но мне интересно, как это работает. Не могли бы вы мне что-нибудь об этом рассказать? ссылку или тему, по которой можно найти ответ? - person south; 25.08.2020
comment
Не работает для динамически генерируемого methodToInject. Выбрасывает все, например AccessViolationException, Internal CLR error, SEXException и т. Д. - person Evgeny Gorbovoy; 09.11.2020
comment
Ой, извини. Чтобы было ясно, он не работает с динамически сгенерированным methodToInject, когда отладчик подключен - person Evgeny Gorbovoy; 09.11.2020

Существует пара фреймворков, которые позволяют динамически изменять любой метод во время выполнения (они используют интерфейс ICLRProfiling, упомянутый пользователем 152949):

  • Prig: бесплатно и с открытым исходным кодом!
  • Microsoft Fakes: коммерческие, включены в Visual Studio Premium и Ultimate, но не в Сообщество и профессиональные
  • Telerik JustMock: коммерческая, доступна "облегченная" версия
  • Typemock Isolator: коммерческий

Есть также несколько фреймворков, которые издеваются над внутренними компонентами .NET, они, вероятно, более хрупкие и, вероятно, не могут изменять встроенный код, но с другой стороны они полностью автономны и не требуют использования кастомная пусковая установка.

  • Harmony: лицензия MIT. Кажется, на самом деле успешно использовался в нескольких игровых модах, поддерживает как .NET, так и Mono.
  • Deviare In Process Instrumentation Engine: GPLv3 и коммерческая. Поддержка .NET в настоящее время помечена как экспериментальная, но, с другой стороны, имеет то преимущество, что она поддерживается на коммерческой основе.
person poizan42    schedule 07.12.2016

Решение 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;
        }
    }
}
person C. McCoy IV    schedule 30.08.2017
comment
Это дало мне: исключение типа «System.AccessViolationException» произошло в MA.ELCalc.FunctionalTests.dll, но не было обработано в пользовательском коде. Дополнительная информация: Попытка чтения или записи в защищенную память. Это часто указывает на то, что другая память повреждена. ,,, При замене геттера. - person N-ate; 12.12.2017
comment
У меня исключение: wapMethodBodies еще не обрабатывает IntPtr размером 8 - person Phong Dao; 18.02.2019

Вы можете заменить метод во время выполнения, используя интерфейс ICLRPRofiling..

  1. Вызовите AttachProfiler, чтобы подключиться к процессу.
  2. Позвоните по телефону SetILFunctionBody для замены кода метода.

См. этот блог для подробнее.

person Community    schedule 27.06.2016

Я знаю, что это не точный ответ на ваш вопрос, но обычный способ сделать это - использовать подход фабрики / прокси.

Сначала мы объявляем базовый тип.

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>>.
person Salvatore Previti    schedule 04.09.2011
comment
Хм .. это все еще требует работы от имени программиста, чтобы активно осознавать распределенную обработку; Я искал решение, которое полагается только на то, что они устанавливают атрибут [Распределенный] в методе (а не на создание подклассов или наследование от ContextBoundObject). Похоже, мне может потребоваться внести некоторые изменения в сборки после компиляции, используя Mono.Cecil или что-то в этом роде. - person June Rhodes; 05.09.2011
comment
Я бы не сказал, что это обычный способ. Этот способ прост с точки зрения требуемых навыков (нет необходимости понимать CLR), но он требует повторения тех же шагов для каждого из заменяемых методов / классов. Если позже вы захотите что-то изменить (например, выполнить какой-то код после, а не только до), вам придется сделать это N раз (в отличие от небезопасного кода, который требует сделать это один раз). Так что это N часов работы против 1 часа работы) - person Evgeny Gorbovoy; 16.06.2020

загляните в Mono.Cecil:

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...
   }
}
person Martin.Martinsson    schedule 14.10.2020

Основываясь на ответе TakeMeAsAGuest, вот аналогичное расширение, которое не требует использования небезопасных блоков.

Вот класс 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 = 0xfff; // 3 bytes
            const int MT_OFFSET_32BIT = 0x28;      // 40 bytes
            const int MT_OFFSET_64BIT = 0x40;      // 64 bytes

            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
                // Remark: the slot number is represented on 3 bytes
                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 (MethodRedirect).

person Spinicoffee    schedule 28.11.2020