【问题标题】:Replacing instructions in a method's MethodBody替换方法的 MethodBody 中的指令
【发布时间】:2011-02-17 17:11:21
【问题描述】:

(首先,这是一篇很长的帖子,但别担心:我已经全部实现了,我只是在询问您的意见,或者可能的替代方案。)

我在执行以下操作时遇到了问题;我会很感激一些帮助:

  1. 我得到一个Type 作为参数。
  2. 我使用反射定义了一个子类。请注意,我不打算修改原始类型,而是创建一个新类型。
  3. 我为原始类的每个字段创建一个属性,如下所示:

    public class OriginalClass {
        private int x;
    }
    
    
    public class Subclass : OriginalClass {
        private int x;
    
        public int X {
            get { return x; }
            set { x = value; }
        }
    
    }
    
  4. 对于超类的每个方法,我在子类中创建一个类似的方法。方法的主体必须相同,只是我将指令ldfld x 替换为callvirt this.get_X,也就是说,我调用get 访问器而不是直接从字段中读取。

我在执行第 4 步时遇到了问题。我知道您不应该像这样操作代码,但我确实需要这样做。

这是我尝试过的:

尝试 #1: 使用 Mono.Cecil。这将允许我将方法的主体解析为人类可读的Instructions,并轻松替换指令。但是,原始类型不在 .dll 文件中,所以我找不到使用 Mono.Cecil 加载它的方法。将类型写入 .dll,然后加载它,然后修改它并将新类型写入磁盘(我认为这是使用 Mono.Cecil 创建类型的方式),然后加载它看起来像一个巨大的 开销。

尝试 #2: 使用 Mono.Reflection。这也可以让我将正文解析为Instructions,但是我不支持替换指令。我已经使用 Mono.Reflection 实现了一个非常丑陋且效率低下的解决方案,但它还不支持包含 try-catch 语句的方法(尽管我想我可以实现这个)并且我担心可能存在其他场景这是行不通的,因为我以一种不寻常的方式使用ILGenerator。此外,它非常难看;)。这是我所做的:

private void TransformMethod(MethodInfo methodInfo) {

    // Create a method with the same signature.
    ParameterInfo[] paramList = methodInfo.GetParameters();
    Type[] args = new Type[paramList.Length];
    for (int i = 0; i < args.Length; i++) {
        args[i] = paramList[i].ParameterType;
    }
    MethodBuilder methodBuilder = typeBuilder.DefineMethod(
        methodInfo.Name, methodInfo.Attributes, methodInfo.ReturnType, args);
    ILGenerator ilGen = methodBuilder.GetILGenerator();

    // Declare the same local variables as in the original method.
    IList<LocalVariableInfo> locals = methodInfo.GetMethodBody().LocalVariables;
    foreach (LocalVariableInfo local in locals) {
        ilGen.DeclareLocal(local.LocalType);
    }

    // Get readable instructions.
    IList<Instruction> instructions = methodInfo.GetInstructions();

    // I first need to define labels for every instruction in case I
    // later find a jump to that instruction. Once the instruction has
    // been emitted I cannot label it, so I'll need to do it in advance.
    // Since I'm doing a first pass on the method's body anyway, I could
    // instead just create labels where they are truly needed, but for
    // now I'm using this quick fix.
    Dictionary<int, Label> labels = new Dictionary<int, Label>();
    foreach (Instruction instr in instructions) {
        labels[instr.Offset] = ilGen.DefineLabel();
    }

    foreach (Instruction instr in instructions) {

        // Mark this instruction with a label, in case there's a branch
        // instruction that jumps here.
        ilGen.MarkLabel(labels[instr.Offset]);

        // If this is the instruction that I want to replace (ldfld x)...
        if (instr.OpCode == OpCodes.Ldfld) {
            // ...get the get accessor for the accessed field (get_X())
            // (I have the accessors in a dictionary; this isn't relevant),
            MethodInfo safeReadAccessor = dataMembersSafeAccessors[((FieldInfo) instr.Operand).Name][0];
            // ...instead of emitting the original instruction (ldfld x),
            // emit a call to the get accessor,
            ilGen.Emit(OpCodes.Callvirt, safeReadAccessor);

        // Else (it's any other instruction), reemit the instruction, unaltered.
        } else {
            Reemit(instr, ilGen, labels);
        }

    }

}

这里是可怕的,可怕的Reemit 方法:

private void Reemit(Instruction instr, ILGenerator ilGen, Dictionary<int, Label> labels) {

    // If the instruction doesn't have an operand, emit the opcode and return.
    if (instr.Operand == null) {
        ilGen.Emit(instr.OpCode);
        return;
    }

    // Else (it has an operand)...

    // If it's a branch instruction, retrieve the corresponding label (to
    // which we want to jump), emit the instruction and return.
    if (instr.OpCode.FlowControl == FlowControl.Branch) {
        ilGen.Emit(instr.OpCode, labels[Int32.Parse(instr.Operand.ToString())]);
        return;
    }

    // Otherwise, simply emit the instruction. I need to use the right
    // Emit call, so I need to cast the operand to its type.
    Type operandType = instr.Operand.GetType();
    if (typeof(byte).IsAssignableFrom(operandType))
        ilGen.Emit(instr.OpCode, (byte) instr.Operand);
    else if (typeof(double).IsAssignableFrom(operandType))
        ilGen.Emit(instr.OpCode, (double) instr.Operand);
    else if (typeof(float).IsAssignableFrom(operandType))
        ilGen.Emit(instr.OpCode, (float) instr.Operand);
    else if (typeof(int).IsAssignableFrom(operandType))
        ilGen.Emit(instr.OpCode, (int) instr.Operand);
    ... // you get the idea. This is a pretty long method, all like this.
}

分支指令是一种特殊情况,因为instr.OperandSByte,但Emit 需要Label 类型的操作数。因此需要Dictionary labels

如您所见,这非常可怕。更重要的是,它不适用于所有情况,例如包含 try-catch 语句的方法,因为我没有使用 ILGenerator 的方法 BeginExceptionBlockBeginCatchBlock 等发出它们。这变得越来越复杂。我想我可以做到:MethodBody 有一个ExceptionHandlingClause 列表,其中应该包含执行此操作的必要信息。但无论如何我都不喜欢这个解决方案,所以我将把它保存为最后的解决方案。

尝试 #3: 直接复制 MethodBody.GetILAsByteArray() 返回的字节数组,因为我只想将一条指令替换为另一条相同大小的指令,从而产生完全相同的结果:它在堆栈上加载相同类型的对象等。因此不会有任何标签移动,并且一切都应该完全相同。我已经这样做了,替换了数组的特定字节,然后调用MethodBuilder.CreateMethodBody(byte[], int),但我仍然得到同样的异常错误,我仍然需要声明局部变量,否则我会得到一个错误......即使当我只是复制方法的主体,不做任何更改。 所以这样效率更高,但我仍然需要处理异常等。

叹息。

以下是尝试 #3 的实现,以防有人感兴趣:

private void TransformMethod(MethodInfo methodInfo, Dictionary<string, MethodInfo[]> dataMembersSafeAccessors, ModuleBuilder moduleBuilder) {

    ParameterInfo[] paramList = methodInfo.GetParameters();
    Type[] args = new Type[paramList.Length];
    for (int i = 0; i < args.Length; i++) {
        args[i] = paramList[i].ParameterType;
    }
    MethodBuilder methodBuilder = typeBuilder.DefineMethod(
        methodInfo.Name, methodInfo.Attributes, methodInfo.ReturnType, args);

    ILGenerator ilGen = methodBuilder.GetILGenerator();

    IList<LocalVariableInfo> locals = methodInfo.GetMethodBody().LocalVariables;
    foreach (LocalVariableInfo local in locals) {
        ilGen.DeclareLocal(local.LocalType);
    }

    byte[] rawInstructions = methodInfo.GetMethodBody().GetILAsByteArray();
    IList<Instruction> instructions = methodInfo.GetInstructions();

    int k = 0;
    foreach (Instruction instr in instructions) {

        if (instr.OpCode == OpCodes.Ldfld) {

            MethodInfo safeReadAccessor = dataMembersSafeAccessors[((FieldInfo) instr.Operand).Name][0];

            // Copy the opcode: Callvirt.
            byte[] bytes = toByteArray(OpCodes.Callvirt.Value);
            for (int m = 0; m < OpCodes.Callvirt.Size; m++) {
                rawInstructions[k++] = bytes[put.Length - 1 - m];
            }

            // Copy the operand: the accessor's metadata token.
            bytes = toByteArray(moduleBuilder.GetMethodToken(safeReadAccessor).Token);
            for (int m = instr.Size - OpCodes.Ldfld.Size - 1; m >= 0; m--) {
                rawInstructions[k++] = bytes[m];
            }

        // Skip this instruction (do not replace it).
        } else {
            k += instr.Size;
        }

    }

    methodBuilder.CreateMethodBody(rawInstructions, rawInstructions.Length);

}


private static byte[] toByteArray(int intValue) {
    byte[] intBytes = BitConverter.GetBytes(intValue);
    if (BitConverter.IsLittleEndian)
        Array.Reverse(intBytes);
    return intBytes;
}



private static byte[] toByteArray(short shortValue) {
    byte[] intBytes = BitConverter.GetBytes(shortValue);
    if (BitConverter.IsLittleEndian)
        Array.Reverse(intBytes);
    return intBytes;
}

(我知道它不漂亮。抱歉。我很快把它放在一起看看它是否有用。)

我不抱太大希望,但谁能提出比这更好的建议?

很抱歉这篇文章太长了,谢谢。


更新 #1: 啊……我刚刚读到这个in the msdn documentation

[CreateMethodBody 方法] 是 目前不完全支持。这 用户无法提供位置 令牌修复和异常处理程序。

在尝试任何事情之前,我真的应该阅读文档。总有一天我会学习...

这意味着选项 #3 不支持 try-catch 语句,这对我来说毫无用处。我真的必须使用可怕的#2吗? :/ 帮助! :P


更新 #2: 我已经成功实现了尝试 #2,支持异常。这很丑陋,但它有效。当我稍微改进代码时,我会在这里发布。这不是优先事项,因此可能需要几周后。如果有人对此感兴趣,请通知您。

感谢您的建议。

【问题讨论】:

  • 您说您已经通过第二种方法解决了问题。你能在这里发布你的解决方案吗(例如链接到源代码)。提前致谢!
  • 是的,将不胜感激,您是否真的设法用新方法替换旧方法?还是您只是创建了具有一些不同行为的新动态方法并将其包装到代理类中?

标签: c# reflection reflection.emit cil mono.cecil


【解决方案1】:

您尝试过 PostSharp 吗?我认为它已经通过On Field Access Aspect 提供了您所需的一切。

【讨论】:

  • 是的,但它不支持我需要的那种运行时编织。不过感谢您的建议。
  • 我明白了。在这种情况下,我认为我会考虑使用 #3,这对我来说似乎要求最小且错误概率最小。
  • 是的,我也最喜欢#3,但我正试图弄清楚如何解决异常问题,但我没有任何运气。在我看来,好像我必须在ILGenerator 上打开和关闭调用BeginExceptionBlockBeginCatchBlock 等的try-catch 语句,但在#3 中我并没有真正使用ILGenerator 来发出指令...我不知道我该怎么做。
  • 好的,我刚刚发现#3 不能支持异常的确认。请参阅更新 #1
  • 很遗憾听到这不起作用。祝你好运,我想你会需要它......
【解决方案2】:

也许我理解错了,但如果你想扩展,拦截一个类的现有实例,你可以看看Castle Dynamic Proxy

【讨论】:

  • 我正在查看它,但目前我还没有找到任何有关分析方法主体和替换特定指令的信息。另外,我真的不想拦截课程。如果用户愿意,我希望用户明确请求类扩展。连接类不是问题,我只是想在新方法中替换那些特定的说明。
【解决方案3】:

您必须首先将基类中的属性定义为虚拟或抽象。 此外,需要将字段修改为“受保护”而不是“私有”。

或者我在这里误解了什么?

【讨论】:

  • 对不起,你是;)。基类中没有任何属性,有私有字段。此外,不需要保护字段而不是私有字段。这些都与我的问题无关:如何逐条指令重写方法。无论如何,我已经解决了(见更新#2)。
【解决方案4】:

基本上,您是在复制原始类的程序文本,然后对其进行定期更改。您当前的方法是复制该类的 object 代码并对其进行修补。我能理解为什么这看起来很丑;你的工作水平非常低。

这似乎很容易通过源到源的程序转换来完成。 这在源代码的 AST 上运行,而不是在源代码本身上运行以获得精度。有关此类工具,请参阅DMS Software Reengineering Toolkit。 DMS 具有完整的 C# 4.0 解析器。

【讨论】:

    【解决方案5】:

    我正在尝试做一件非常相似的事情。我已经尝试过你的 #1 方法,我同意,这会产生巨大的开销(虽然我还没有准确测量过)。

    有一个DynamicMethod 类——根据 MSDN——“定义并表示可以编译、执行和丢弃的动态方法。丢弃的方法可用于垃圾回收。”

    性能方面听起来不错。

    使用ILReader 库,我可以将普通的MethodInfo 转换为DynamicMethod。当您查看 ILReader 库的 DyanmicMethodHelper 类的 ConvertFrom 方法时,您会找到我们需要的代码:

    byte[] code = body.GetILAsByteArray();
    ILReader reader = new ILReader(method);
    ILInfoGetTokenVisitor visitor = new ILInfoGetTokenVisitor(ilInfo, code);
    reader.Accept(visitor);
    
    ilInfo.SetCode(code, body.MaxStackSize);
    

    理论上这让我们修改现有方法的代码并将其作为动态方法运行。

    我现在唯一的问题是 Mono.Cecil 不允许我们保存方法的字节码(至少我找不到方法)。当您下载 Mono.Cecil 源代码时,它有一个 CodeWriter 类来完成任务,但它不是公开的。

    我对这种方法的另一个问题是 MethodInfo -> DynamicMethod 转换仅适用于具有ILReader 的静态方法。但这可以解决。

    调用的性能取决于我使用的方法。调用短方法 10'000'000 次后得到以下结果:

    • Reflection.Invoke - 14 秒
    • DynamicMethod.Invoke - 26 秒
    • DynamicMethod 与代表 - 9 秒

    接下来我要尝试的是:

    1. 用 Cecil 加载原始方法
    2. 修改Cecil中的代码
    3. 从程序集中剥离未修改的代码
    4. 将程序集保存为 MemoryStream 而不是 File
    5. 使用反射加载新程序集(从内存中)
    6. 如果是一次性调用,则使用反射调用调用该方法
    7. 如果我想定期调用该方法,请生成 DynamicMethod 的委托并存储它们
    8. 尝试找出是否可以从内存中卸载不需要的程序集(释放 MemoryStream 和运行时程序集表示)

    听起来工作量很大,但可能行不通,我们会看到 :)

    希望对你有帮助,让我知道你的想法。

    【讨论】:

    • 真的很多工作。如果您成功存储了 DynamicMethod 的委托以定期调用它们,请告诉我您是如何做到的?
    【解决方案6】:

    使用 SetMethodBody 代替 CreateMethodBody 怎么样(这将是 #3 的变体)?这是 .NET 4.5 中引入的一种新方法,似乎支持异常和修复。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2017-03-31
      • 2016-01-26
      • 2021-11-21
      • 2023-03-24
      • 1970-01-01
      • 1970-01-01
      • 2021-06-13
      相关资源
      最近更新 更多