that instrumentation is not easily possible because of JIT compilation of IL instructions.
Well it's actually the opposite. Tracing methods is quite easy - it would be difficult if we would like to trace fields usages. .NET assemblies contain rich type/method information that can be used to inspect/modify them. There's also a great library that does that plus more - Mono.Cecil.
With the help if this library we can achieve what you want (gist).
using System;
using System.IO;
using System.Linq;
using Mono.Cecil;
using Mono.Cecil.Cil;
class TraceIL
{
static void Main(string[] args)
{
if (args.Length != 1)
{
Console.WriteLine("TraceIL.exe <assembly>");
return;
}
string fileName = args[0];
ModuleDefinition module = ModuleDefinition.ReadModule(fileName);
MethodReference consoleWriteLine =
module.ImportReference(typeof(Console).GetMethod("WriteLine", new Type[] {typeof(object)}));
foreach (TypeDefinition type in module.Types)
{
foreach (var methodDefinition in type.Methods)
{
var ilBody = methodDefinition.Body;
var ilProcessor = ilBody.GetILProcessor();
var firstOp = methodDefinition.Body.Instructions.First();
var ldstrEntering = Instruction.Create(OpCodes.Ldstr, $"--Entering {methodDefinition.Name}");
ilProcessor.InsertBefore(firstOp, ldstrEntering);
var call = Instruction.Create(OpCodes.Call, consoleWriteLine);
ilProcessor.InsertBefore(firstOp, call);
var ldstrLeaving = Instruction.Create(OpCodes.Ldstr, $"--Leaving {methodDefinition.Name}");
var lastOp = methodDefinition.Body.Instructions.Last();
ilProcessor.InsertBefore(lastOp, ldstrLeaving);
ilProcessor.InsertBefore(lastOp, call);
}
}
module.Write(Path.GetFileNameWithoutExtension(fileName)+".modified"+Path.GetExtension(fileName));
}
}
Just to explain a little bit what's going on. For every method on every type we find in the assembly, we get the ILProcessor
that allows us to modify the code. Having that we just insert a string and a call to Console.WriteLine
just before the first existing opcode, and similar code just before the end of the method. At the very end we save the new binary under the same name with .modified
added.
The example run:
λ TestApp.exe
Inside Run
Inside Inside
MoreInside
OtherProgramRun
λ TraceIL.exe TestApp.exe
λ TestApp.modified.exe
--Entering Main
--Entering .ctor
--Leaving .ctor
--Entering Run
Inside Run
--Entering Inside
Inside Inside
--Entering MoreInside
MoreInside
--Entering .ctor
--Leaving .ctor
--Entering OtherProgramRun
OtherProgramRun
--Leaving OtherProgramRun
--Leaving MoreInside
--Leaving Inside
--Leaving Run
--Leaving Main
Of course form there you can do some crazy stuff, like adding timestamp and/or list passed arguments, etc. Since your sample is obfuscated it might not work form the start, but I guess this code is not that difficult to be modified as needed.