替换方法的MethodBody中的指令

时间:2010-05-10 14:49:05

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

(首先,这是一个非常冗长的帖子,但不要担心:我已经实现了所有这些,我只是问你的意见,或者可能的替代方案。)

我在实施以下方面遇到了麻烦;我很感激一些帮助:

  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访问器。

  5. 我遇到了第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语句的方法,因为我没有使用{{1}的方法BeginExceptionBlockBeginCatchBlock等发出它们。 }}。这变得复杂了。我想我可以这样做:ILGenerator有一个MethodBody列表,其中应包含执行此操作的必要信息。但我无论如何都不喜欢这个解决方案,因此我将此作为最后的解决方案保存。

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

    叹息。

    以下是尝试#3的实施,如果有人感兴趣的话:

    MethodBuilder.CreateMethodBody(byte[], int)

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

    我没有太多希望,但有人能提出比这更好的建议吗?

    对于非常冗长的帖子感到抱歉,谢谢。


    更新#1: Aggh ...我刚读过这个in the msdn documentation

      

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

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

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


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

    感谢您的建议。

6 个答案:

答案 0 :(得分:1)

我正在尝试做一个非常相似的事情。我已经尝试了你的#1方法,我同意,这会产生巨大的开销(尽管我还没有完全测量过)。

根据MSDN,有一个DynamicMethod类 - “定义并表示可以编译,执行和丢弃的动态方法。废弃收集可以使用丢弃的方法。”

表现明智,听起来不错。

使用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 - &gt; DynamicMethod转换仅适用于ILReader的静态方法。但这可以解决。

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

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

接下来我要尝试的是:

  1. 使用Cecil加载原始方法
  2. 修改Cecil中的代码
  3. 从程序集中删除未修改的代码
  4. 将程序集保存为MemoryStream而不是File
  5. 使用Reflection
  6. 加载新程序集(从内存中)
  7. 如果是一次性调用
  8. ,则使用反射调用调用该方法
  9. 生成DynamicMethod的委托并存储它们,如果我想定期调用该方法
  10. 尝试找出是否可以从内存中卸载不必要的程序集(释放MemoryStream和运行时程序集表示)
  11. 这听起来像很多工作而且可能不起作用,我们会看到:)

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

答案 1 :(得分:0)

你试过PostSharp吗?我认为它已经通过On Field Access Aspect开箱即用。

答案 2 :(得分:0)

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

答案 3 :(得分:0)

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

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

答案 4 :(得分:0)

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

这似乎很容易用源到源程序转换。 这对源代码的AST而不是源代码本身进行精度操作。有关此类工具,请参阅DMS Software Reengineering Toolkit。 DMS有一个完整的C#4.0解析器。

答案 5 :(得分:0)

如何使用SetMethodBody而不是CreateMethodBody(这将是#3的变体)?这是.NET 4.5中引入的一种新方法,似乎支持异常和修正。