如果您查看原始 IL 代码,您会看到如下内容:
.method public hidebysig
instance void SomeMethod (
class Foo foo
) cil managed
{
// Method begins at RVA 0x2360
// Code size 20 (0x14)
.maxstack 2
.locals init (
[0] int32
)
IL_0000: nop
IL_0001: ldarg.0
IL_0002: ldarg.1
IL_0003: call instance void Driver::DoSomethingWithFoo(class Foo)
IL_0008: nop
IL_0009: ldarg.1
IL_000a: ldfld int32 Foo::ExamleValue
IL_000f: ldc.i4.s 12
IL_0011: mul
IL_0012: stloc.0
IL_0013: ret
} // end of method Driver::SomeMethod
基本上你需要做的是:
-
将参数类型Foo替换为BitwiseReader
-
找到正在加载第一个参数的指令(上面的IL_0002),即以前的foo,现在的reader
-
在上一步中找到的指令之后添加对ReadFoo() 的调用。
完成这些步骤后,您的 IL 将如下所示:
.method public hidebysig
instance void SomeMethod (
class BitwiseReader reader
) cil managed
{
// Method begins at RVA 0x2360
// Code size 25 (0x19)
.maxstack 2
.locals init (
[0] int32
)
IL_0000: nop
IL_0001: ldarg.0
IL_0002: ldarg.1
IL_0003: call instance class Foo BitwiseReader::ReadFoo()
IL_0008: call instance void Driver::DoSomethingWithFoo(class Foo)
IL_000d: nop
IL_000e: ldarg.1
IL_000f: ldfld int32 Foo::ExamleValue
IL_0014: ldc.i4.s 12
IL_0016: mul
IL_0017: stloc.0
IL_0018: ret
} // end of method Driver::SomeMethod
**** 警告****
下面的代码高度依赖于这样一个事实:SomeMethod() 采用单个 Foo 参数,并且它会执行一些期望此引用位于堆栈顶部的操作(在本例中,调用 DoSomethingWithFoo())
如果您更改 SomeMethod() 的实现,您很可能需要调整 Cecil 代码以更改其签名/实现。
还请注意,为简单起见,我在同一个程序集中定义了BitwiseReader;如果它在不同的程序集中声明,您可能需要更改找到该方法的代码(另一种方法是手动构造 MethodReference 实例)
using Mono.Cecil;
using Mono.Cecil.Cil;
using System.Linq;
class Driver
{
public static void Main(string[] args)
{
if (args.Length == 1 && args[0] == "run")
{
ProofThatItWorks();
return;
}
using var assembly = AssemblyDefinition.ReadAssembly(typeof(Foo).Assembly.Location);
var driver = assembly.MainModule.Types.Single(t => t.Name == "Driver");
var someMethod = driver.Methods.Single(m => m.Name == "SomeMethod");
var bitwiseReaderType = assembly.MainModule.Types.Single(t => t.Name == "BitwiseReader");
var paramType = someMethod.Parameters[0].ParameterType;
// 1.
someMethod.Parameters.RemoveAt(0); // Remove Foo parameter
someMethod.Parameters.Add(new ParameterDefinition("reader", ParameterAttributes.None, bitwiseReaderType)); // Add reader parameter
var ilProcessor = someMethod.Body.GetILProcessor();
// 2.
var loadOldFooParam = ilProcessor.Body.Instructions.FirstOrDefault(inst => inst.OpCode == OpCodes.Ldarg_1);
// 3.
var readFooMethod = bitwiseReaderType.Methods.Single(m => m.Name == "ReadFoo");
var callReadFooMethod = ilProcessor.Create(OpCodes.Call, readFooMethod);
ilProcessor.InsertAfter(loadOldFooParam, callReadFooMethod);
// Save the modified assembly alongside a .runtimeconfig.json file to be able to run it through 'dotnet'
var originalAssemblyPath = typeof(Driver).Assembly.Location;
var outputPath = Path.Combine(Path.GetDirectoryName(originalAssemblyPath), "driver_new.dll");
var originalRuntimeDependencies = Path.ChangeExtension(originalAssemblyPath, "runtimeconfig.json");
var newRuntimeDependencies = Path.ChangeExtension(outputPath, "runtimeconfig.json");
File.Copy(originalRuntimeDependencies, newRuntimeDependencies, true);
System.Console.WriteLine($"\nWritting modified assembly to {outputPath}");
Console.ForegroundColor = ConsoleColor.Magenta;
System.Console.WriteLine($"execute: 'dotnet {outputPath} run' to test.");
assembly.Name.Name = "driver_new";
assembly.Write(outputPath);
}
static void ProofThatItWorks()
{
// call through reflection because the method parameter does not mach
// during compilation...
var p = new Driver();
var m = p.GetType().GetMethod("SomeMethod");
System.Console.WriteLine($"Calling {m}");
m.Invoke(p, new [] { new BitwiseReader() });
}
public void SomeMethod(Foo foo)
{
DoSomethingWithFoo(foo);
int someInfo = foo.ExamleValue * 12;
// etc
}
void DoSomethingWithFoo(Foo foo) {}
}
public class Foo
{
public int ExamleValue;
}
public class BitwiseReader
{
public Foo ReadFoo()
{
System.Console.WriteLine("ReadFoo called...");
return new Foo();
}
}
最后,一些您可以使用的好工具在您尝试 Mono.Cecil / IL / C# 时可能会很有用:
- https://sharplab.io
-
https://cecilifier.me(免责声明,我是这篇文章的作者)