使用 c# 将秒表注入所有使用 Mono.Cecil 的 dll 方法,包括具有多个 Return 语句的方法

Inject stopwatch using c# into all dll methods using Mono.Cecil, including methods with multiple Return statements

Case 1 Case 2 Case 3 Case 4

Objective: 使用注入器代码,我试图在目标 dll 的所需代码位置注入秒表方法(在秒表 dll 中),以便计算目标 dll 中每个方法所花费的时间,这可能是也可能不是无效的方法,它可能有多个 return 语句。

目标 dll

public class targetDll
{

void func1(){
     //Inject Stopwatch_start(); method here
        int a = 3;
        int b = 4;
        int temp;
        temp = a;
        a = b;
        b =temp;
        if (a + b > 2)
        {
            Console.WriteLine("function____1");
        }
      #Stopwatch_stop()  //Inject stop time here
    }

String func2(){
      //Inject Stopwatch_start(); method here
        int a = 3;
        int b = 4;
        int c = 5;
        int temp;
        temp = a;
        a = b;
        b = c;
        c = temp;
        if (a + b > 5)
        {
            Console.WriteLine("function____2");
        //inject Stopwatch_stop() method here
          return ;
        }
        a = temp;
      //inject Stopwatch_stop(); method here
          return;
  }
}

源dll(秒表dll)

 public  static class stopwatch_class
{
  static System.Diagnostics.Stopwatch stopwatch_obj = new System.Diagnostics.Stopwatch();

    public static void stopwatch_start()
    {
        stopwatch_obj.Start();
    }

    public static  void stopwatch_stop()
    {            
        stopwatch_obj.Stop();
        Console.WriteLine(stopwatch_obj.ElapsedMilliseconds);            
     }        
    }
 }

喷油器代码

 class Trial_injector
{
    static void Main(string[] args)
    {
        var start_method = (dynamic)null;
        var stop_method = (dynamic)null;

        AssemblyDefinition target_assembly = AssemblyDefinition.ReadAssembly("targetDll.dll", 
        new ReaderParameters { ReadWrite = true });

        var target_modules = target_assembly.MainModule;
        TypeDefinition[] target_module = target_modules.Types.ToArray();

        AssemblyDefinition source_assembly = AssemblyDefinition.ReadAssembly("stopwatch.dll", new 
        ReaderParameters { ReadWrite = true });

        var source_modules = source_assembly.MainModule;
        TypeDefinition[] source_module = source_modules.Types.ToArray();


        foreach (var type in source_module)
        {
            foreach (var method in type.Methods)
            {
                if (method.Name == "stopwatch_start")
                {
                    start_method = method;
                }

                if (method.Name == "stopwatch_stop")
                {
                    stop_method = method;
                }
            }
        }

        foreach(var module_ in target_module)
        {
            foreach(var method_ in module_.Methods)
            {
               String stg="hello_world";
                var processor2 = method_.Body.GetILProcessor();
                var first_instruction = method_.Body.Instructions.First();
                var last_instruction = method_.Body.Instructions.Last();
                var ldstr = processor2.Create(OpCodes.Ldstr, stg);

                var call = processor2.Create(OpCodes.Call, method_.Module.Import(start_method));
                var call2 = processor2.Create(OpCodes.Call, method_.Module.Import(stop_method));
                processor2.InsertBefore(first_instruction, ldstr);
                processor2.InsertAfter(first_instruction, call);

                processor2.InsertBefore(last_instruction, ldstr);
                processor2.InsertBefore(last_instruction, call2);
            }
        }
        target_assembly.Write();
    }

您的代码几乎是正确的。需要做的修改很少。

不确定为什么需要 ldstr 操作码,因为它在任何地方都不需要。对于您想要插入 before 的调用,而不是在第一个操作码之后。至于最后一条指令,您可以使用 InsertBefore。所以最终的代码可能是这样的:

foreach (var module_ in target_module)
{
    foreach (var method_ in module_.Methods)
    {
        var processor2 = method_.Body.GetILProcessor();
        var first_instruction = method_.Body.Instructions.First();
        var last_instruction = method_.Body.Instructions.Last();
        var call = processor2.Create(OpCodes.Call, method_.Module.Import(start_method));
        var call2 = processor2.Create(OpCodes.Call, method_.Module.Import(stop_method));
        processor2.InsertBefore(first_instruction, call);

        processor2.InsertBefore(last_instruction, call2);
    }
}

但这不适用于某些早期的 return。为什么?早期的 return 被编码为 brbr_s 操作码到 ret 在过程结束时,如果我们在 ret 之前注入我们的 call returns 将跳过它。在您的示例中,不需要它,因为此代码已转换为 if-else 并且我们在两种情况下都正确匹配了分支。但是图像我们有这样的代码:

int a = 3;
if (a == 3)
{
  return; // very early return here
}
// the rest as in original one

我们不会看到为此方法打印的经过时间,因为 return 将在我们注入的调用之后指导执行。我们在这里需要做的是更新所有负责早期 returns 的分支指令(因此它们跳转到 ret 操作码)并将它们指向我们的调用。我们可以通过以下方式做到这一点:

foreach (var bodyInstruction in method_.Body.Instructions)
{
    if (bodyInstruction.OpCode != OpCodes.Br && bodyInstruction.OpCode != OpCodes.Br_S) continue;
    if (((Instruction)bodyInstruction.Operand).OpCode != OpCodes.Ret) continue;

    bodyInstruction.Operand = call2;
}

所以我们在这里做的是扫描所有操作码,看看是否有 brbr_s 跳转到 return 我们更新它以跳转到我们的调用反而。中提琴

注意:使用 Elapsed 而不是 ElapsedMilliseconds,因为前者给出全零。

完整代码:

var start_method = (dynamic) null;
var stop_method = (dynamic) null;

AssemblyDefinition target_assembly = AssemblyDefinition.ReadAssembly("target.exe", new ReaderParameters {ReadWrite = true});

var target_modules = target_assembly.MainModule;
TypeDefinition[] target_module = target_modules.Types.ToArray();


AssemblyDefinition source_assembly = AssemblyDefinition.ReadAssembly("stopwatch.dll", new ReaderParameters {ReadWrite = true});

var source_modules = source_assembly.MainModule;
TypeDefinition[] source_module = source_modules.Types.ToArray();

foreach (var type in source_module)
{
  foreach (var method in type.Methods)
  {
    if (method.Name == "stopwatch_start")
    {
      start_method = method;
    }

    if (method.Name == "stopwatch_stop")
    {
      stop_method = method;
    }
  }
}

foreach (var module_ in target_module)
{
  foreach (var method_ in module_.Methods)
  {
    var processor2 = method_.Body.GetILProcessor();
    var first_instruction = method_.Body.Instructions.First();
    var last_instruction = method_.Body.Instructions.Last();
    var call = processor2.Create(OpCodes.Call, method_.Module.Import(start_method));
    var call2 = processor2.Create(OpCodes.Call, method_.Module.Import(stop_method));
    processor2.InsertBefore(first_instruction, call);

    processor2.InsertBefore(last_instruction, call2);

    foreach (var bodyInstruction in method_.Body.Instructions)
    {
      if (bodyInstruction.OpCode != OpCodes.Br && bodyInstruction.OpCode != OpCodes.Br_S) continue;
      if (((Instruction)bodyInstruction.Operand).OpCode != OpCodes.Ret) continue;

      bodyInstruction.Operand = call2;
    }
  }
}
target_assembly.Write();

上的自我推销

我碰巧用 Mono.Cecil 录制了两个关于这样做的视频(以有点不同的方式)。你可以找到它 Writing simple .NET execution tracer with Mono.Cecil and Instrumenting .NET assemblies to measure method's execution time in with Mono.Cecil

关闭自我推销