3

Disclaimer: I'm doing this for learning purposes. This is not going to be used in code.

I'm trying to understand how method table are structure for generics, I want to dynamically appending to methods at runtime. I found a very useful stack overflow question reference for getting me started.

I have a simple controller which I'm using as a test to verify my methods are swapping:

public class ValuesController : ControllerBase
{
    static ValuesController() {
        var methodToReplace = typeof(ValuesController).GetMethod(nameof(ValuesController.Seven),
            BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);

        var methodToAppend = typeof(ValuesController).GetMethod(nameof(ValuesController.Eight),
            BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);

        new Initializer(methodToReplace, methodToAppend);
    }

    [HttpGet("Seven")]
    public int Seven(string id)
    {
        return 7;
    }

    [HttpGet("Eight")]
    public int Eight(string id)
    {
        return 8;
    }
}

I have a class Initializer which is in charge of handling appending to the method.

public class Initializer
{
    public Initializer(MethodInfo methodToReplace, MethodInfo methodToAppend)
    {
        var dummyMethod = typeof(Initializer).GetMethod(nameof(Dummy),
            BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);

        var proxyMethod = typeof(Initializer).GetMethod(nameof(Proxy),
            BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);

        var appendedMethod = typeof(Initializer).GetMethod(nameof(Appended),
            BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);

        dummyMethod.OneWayReplace(methodToReplace);
        methodToReplace.OneWayReplace(proxyMethod);
        appendedMethod.OneWayReplace(methodToAppend);
    }

    public int Proxy(string id)
    {
        Dummy(id);
        return Appended(id);
    }

    public int Dummy(string id)
    {
        return 0;
    }

    public int Appended(string id)
    {
        return 0;
    }
}

And then I have the Extensions which I've obtained from the original stackoverflow question:

public static class InjectionExtensions
{
    // Note: This method replaces methodToReplace with methodToInject
    // Note: methodToInject will still remain pointing to the same location
    public static unsafe MethodReplacementState OneWayReplace(this MethodInfo methodToReplace, MethodInfo methodToInject)
    {
        //#if DEBUG
        RuntimeHelpers.PrepareMethod(methodToReplace.MethodHandle);
        RuntimeHelpers.PrepareMethod(methodToInject.MethodHandle);
        //#endif
        MethodReplacementState state;

        IntPtr tar = methodToReplace.MethodHandle.Value;
        var inj = methodToInject.MethodHandle.Value + 8;

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

Note: Using the current setup everything works fine. However, the second I change the Initializer class to be a generic class Initializer<T> I get a memory violation:

System.AccessViolationException: 'Attempted to read or write protected memory. This is often an indication that other memory is corrupt.'

My guess is that either the methodToReplace.DeclaringType.TypeHandle.Value calculation differs for generics, Or since the compiler is the one who generates the generic class it written to protected memory?

Edit I've found more information I need to prepare the method properly when using generic parameters e.g:

RuntimeHelpers.PrepareMethod(methodToInject.MethodHandle, new[] { typeof(T).TypeHandle });

However there are still a few more pieces to the puzzle to get this working.

Edit

There are a few open source project's such as harmony that do similar things, However it looks like their emitting their own assemblies. While I've considered the option, I would still prefer to understand how I method tables work with generics

How can I append to methods that reside in generic classes?

johnny 5
  • 19,893
  • 50
  • 121
  • 195
  • `DeclaringType` will be generic type in this case, right? Do you need to rebuild the method signature like in this [SO answer](https://stackoverflow.com/questions/232535/how-do-i-use-reflection-to-call-a-generic-method)? – timur Jan 17 '20 at 20:41
  • 1
    @timur, the DeclaringType is generic, however, the method has no reference to the generic type. Generics clearly have some affects on the method swap. But I don't have enough information to understand, my guess is the declaring type modifies the address calculation of the class start. So I'm writing to protected memory because I'm overwriting a memory barrier. I'm a bit confused on what you mean by rebuilding the method signature, I'm not invoking the method though reflection, I'm just swapping the pointers – johnny 5 Jan 18 '20 at 23:30

1 Answers1

1

I suppose you have already seen: Dynamically replace the contents of a C# method?

I have adapted some of those methods in my own project @ https://github.com/juliusfriedman/net7mma_core/blob/master/Concepts/Classes/MethodHelper.cs

I think the problem is that if your are running with the Debugger Attached then you need to also handle the portion of the logic which is currently defined by IFDEF at compilation time and replace that with an System.Diagnostics.Debugger.IsAttached although the offsets calculations (to jump over the debugger injected code) will probably have to change depending on various things like the version of the framework in use.

See https://github.com/juliusfriedman/net7mma_core/blob/master/Concepts/Classes/MethodHelper.cs#L35

This works for me in .Net Core 3.1 when the debugger IS NOT attached and I am running in Release mode, when running in Debug mode with or without the debugger attached or in Release mode with the debugger attached I receive different exceptions. (In debug I receive Arithmetic Overflow, while in release I receive Execution Engine Exception).

Furthermore this only works until the JIT Tiering kicks in, if I run the method a 2nd time without the debugger attached I am getting a Internal CLR Error.

I believe this has to do with the code injected by the debugger when attached and to be honest I am not up to do date on exactly what the debugger is injecting when attached.

I would make a simplified repo of the problem and ask a question @ https://github.com/dotnet/runtime if you need this to work with the debugger attached and I am sure someone there will guide you in the right direction.

Jay
  • 3,276
  • 1
  • 28
  • 38
  • I'm usually running in debug mode, with the debugger attached, The code I posted works fine it's only when I switch the Initalizer to be generic in which it breaks or if I try to swap a generic method, I think it's because of how generics are done under the hood. Are you saying this will work in release mode? – johnny 5 Feb 16 '20 at 17:41
  • It might at least the first time. – Jay Feb 16 '20 at 20:34