内存违规:在运行时动态添加方法

3

免责声明:我这样做是为了学习目的,不会在代码中使用。

我正在尝试理解泛型方法表的结构,我想在运行时动态添加方法。我发现一个非常有用的堆栈溢出问题的参考资料,让我开始了解。

我有一个简单的控制器,我正在使用它作为测试来验证我的方法是否交换:

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

我有一个名为 Initializer 的类,负责处理方法的追加。
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;
    }
}

然后我有一些扩展,这些扩展是从原始的stackoverflow问题中获得的:

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

注意: 使用当前设置一切正常。但是,一旦我将Initializer类更改为泛型类Initializer<T>,就会出现内存违规的问题:

System.AccessViolationException: '尝试读取或写入受保护的内存。这通常是其他内存损坏的指示。'

我猜测可能是methodToReplace.DeclaringType.TypeHandle.Value计算在泛型情况下有所不同,或者由于编译器生成泛型类,因此它被写入了受保护的内存?

编辑 我发现了更多信息,在使用泛型参数时需要正确准备这个方法,例如:

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

然而,要使它正常工作,还需要解决一些难题。 编辑 有一些开源项目(例如harmony),它们做了类似的事情。然而,看起来它们正在发出自己的程序集。虽然我已经考虑过这个选项,但我仍然更愿意了解如何处理泛型中的方法表。
如何将内容添加到泛型类中的方法?

DeclaringType 在这种情况下将是泛型类型,对吗?你需要像这个 SO answer 中那样重新构建方法签名吗? - timur
1
@timur,DeclaringType是泛型的,但是该方法没有引用泛型类型。泛型显然对swap方法有一些影响。但是我没有足够的信息来理解,我的猜测是声明类型修改了类开始的地址计算。所以我正在写入受保护的内存,因为我正在覆盖内存屏障。关于您所说的重建方法签名,我有点困惑,我不是通过反射调用方法,我只是交换指针。 - johnny 5
1个回答

1

我想你已经看到了:如何动态替换C#方法的内容?

我在自己的项目中使用了其中的一些方法,链接为 https://github.com/juliusfriedman/net7mma_core/blob/master/Concepts/Classes/MethodHelper.cs

我认为问题是,如果您正在使用调试器,则还需要处理目前由IFDEF在编译时定义的逻辑部分,并将其替换为System.Diagnostics.Debugger.IsAttached,尽管偏移量计算(以跳过注入的调试器代码)可能会因多种因素而改变,例如所使用的框架版本。

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

当调试器未连接且以发布模式运行时,.Net Core 3.1 对我有效;但在调试模式下无论是否连接调试器或在带有调试器的发布模式下,都会收到不同的异常(在调试模式下,我收到算术溢出异常,而在发布模式下,我收到执行引擎异常)。

此外,只有在JIT分层启动之前才能正常工作,如果我在未连接调试器的情况下再次运行该方法,我会收到内部CLR错误。

我认为这与调试器连接时注入的代码有关,老实说,我对调试器连接时注入的内容并不了解。

如果您需要在连接调试器的情况下使其正常工作,并且需要简化问题的存储库,请在https://github.com/dotnet/runtime上提问,我相信那里的某个人会指导您正确的方向。


我通常在调试模式下运行,并附加了调试器。我发布的代码正常运行,只有当我将初始化器切换为通用类型时它才会出现错误,或者如果我尝试交换通用方法,我认为这是由于泛型背后的实现机制所导致的。你是在说这个在发布模式下会工作吗? - johnny 5
这可能至少是第一次。 - Jay

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接