如何解析IL字节中的方法调用?查找调用指定方法的程序集方法
解决方案
一、用现成库快速实现(推荐)
不用自己手动解析IL,直接用ILSpy背后的ICSharpCode.Decompiler库,它已经封装了IL解析逻辑,能轻松检测方法调用。
步骤:
- 安装NuGet包
ICSharpCode.Decompiler - 遍历程序集中的所有方法,对每个有方法体的方法,用库分析其IL中的调用指令:
示例代码:
using ICSharpCode.Decompiler.CSharp; using ICSharpCode.Decompiler.IL; using System.Reflection; public static IEnumerable<MethodInfo> FindCallers(MethodInfo targetMethod, Assembly assembly) { var decompiler = new CSharpDecompiler(assembly.Location, new DecompilerSettings()); foreach (var type in assembly.GetTypes()) { foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static)) { if (!method.HasMethodBody) continue; try { var ilMethod = decompiler.DecompileMethod(method); // 查找所有调用指令,包括call、callvirt、newobj(构造函数调用) var calls = ilMethod.Descendants.OfType<Call>() .Concat(ilMethod.Descendants.OfType<NewObj>()); foreach (var call in calls) { var calledMethod = call.Target.MetadataToken.Resolve(method.Module) as MethodInfo; if (calledMethod == targetMethod) { yield return method; break; // 找到一个调用就停止检查当前方法 } } } catch { /* 跳过无法反编译的方法 */ } } } }
这个方法能直接定位所有调用目标方法的函数,包括构造函数的newobj调用。
二、手动解析IL的实现思路
如果一定要自己写解析逻辑,核心是识别IL中的调用指令并解析元数据令牌:
关键IL调用指令
call(操作码0x28):调用静态方法、非虚实例方法callvirt(操作码0x6F):调用虚方法、接口方法newobj(操作码0x73):调用构造函数(创建实例时)
这些指令的结构都是:操作码 + 4字节元数据令牌,令牌指向被调用方法的元数据信息。
算法大纲
- 遍历目标程序集中的所有类型,再遍历每个类型的所有方法(注意包含非公开、静态方法)
- 对每个方法,跳过无方法体的(抽象、接口方法等)
- 获取方法体的IL字节数组:
method.GetMethodBody()?.GetILAsByteArray() - 遍历IL字节数组,逐个解析指令:
- 读取当前字节作为操作码,判断是否是
call/callvirt/newobj - 如果是,读取后续4字节(小端序)作为元数据令牌
- 用
module.ResolveMethod(token)将令牌转为MethodInfo - 对比该
MethodInfo是否等于目标方法,是则记录当前方法为调用者
- 读取当前字节作为操作码,判断是否是
- 继续遍历直到IL数组结束
示例代码片段(核心解析部分):
public static IEnumerable<MethodInfo> FindCallersManual(MethodInfo targetMethod, Assembly assembly) { var targetToken = targetMethod.MetadataToken; var targetModule = targetMethod.Module; foreach (var type in assembly.GetTypes()) { foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static)) { var body = method.GetMethodBody(); if (body == null) continue; var ilBytes = body.GetILAsByteArray(); int offset = 0; while (offset < ilBytes.Length) { byte opcode = ilBytes[offset]; offset++; // 检查是否是调用类指令 if (opcode == 0x28 || opcode == 0x6F || opcode == 0x73) { // 读取4字节元数据令牌(小端) int token = BitConverter.ToInt32(ilBytes, offset); offset += 4; try { var calledMethod = targetModule.ResolveMethod(token); if (calledMethod.MetadataToken == targetToken && calledMethod.Module == targetModule) { yield return method; break; } } catch { /* 跳过无效令牌 */ } } else { // 跳过非调用指令,根据操作码长度移动偏移 offset += GetOperandSize(opcode); } } } } } // 辅助函数:获取操作码的操作数长度(简化版) private static int GetOperandSize(byte opcode) { // 大部分单字节操作码无操作数或操作数长度固定,这里只处理常见情况 switch (opcode) { case 0x00: case 0x01: case 0x02: case 0x03: case 0x04: case 0x05: case 0x06: case 0x07: case 0x08: case 0x09: case 0x0A: case 0x0B: case 0x0C: case 0x0D: case 0x0E: case 0x0F: case 0x10: case 0x11: case 0x12: case 0x13: case 0x14: case 0x15: case 0x16: case 0x17: case 0x18: case 0x19: case 0x1A: case 0x1B: case 0x1C: case 0x1D: case 0x1E: case 0x1F: return 0; // 无操作数 case 0x20: case 0x21: case 0x22: case 0x23: case 0x24: case 0x25: case 0x26: case 0x27: case 0x29: case 0x2A: case 0x2B: case 0x2C: case 0x2D: case 0x2E: case 0x2F: case 0x30: return 4; // 4字节操作数 default: return 1; } }
三、入门资料推荐
- 《CLR via C#》:书中有专门章节讲解IL指令和方法体结构,比CLI文档更通俗易懂,适合快速上手。
- CLI Partition III:CIL Instruction Set:虽然CLI文档厚,但只需要看第三部分的IL指令集章节,重点关注
call、callvirt、newobj这几个指令的格式,不用通读全文。 - 微软官方IL指令参考:直接搜索“IL指令集”即可找到官方文档,每个指令的格式和用途都有明确说明。
内容的提问来源于stack exchange,提问作者ChrisW
相关产品推荐
相关产品推荐

