Hitmasu / Jitex

A library to modify MSIL and native code at runtime
MIT License
119 stars 15 forks source link

Can Jitex still intercept methods that have already been JIT-compiled? #85

Open InCerryGit opened 1 year ago

InCerryGit commented 1 year ago

Hi! This is a great project and work. I have a question about Jitex: can it be used for methods that have already been JIT-compiled? Because many times, we may not know in advance which methods need to be intercepted; it's only during the program's runtime that we discover the methods that need to be intercepted, and by that time, they may have already been JIT-compiled. I would like to know if Jitex can be applied in such a scenario. If it is currently not supported, are there any plans to implement this feature in the future?

Thank you for reading!

Hitmasu commented 1 year ago

Hi!

Thanks a lot!

Yes, Jitex can intercept methods already compiled by JIT. You can use: MethodHelper.ForceRecompile to help you. This method force JIT compile method again, making possible to Jitex intercept.

See this example intercepting a method Sum:

using Jitex;

var result = MyMath.Sum(1, 1);
Console.WriteLine(result);

JitexManager.MethodResolver += context =>
{
    if (context.Method.Name == "Sum")
        context.InterceptCall();
};

JitexManager.Interceptor += async context =>
{
    if (context.Method.Name == "Sum")
    {
        Console.WriteLine("Method sum intercepted");
        context.SetReturnValue(-1);
    }
};

//Will not be intercepted, because was already compiled.
result = MyMath.Sum(10, 10);
Console.WriteLine(result);

class MyMath
{
    public static int Sum(int a, int b)
    {
        Console.WriteLine("Sum called");
        return a + b;
    }
}

Output:

Sum called
2
Sum called
20

Calling MethodHelper.ForceRecompile:

using System.Reflection;
using Jitex;
using Jitex.Utils;

var result = MyMath.Sum(1, 1);
Console.WriteLine(result);

JitexManager.MethodResolver += context =>
{
    if (context.Method.Name == "Sum")
        context.InterceptCall();
};

JitexManager.Interceptor += async context =>
{
    if (context.Method.Name == "Sum")
    {
        Console.WriteLine("Method sum intercepted");
        context.SetReturnValue(-1);
    }
};

var sumMethod = typeof(MyMath).GetMethod("Sum", (BindingFlags)(-1));

//Force jit compile method again
MethodHelper.ForceRecompile(sumMethod);

result = MyMath.Sum(10, 10);
Console.WriteLine(result);

class MyMath
{
    public static int Sum(int a, int b)
    {
        Console.WriteLine("Sum called");
        return a + b;
    }
}

Output:

Sum called
2
Method sum intercepted
-1

If your method is a R2R, you can try:

MethodHelper.DisableReadyToRun(sumMethod)
MethodHelper.ForceRecompile(sumMethod)

Tell me if worked.

InCerryGit commented 1 year ago

Thanks a lot, I'll try it out and let you know the result

InCerryGit commented 1 year ago

Yes, it worked, at first I was using .NET Core 3.1 version but it reported error Recompile method is only supported on .NET 5 or above.. Then I switched to .NET 6.0 and it worked. But it seems that now it doesn't support .NET 7.0 and throws the following exception:

Fatal error. internal CLR error.(0x80131506)
   at Jitex.JIT.CorInfo.CEEInfo.ConstructStringLiteral(IntPtr, IntPtr, Int32, IntPtr)
   at Jitex.JIT.ManagedJit.ConstructStringLiteral(IntPtr, IntPtr, Int32, IntPtr)
   at Jitex.JIT.ManagedJit.CompileMethod(IntPtr, IntPtr, IntPtr, UInt32, IntPtr ByRef, Int32 ByRef)
   at Jitex.JitexManager.get_IsEnabled()
   at Jitex.JitexManager.AddMethodResolver(MethodResolverHandler)
   JitexManager.add_MethodResolver(MethodResolverHandler) at Jitex.
   at Program.<Main>$(System.String[])

Are there any plans for .NET Core 2.1/3.1 support for the Recompile method in the future, and will Jitex support .NET 7.0?

Hitmasu commented 1 year ago

Thanks!

For .NET Core 2.1/3.1, when I developed that feature, I couldn't make it work with .NET Core; it only works with .NET 5 or above. That's the reason it doesn't work on those versions. I will try to make it compatible with these versions again.

Regarding .NET 7, yes, I can make it work on .NET 7. Honestly, Jitex development was paused on .NET 6 because no one was using Jitex.

I'll work to add support for .NET 7 this week. It will probably take 2 weeks to be completed. After that, I will try to enable ForceRecompile on versions below .NET 5.

InCerryGit commented 1 year ago

I am highly anticipating the support of .NET 7.0, and the release of .NET 8.0 in just a few months' time. I recognize that the development of Jitex is a formidable and challenging undertaking, and many people have yet to fully realize its value. The ability to non-invasively modify any method's IL is truly remarkable, and there is no other project that can achieve this functionality. I am currently planning to utilize Jitex for a project of my own, as it perfectly satisfies my requirements.

InCerryGit commented 1 year ago

I have another concern, and I am unsure if this is a problem with my usage or something else, or if I should open a new issue. This is a test project for .NET 6.0, and I am attempting to retrieve the result of DateTime.Now. However, it seems that Console.WriteLine($"After interceptor! Result:{context.GetReturnValue()}") is never executed.

using System.Reflection;
using Jitex;
using Jitex.Utils;

Console.WriteLine(DateTime.Now);
var nowMethod = typeof(DateTime).GetProperty("Now", (BindingFlags)(-1)).GetGetMethod();

JitexManager.MethodResolver += context =>
{
    Console.WriteLine(context.Method.Name);
    if (context.Method.Name == nowMethod.Name)
    {
        Console.WriteLine("MethodResolver");
        context.InterceptCall();   
    }
};

JitexManager.Interceptor += async context =>
{
    if (context.Method.Name == nowMethod.Name)
    {
        Console.WriteLine("Before interceptor! ");
        await context.ContinueAsync();
>>>>   Console.WriteLine($"After interceptor! Result:{context.GetReturnValue()}"); <<<<
    }
};

if (MethodHelper.IsReadyToRun(nowMethod))
{
    MethodHelper.DisableReadyToRun(nowMethod);
}

//Force jit compile method again
MethodHelper.ForceRecompile(nowMethod);

Console.WriteLine(DateTime.Now);

The output of the project appears as follows:

2023/7/25 8:32:21
MethodResolver
Before interceptor!
2023/7/25 8:32:21

I am not sure whether the issue lies with my usage or with something else. Thank you for reading!

Hitmasu commented 1 year ago

Yes, it's a bug.

There are multiple 'ret' instructions in the body from DateTime.Now:

[0] = {Instruction} call System.DateTime get_UtcNow()
[1] = {Instruction} stloc.0 
[2] = {Instruction} ldloc.0 
[3] = {Instruction} ldloca.s 2
[4] = {Instruction} call System.TimeSpan GetDateTimeNowUtcOffsetFromUtc(System.DateTime, Boolean ByRef)
[5] = {Instruction} stloc.s 4
[6] = {Instruction} ldloca.s 4
[7] = {Instruction} call Int64 get_Ticks()
[8] = {Instruction} stloc.1 
[9] = {Instruction} ldloca.s 0
[10] = {Instruction} call Int64 get_Ticks()
[11] = {Instruction} ldloc.1 
[12] = {Instruction} add 
[13] = {Instruction} stloc.3 
[14] = {Instruction} ldloc.3 
[15] = {Instruction} ldc.i8 3155378975999999999
[16] = {Instruction} bgt.un.s 37
[17] = {Instruction} ldloc.2 
[18] = {Instruction} brtrue.s 17
[19] = {Instruction} ldloc.3 
[20] = {Instruction} ldc.i8 -9223372036854775808
[21] = {Instruction} or 
[22] = {Instruction} newobj Void .ctor(UInt64)
[23] = {Instruction} ret   <------------------------------------- HERE
[24] = {Instruction} ldloc.3 
[25] = {Instruction} ldc.i8 -4611686018427387904
[26] = {Instruction} or 
[27] = {Instruction} newobj Void .ctor(UInt64)
[28] = {Instruction} ret <------------------------------------- AND HERE
[29] = {Instruction} ldloc.3 
[30] = {Instruction} ldc.i4.0 
[31] = {Instruction} conv.i8 
[32] = {Instruction} blt.s 11
[33] = {Instruction} ldc.i8 -6067993060854775809
[34] = {Instruction} br.s 9
[35] = {Instruction} ldc.i8 -9223372036854775808
[36] = {Instruction} newobj Void .ctor(UInt64)
[37] = {Instruction} ret  <------------------------------- WE JUST EXPECTED THIS RET

Currently, Jitex only expects one 'ret' instruction and just replace the last, not covering all paths.

I'll open a new issue for that.

InCerryGit commented 1 year ago

Okay, thank you very much for your time. There is another scenario, if the target method throws an exception, the After interceptor will not be executed. I want to use Jitex to implement AOP programming, so I need this.

I just read through the source code and noticed that your implementation of the Intercepter aspect is different from other AOP frameworks. Is it because of support for async/await? If we want to make the program more robust, we might need to modify the target method like this:

public int Sum(int n1, int n2)
{

    try
    {
        CallContext context = new CallContext(methodHandle, Pointer.Box((void*)this), Pointer.Box((void*)n1),Pointer.Box((void*)n2));
        CallManager callManager = new CallManager(context);
        callManager.CallInteceptorsAsync();        
    }
    catch (System.Exception ex)
    {
        // log exception
    }

    try
    {
        if(context.ProceedCall){
        int result = n1+n2;
        context.SetResult(Pointer.Box((void*)result);
    }
    }
    catch (System.Exception ex)
    {
        // log exception
        context.SetException(ex);
    }

    callManager.ReleaseTask();

    if(context.Exception != null)
        throw context.Exception;

    return context.GetResult<int>();
}

I have seen the implementation of Datadog before, and this is how they did it:

Rewrite the target method body with the calltarget implementation. (This is function is triggered by the ReJIT
handler) Resulting code structure:

- Add locals for TReturn (if non-void method), CallTargetState, CallTargetReturn/CallTargetReturn<TReturn>,
Exception
- Initialize locals

try
{
  try
  {
    try
    {
      - Invoke BeginMethod with object instance (or null if static method) and original method arguments
      - Store result into CallTargetState local
    }
    catch when exception is not Datadog.Trace.ClrProfiler.CallTarget.CallTargetBubbleUpException
    {
      - Invoke LogException(Exception)
    }

    - Execute original method instructions
      * All RET instructions are replaced with a LEAVE_S. If non-void method, the value on the stack is first stored
      in the TReturn local.
  }
  catch (Exception)
  {
    - Store exception into Exception local
    - throw
  }
}
finally
{
  try
  {
    - Invoke EndMethod with object instance (or null if static method), TReturn local (if non-void method),
    CallTargetState local, and Exception local
    - Store result into CallTargetReturn/CallTargetReturn<TReturn> local
    - If non-void method, store CallTargetReturn<TReturn>.GetReturnValue() into TReturn local
  }
  catch when exception is not Datadog.Trace.ClrProfiler.CallTarget.CallTargetBubbleUpException
  {
    - Invoke LogException(Exception)
  }
}

- If non-void method, load TReturn local
- RET

I will also try to see if I can solve this problem later, but I am not very familiar with MSIL. If you have time to do all of this, that would be great.

Thank you for reading!

Hitmasu commented 1 year ago

That's a good approach using exceptions. We can try to implement that in the near future.

I just read through the source code and noticed that your implementation of the Intercepter aspect is different from other AOP frameworks. Is it because of support for async/await?

I'm not familiar with how other frameworks implement Interceptors, as they require a lot of work (interfaces, virtual methods, ...) to intercept methods, and I never dug too deep to understand how they work. Perhaps we can explore new approaches in the future to simplify and enhance our implementation.

There is another scenario, if the target method throws an exception, the After interceptor will not be executed.

I'll open a new issue for this bug.

Thanks!