แทนที่เนื้อหาของวิธี C # แบบไดนามิก?


114

สิ่งที่ฉันต้องการทำคือเปลี่ยนวิธีการดำเนินการของ 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 (ซึ่งฉันทำได้อยู่แล้ว) จากนั้นใส่โค้ดก่อนที่เนื้อหาของฟังก์ชันจะดำเนินการและหลังจากที่ฟังก์ชันกลับมา ที่สำคัญกว่านั้นคือฉันต้องสามารถทำได้โดยไม่ต้องแก้ไขโค้ดที่มีการเรียกใช้การแก้ปัญหาหรือเมื่อเริ่มต้นฟังก์ชัน (ในเวลาคอมไพล์การทำเช่นนั้นในเวลาทำงานเป็นวัตถุประสงค์)

ในขณะนี้ฉันได้ลองใช้บิตของโค้ดนี้แล้ว(สมมติว่า t เป็นประเภทที่มีการแก้ปัญหาและ m คือ MethodInfo ของการแก้ปัญหา) :

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 ตลอดจนอัปเดตการเรียกทั้งหมดเพื่อแก้ไข () เพื่อให้ จะชี้ไปที่สำเนาใหม่


3
ไม่สามารถเปลี่ยนวิธีการที่โหลดไว้แล้ว ไม่งั้น Spring.Net คงไม่ต้องทำเรื่องแปลก ๆ กับพร็อกซีและอินเทอร์เฟซ :-) อ่านคำถามนี้มันสัมผัสได้ถึงปัญหาของคุณ: stackoverflow.com/questions/25803/… (หากคุณสามารถสกัดกั้นได้คุณสามารถทำสิ่งที่ต้องการได้ - สลับมัน ... ถ้าคุณไม่สามารถ 1 ได้อย่างชัดเจนคุณจะทำไม่ได้ 2)
xanatos

ในกรณีนั้นจะมีวิธีคัดลอกเมธอดไปยังโมดูลไดนามิกหรือไม่และอัปเดตส่วนที่เหลือของแอสเซมบลีที่เรียกให้เมธอดนั้นชี้ไปที่สำเนาใหม่หรือไม่
June Rhodes

เก่าเหมือนเดิม ถ้าทำได้ง่ายๆคอนเทนเนอร์ IoC ต่างๆก็น่าจะทำได้ พวกเขาไม่ทำ -> 99% มันทำไม่ได้ :-) (หากไม่มีแฮ็กที่น่ากลัวและไร้เดียงสา) มีความหวังเดียว: พวกเขาสัญญาว่าจะมีการเขียนโปรแกรมเมตาโพรแกรมและ async ใน C # 5.0 Async ที่เราเคยเห็น ... Metaprogramming ไม่มีอะไร ... แต่มันอาจจะเป็นได้!
xanatos

1
คุณไม่ได้อธิบายจริงๆว่าทำไมคุณถึงอยากปล่อยให้ตัวเองเจ็บปวด
DanielOfTaebl

6
โปรดดูคำตอบของฉันด้านล่าง นี้เป็นไปได้ทั้งหมด คุณไม่ได้เป็นเจ้าของรหัสและระหว่างรันไทม์ ฉันไม่เข้าใจว่าทำไมหลายคนถึงคิดว่าสิ่งนี้เป็นไปไม่ได้
Andreas Pardeike

คำตอบ:


212

การเปิดเผยข้อมูล: Harmony เป็นห้องสมุดที่เขียนและดูแลโดยฉันผู้เขียนโพสต์นี้

Harmony 2เป็นไลบรารีโอเพนซอร์ส (ใบอนุญาต MIT) ที่ออกแบบมาเพื่อแทนที่ตกแต่งหรือแก้ไขวิธีการ C # ที่มีอยู่ไม่ว่าจะเป็นแบบใดก็ตามระหว่างรันไทม์ จุดสนใจหลักคือเกมและปลั๊กอินที่เขียนด้วย Mono หรือ. NET ดูแลการเปลี่ยนแปลงหลายอย่างในวิธีการเดียวกัน - จะสะสมแทนที่จะเขียนทับซึ่งกันและกัน

สร้างวิธีการแทนที่แบบไดนามิกสำหรับทุกวิธีการดั้งเดิมและส่งรหัสไปยังพวกเขาที่เรียกใช้วิธีการแบบกำหนดเองที่จุดเริ่มต้นและจุดสิ้นสุด นอกจากนี้ยังช่วยให้คุณสามารถเขียนตัวกรองเพื่อประมวลผลรหัส IL ดั้งเดิมและตัวจัดการข้อยกเว้นแบบกำหนดเองซึ่งช่วยให้สามารถจัดการวิธีการดั้งเดิมได้อย่างละเอียดมากขึ้น

เพื่อให้กระบวนการเสร็จสมบูรณ์มันเขียนแอสเซมเบลอร์อย่างง่ายกระโดดลงในแทรมโพลีนของวิธีการดั้งเดิมที่ชี้ไปยังแอสเซมเบลอร์ที่สร้างขึ้นจากการคอมไพล์เมธอดไดนามิก ใช้งานได้กับ 32 / 64Bit บน 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()
    {
        // ...
    }
}

ดูซอร์สโค้ดน่าสนใจมาก! คุณช่วยอธิบาย (ที่นี่และ / หรือในเอกสารประกอบMemory.WriteJump) ได้ไหมว่าคำแนะนำเฉพาะทำงานอย่างไรซึ่งใช้ในการกระโดด (เข้า)
ทอม

1
เท่าที่ฉันบอกได้ว่ามันยังไม่รองรับ. NET Core 2 แต่การได้รับข้อยกเว้นบางอย่างกับ AppDomain.CurrentDomain.DefineDynamicAssembly
สูงสุด

1
เพื่อนของฉัน 0x0ade พูดกับฉันว่ามีทางเลือกที่โตน้อยกว่าซึ่งทำงานบน. NET Core คือ MonoMod.RuntimeDetour บน NuGet
Andreas Pardeike

1
อัปเดต: โดยรวมการอ้างอิงถึง System.Reflection.Emit ตอนนี้ Harmony รวบรวมและทดสอบตกลงกับ. NET Core 3
Andreas Pardeike

1
@rraallvv ในการแทนที่เมธอดคุณจะต้องสร้างคำนำหน้าที่มีอาร์กิวเมนต์ทั้งหมดของค่าบวกดั้งเดิม__instance(ถ้าไม่ใช่แบบคงที่) ref __resultและปล่อยให้มันกลับเท็จเพื่อข้ามต้นฉบับ ในนั้นคุณใช้ __instance และคุณกำหนดผลลัพธ์ให้กับ __result จากนั้นส่งคืนเท็จ
Andreas Pardeike

183

สำหรับ. 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");
        }
    }

}

15
สิ่งนี้สมควรได้รับการโหวตเพิ่มขึ้นมากมาย ฉันมีสถานการณ์ที่แตกต่างไปจากเดิมอย่างสิ้นเชิง แต่ตัวอย่างข้อมูลนี้เป็นสิ่งที่ฉันต้องการเพื่อกำหนดทิศทางที่ถูกต้อง ขอบคุณ.
SC

2
@Logman คำตอบที่ดี แต่คำถามของฉันคือเกิดอะไรขึ้นในโหมดดีบั๊ก และสามารถแทนที่คำสั่งเพียงคำสั่งเดียวได้หรือไม่? ตัวอย่างเช่นหากฉันต้องการแทนที่การกระโดดแบบมีเงื่อนไขในการกระโดดที่ไม่มีเงื่อนไข? AFAIK คุณกำลังเปลี่ยนวิธีการคอมไพล์ดังนั้นจึงไม่ใช่เรื่องง่ายที่จะตัดสินว่าเราควรเปลี่ยนเงื่อนไขใด ...
Alex Zhukovskiy

2
@AlexZhukovskiy ถ้าคุณชอบโพสต์บนกองและส่งลิงค์มาให้ฉัน ฉันจะตรวจสอบและให้คำตอบคุณหลังสุดสัปดาห์ เครื่องจักรฉันจะตรวจสอบคำถามของคุณหลังสุดสัปดาห์
Logman

2
สองสิ่งที่ฉันสังเกตเห็นเมื่อทำเช่นนี้สำหรับการทดสอบการรวมกับ MSTest: (1) เมื่อคุณใช้thisภายในinjectionMethod*()จะอ้างอิงInjectionอินสแตนซ์ในช่วงเวลาคอมไพล์แต่เป็นTargetอินสแตนซ์ระหว่างรันไทม์ (เป็นจริงสำหรับการอ้างอิงทั้งหมดไปยังสมาชิกอินสแตนซ์ที่คุณใช้ภายในการแทรก วิธี). (2) ด้วยเหตุผลบางประการ#DEBUGส่วนนี้จะใช้งานได้เฉพาะเมื่อทำการดีบักการทดสอบ แต่ไม่ใช่เมื่อเรียกใช้การทดสอบที่คอมไพล์ดีบัก ฉันสิ้นสุดขึ้นมักจะใช้#elseเป็นส่วนหนึ่ง ฉันไม่เข้าใจว่าทำไมถึงได้ผล แต่มันทำได้
Good Night Nerd Pride

2
ดีมาก. ถึงเวลาทำลายทุกสิ่ง! @GoodNightNerdPride ใช้Debugger.IsAttachedแทน#if ตัวประมวลผลล่วงหน้า
M.kazem Akhgary

25

คุณสามารถแก้ไขเนื้อหาของเมธอดได้ในขณะรันไทม์ แต่คุณไม่ควรทำและขอแนะนำอย่างยิ่งให้เก็บไว้เพื่อวัตถุประสงค์ในการทดสอบ

เพียงแค่ดูที่:

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

โดยทั่วไปคุณสามารถ:

  1. รับเนื้อหาวิธีการ IL ผ่าน MethodInfo.GetMethodBody () GetILAsByteArray ()
  2. ยุ่งกับไบต์เหล่านี้

    หากคุณต้องการเพียงแค่ใส่คำนำหน้าหรือต่อท้ายรหัสบางส่วนให้ใส่รหัสก่อนหน้า / ต่อท้ายรหัสที่คุณต้องการ (โปรดระวังในการทิ้งสแต็กให้สะอาด)

    ต่อไปนี้เป็นเคล็ดลับในการ "ไม่คอมไพล์" IL ที่มีอยู่:

    • ไบต์ที่ส่งคืนเป็นลำดับของคำสั่ง IL ตามด้วยอาร์กิวเมนต์ (ถ้ามี - ตัวอย่างเช่น ".call" มีอาร์กิวเมนต์เดียว: โทเค็นเมธอดที่เรียกว่าและ ".pop" ไม่มี)
    • ความสอดคล้องระหว่างรหัส IL และไบต์ที่คุณพบในอาร์เรย์ที่ส่งคืนสามารถพบได้โดยใช้ OpCodes.YourOpCode.Value (ซึ่งเป็นค่าไบต์ opcode จริงตามที่บันทึกไว้ในแอสเซมบลีของคุณ)
    • อาร์กิวเมนต์ที่ต่อท้ายรหัส IL อาจมีขนาดแตกต่างกัน (ตั้งแต่หนึ่งถึงหลายไบต์) ขึ้นอยู่กับรหัสที่เรียก
    • คุณอาจพบโทเค็นที่อาร์กิวเมนต์เหล่านี้อ้างถึงผ่านวิธีการที่เหมาะสม ตัวอย่างเช่นหาก IL ของคุณมี ".call 354354" (รหัสเป็น 28 00 05 68 32 ใน hexa, 28h = 40 เป็น opcode '.call' และ 56832h = 354354) วิธีการเรียกที่ตรงกันสามารถพบได้โดยใช้ MethodBase.GetMethodFromHandle (354354 )
  3. เมื่อแก้ไขแล้วคุณสามารถใส่อาร์เรย์ไบต์ IL ของคุณใหม่ได้โดยใช้ InjectionHelper.UpdateILCodes (MethodInfo method, byte [] ilCodes) - ดูลิงค์ที่กล่าวถึงข้างต้น

    นี่คือส่วนที่ "ไม่ปลอดภัย" ... ทำงานได้ดี แต่ประกอบด้วยการแฮ็กกลไก CLR ภายใน ...


7
เพื่อเป็นการอวดดี 354354 (0x00056832) ไม่ใช่โทเค็นข้อมูลเมตาที่ถูกต้องไบต์ลำดับสูงควรเป็น 0x06 (MethodDef), 0x0A (MemberRef) หรือ 0x2B (MethodSpec) นอกจากนี้โทเค็นข้อมูลเมตาควรเขียนตามลำดับไบต์ endian น้อย สุดท้ายโทเค็นข้อมูลเมตาเป็นเฉพาะโมดูลและ MethodInfo.MetadataToken จะส่งคืนโทเค็นจากโมดูลการประกาศทำให้ไม่สามารถใช้งานได้หากคุณต้องการเรียกใช้เมธอดที่ไม่ได้กำหนดไว้ในโมดูลเดียวกับวิธีที่คุณกำลังแก้ไข
Brian Reichle

13

คุณสามารถแทนที่ได้หากวิธีการนั้นไม่ใช่เสมือนไม่ใช่ทั่วไปไม่ใช่ในประเภททั่วไปไม่ใช่แบบอินไลน์และบนแผ่นฟอร์ม 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.

นั่นดูอันตรายอย่างบ้าคลั่ง ฉันหวังว่าจะไม่มีใครใช้มันในรหัสการผลิต
Brian Reichle

2
สิ่งนี้ถูกใช้โดยเครื่องมือการตรวจสอบประสิทธิภาพของแอปพลิเคชัน (APM) และใช้ในการผลิตด้วย
Martin Kersten

1
ขอบคุณสำหรับการตอบกลับฉันกำลังทำโครงการเพื่อเสนอความสามารถประเภทนี้ในชื่อ Aspect Oriented Programming API ฉันแก้ไขข้อ จำกัด ของฉันในการจัดการวิธีการเสมือนและวิธีการทั่วไปทั้งบน x86 & x64 โปรดแจ้งให้เราทราบหากคุณต้องการรายละเอียดเพิ่มเติม
Teter28

6
Metadata ของคลาสคืออะไร
Sebastian

คำตอบนี้เป็นรหัสหลอกและล้าสมัย หลายวิธีไม่มีอยู่แล้ว
N-ate

9

มีเฟรมเวิร์กอยู่สองตัวที่อนุญาตให้คุณเปลี่ยนเมธอดใด ๆ แบบไดนามิกในรันไทม์ (ใช้อินเทอร์เฟซ ICLRProfiling ที่กล่าวถึงโดย user152949):

  • Prig : ฟรีและโอเพ่นซอร์ส!
  • Microsoft Fakes : เชิงพาณิชย์รวมอยู่ใน Visual Studio Premium และ Ultimate แต่ไม่ใช่ Community และ Professional
  • Telerik JustMock : Commercial เวอร์ชัน "lite" พร้อมใช้งานแล้ว
  • Typemock Isolator : เชิงพาณิชย์

นอกจากนี้ยังมีกรอบงานบางส่วนที่ล้อเลียนกับภายในของ. NET ซึ่งมีแนวโน้มที่จะเปราะบางกว่าและอาจไม่สามารถเปลี่ยนรหัสแบบอินไลน์ได้ แต่ในทางกลับกันพวกเขามีอยู่ในตัวเองอย่างสมบูรณ์และไม่จำเป็นต้องให้คุณใช้ a ตัวเรียกใช้งานที่กำหนดเอง

  • Harmony : MIT ได้รับอนุญาต ดูเหมือนว่าจะถูกนำมาใช้อย่างประสบความสำเร็จใน mod เกมบางตัวรองรับทั้ง. NET และ Mono
  • Deviare ในเครื่องมือวัดกระบวนการ : GPLv3 และเชิงพาณิชย์ ขณะนี้การสนับสนุน. NET ถูกทำเครื่องหมายเป็นรุ่นทดลอง แต่ในทางกลับกันมีประโยชน์ในการได้รับการสนับสนุนในเชิงพาณิชย์

8

โซลูชันของ 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;
        }
    }
}

1
สิ่งนี้ทำให้ฉัน: มีข้อยกเว้นประเภท 'System.AccessViolationException' เกิดขึ้นใน MA.ELCalc.FunctionalTests.dll แต่ไม่ได้รับการจัดการในรหัสผู้ใช้ข้อมูลเพิ่มเติม: พยายามอ่านหรือเขียนหน่วยความจำที่มีการป้องกัน นี่มักเป็นข้อบ่งชี้ว่าหน่วยความจำอื่นเสียหาย ,,, เมื่อเปลี่ยน getter
N-ate

ฉันได้รับข้อยกเว้น "wapMethodBodies ยังไม่รองรับ IntPtr ขนาด 8"
ฟองดาว

8

จากคำตอบสำหรับคำถามนี้และอีกประการหนึ่ง 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
        }
    }

สำหรับช่วงเวลานี้คำตอบที่ดีที่สุด
Eugene Gorbovoy

จะเป็นประโยชน์ในการเพิ่มตัวอย่างการใช้งาน
kofifus

สุดทึ่ง! ฉันเพิ่งลองและได้ผล ~ 0 ~ แต่ฉันสงสัยว่ามันทำงานอย่างไร คุณช่วยบอกฉันเกี่ยวกับเรื่องนี้ได้ไหม ลิงค์หรือหัวข้อเพื่อให้ค้นหาคำตอบ?
ใต้

5

คุณสามารถเปลี่ยนวิธีการที่รันไทม์โดยใช้ICLRPRofiling อินเตอร์เฟซ

  1. เรียกAttachProfilerเพื่อแนบไปกับกระบวนการ
  2. เรียกใช้SetILFunctionBodyเพื่อแทนที่รหัสวิธีการ

ดูบล็อกนี้สำหรับรายละเอียดเพิ่มเติม


3

ฉันรู้ว่านี่ไม่ใช่คำตอบที่แน่นอนสำหรับคำถามของคุณ แต่วิธีปกติในการดำเนินการคือใช้วิธีการโรงงาน / พร็อกซี

อันดับแรกเราจะประกาศประเภทพื้นฐาน

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

1
อืม .. ที่ยังต้องทำงานในนามของโปรแกรมเมอร์เพื่อรับรู้การประมวลผลแบบกระจาย ฉันกำลังมองหาโซลูชันที่อาศัยการตั้งค่าแอตทริบิวต์ [Distributed] บนเมธอดเท่านั้น (ไม่ใช่คลาสย่อยหรือสืบทอดจาก ContextBoundObject) ดูเหมือนว่าฉันอาจต้องทำการแก้ไขโพสต์คอมไพล์ในชุดประกอบโดยใช้ Mono.Cecil หรืออะไรทำนองนั้น
June Rhodes

ฉันจะไม่พูดว่านี่เป็นวิธีปกติ วิธีนี้ง่ายในแง่ของทักษะที่จำเป็น (ไม่จำเป็นต้องเข้าใจ CLR) แต่ต้องทำซ้ำขั้นตอนเดียวกันสำหรับแต่ละวิธี / คลาสที่ถูกแทนที่ หากคุณต้องการเปลี่ยนแปลงบางสิ่งในภายหลัง (ตัวอย่างเช่นรันโค้ดบางอย่างหลังจากไม่ใช่ก่อนหน้านี้เท่านั้น) คุณจะต้องทำ N ครั้ง (ตรงกันข้ามกับโค้ดที่ไม่ปลอดภัยซึ่งต้องทำครั้งเดียว) ดังนั้นจึงเป็นงาน N ชั่วโมงเทียบกับงาน 1 ชั่วโมง)
Eugene Gorbovoy
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.