gpt4 book ai didi

c# - 替换方法的 MethodBody 中的指令

转载 作者:太空狗 更新时间:2023-10-29 19:48:09 27 4
gpt4 key购买 nike

(首先,这是一篇很长的文章,但别担心:我已经实现了所有这些,我只是在询问您的意见或可能的替代方案。)

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

  • 我得到一个 Type作为参数。
  • 我使用反射定义了一个子类。请注意,我不打算修改原始类型,而是创建一个新类型。
  • 我为原始类的每个字段创建一个属性,如下所示:
    public class OriginalClass {
    private int x;
    }


    public class Subclass : OriginalClass {
    private int x;

    public int X {
    get { return x; }
    set { x = value; }
    }

    }
  • 对于父类(super class)的每个方法,我在子类中创建了一个类似的方法。方法的主体必须相同,只是我替换了说明 ldfld xcallvirt 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 语句的方法,因为我没有使用方法 BeginExceptionBlock 发出它们。 , BeginCatchBlockILGenerator .这变得越来越复杂。我想我可以做到: 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 :

    [The CreateMethodBody method] is currently not fully supported. The user cannot supply the location of token fix ups and exception handlers.



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

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

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

    感谢您的建议。

    最佳答案

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

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

    性能方面听起来不错。

    ILReader库我可以转换正常 MethodInfoDynamicMethod .当您查看 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 次后,我得到以下结果:
  • 反射.调用 - 14 秒
  • DynamicMethod.Invoke - 26 秒
  • 带有委托(delegate)的 DynamicMethod - 9 秒

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

  • 听起来工作量很大,但可能行不通,我们拭目以待:)

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

    关于c# - 替换方法的 MethodBody 中的指令,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/2803583/

    27 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com