我正在尝试使用 Mono.Cecil 将一些检测代码编织到现有方法中 - 基本上只是所有序列点之前的日志。这很简单,我可以迭代所有序列点并插入
Ldstr
来加载日志文本和 Call
指令,例如 Console.WriteLine
。为了确保分支目标不会在此日志记录中丢失,我再次查看说明,并修补每个分支目标以确保它们现在指向这些前面的日志指令。从理论上讲,这听起来很简单,这是我最终得到的编织器代码:
using Mono.Cecil;
using Mono.Cecil.Cil;
using Mono.Cecil.Rocks;
internal sealed class InstrumentationWeaver
{
public static void WeaveInstrumentationCode(string assemblyLocation)
{
var readerParameters = new ReaderParameters { ReadSymbols = true };
using var assemblyDefinition = AssemblyDefinition.ReadAssembly(assemblyLocation, readerParameters);
var weaver = new InstrumentationWeaver(assemblyDefinition.MainModule);
weaver.WeaveModule();
assemblyDefinition.Write("WeaveOutput.dll");
}
private readonly ModuleDefinition weavedModule;
private InstrumentationWeaver(ModuleDefinition weavedModule)
{
this.weavedModule = weavedModule;
}
private void WeaveModule()
{
foreach (var type in this.weavedModule.GetTypes()) this.WeaveType(type);
}
private void WeaveType(TypeDefinition type)
{
foreach (var method in type.Methods) this.WeaveMethod(method);
}
private void WeaveMethod(MethodDefinition method)
{
if (method.IsAbstract || method.IsPInvokeImpl || method.IsRuntime || !method.HasBody) return;
var jumpTargetPatches = new Dictionary<int, Instruction>();
var ilProcessor = method.Body.GetILProcessor();
// Weave each instruction for instrumentation
var sequencePoints = method.DebugInformation.SequencePoints;
foreach (var sequencePoint in sequencePoints)
{
var instruction = this.WeaveInstruction(ilProcessor, sequencePoint);
if (instruction is not null) jumpTargetPatches.Add(sequencePoint.Offset, instruction);
}
// Patch jump targets
foreach (var instruction in ilProcessor.Body.Instructions)
{
this.PatchJumpTarget(instruction, jumpTargetPatches);
}
// Patch exception handlers
foreach (var exceptionHandler in method.Body.ExceptionHandlers)
{
this.PatchExceptionHandler(exceptionHandler, jumpTargetPatches);
}
method.Body.OptimizeMacros();
}
private Instruction? WeaveInstruction(ILProcessor ilProcessor, SequencePoint sequencePoint)
{
if (sequencePoint.IsHidden) return null;
// Get the instruction at the sequence point
var instruction = ilProcessor.Body.Instructions.FirstOrDefault(i => i.Offset == sequencePoint.Offset);
if (instruction is null) return null;
// Insert the instrumentation code before the instruction
return this.AddInstrumentationCode(ilProcessor, instruction, sequencePoint);
}
private Instruction AddInstrumentationCode(ILProcessor ilProcessor, Instruction before, SequencePoint sequencePoint)
{
// For now we just write a message to the console
var message = $"Sequence point at {sequencePoint.Document.Url}:{sequencePoint.StartLine}";
var writeLineMethod = this.weavedModule.ImportReference(typeof(System.Console).GetMethod("WriteLine", [typeof(string)]));
var instructions = new[]
{
Instruction.Create(OpCodes.Ldstr, message),
Instruction.Create(OpCodes.Call, writeLineMethod),
};
foreach (var instruction in instructions) ilProcessor.InsertBefore(before, instruction);
return instructions[0];
}
private void PatchExceptionHandler(ExceptionHandler exceptionHandler, IReadOnlyDictionary<int, Instruction> jumpTargetPatches)
{
this.PatchJumpTarget(exceptionHandler.TryStart, jumpTargetPatches);
this.PatchJumpTarget(exceptionHandler.TryEnd, jumpTargetPatches);
this.PatchJumpTarget(exceptionHandler.HandlerStart, jumpTargetPatches);
this.PatchJumpTarget(exceptionHandler.HandlerEnd, jumpTargetPatches);
this.PatchJumpTarget(exceptionHandler.FilterStart, jumpTargetPatches);
}
private void PatchJumpTarget(Instruction instruction, IReadOnlyDictionary<int, Instruction> jumpTargetPatches)
{
if (instruction.Operand is Instruction targetInstruction)
{
if (jumpTargetPatches.TryGetValue(targetInstruction.Offset, out var newTarget))
{
instruction.Operand = newTarget;
}
}
else if (instruction.Operand is Instruction[] targetInstructions)
{
for (var i = 0; i < targetInstructions.Length; i++)
{
if (jumpTargetPatches.TryGetValue(targetInstructions[i].Offset, out var newTarget))
{
targetInstructions[i] = newTarget;
}
}
}
}
}
它适用于循环、if-else 语句,但由于某种原因,当我在 for 循环中使用 if-else 时,跳转目标会完全混乱。我能找到的最小的例子就是编织这个:
public static class Class1
{
public static void TestMethod()
{
for (var i = 0; i < 10; i++)
{
if (i == 0)
{
Console.WriteLine(i);
}
else
{
}
}
}
}
虽然生成的 IL 中的某些分支目标似乎是正确的,但最后的跳转指向比最后一条指令更高的地址(图片来自 ILSpy):
由于循环和 if-else 各自起作用,我不认为它们本身特别向后跳转会扰乱逻辑,但我无法弄清楚它是什么。
事实证明,Cecil 不会处理更改短格式跳转,以防您注入太多代码。因此,首先您需要在注入前调用
method.Body.SimplifyMacros();
,并在完成后调用 method.Body.OptimizeMacros();
。奇怪的是,后一个代码已经存在,这是反复试验的结果。