要理解C#程序在CLR上的真实执行逻辑,查看IL代码是一条绕不开的路。C#源代码经过编译器生成程序集,程序集中保存的是IL指令和元数据,而不是原始C#文本。通过ILDASM和dnSpy,可以直观看到编译器如何翻译语法糖、如何分配局部变量、如何调用方法。接下来分别说明两种工具的安装、使用场景和阅读方法。

一、先认识ILDASM:SDK自带的IL查看器
ILDASM是.NET SDK附带的一个命令行工具,全称是IL Disassembler。安装.NET SDK后,通常可以在类似C:\Program Files\dotnet\sdk\8.0.100\ildasm.exe这样的路径下找到它。直接在终端输入ildasm并回车,会打开一个图形界面窗口。这个工具不需要额外安装第三方包,适合快速确认程序集内部结构、查看类列表、方法签名以及原始IL指令。
使用ILDASM时,先通过File菜单打开一个dll或exe文件,左侧会以树形结构展示程序集清单、命名空间、类、字段和方法。选中某个方法后双击,会弹出新窗口显示该方法完整的IL代码。例如准备一个简单的C#类并编译成dll:
public class Calculator
{
public int Add(int a, int b)
{
int sum = a + b;
return sum;
}
}用ILDASM打开这个dll后,展开Calculator类,双击Add方法,可以看到如下IL片段:
.method public hidebysig instance int32 Add(int32 a, int32 b) cil managed
{
.maxstack 2
.locals init (int32 V_0)
IL_0000: ldarg.1
IL_0001: ldarg.2
IL_0002: add
IL_0003: stloc.0
IL_0004: ldloc.0
IL_0005: ret
}这里IL_0000和IL_0001分别把第一个参数和第二个参数压入计算栈,IL_0002执行加法,IL_0003把结果存入局部变量V_0,最后IL_0004重新加载该变量并返回。看到这些指令后,就能明白编译器连一个简单的加法也会拆成加载、运算、存储、返回四步。ILDASM的优势在于轻量、随SDK附带,但缺点是不具备反编译回C#的能力,阅读大量IL时效率不高。
二、dnSpy:反编译、调试与IL修改
dnSpy是一款开源的.NET程序集分析工具,可以从GitHub发布页下载压缩包,解压后直接运行dnSpy.exe。它支持加载exe、dll等多种程序集格式,左侧是类型树,中间可以显示反编译后的C#代码,也可以在右键菜单里切到IL视图。与ILDASM相比,dnSpy最明显的优势是能把IL还原成可读性较高的C#代码,同时还能附加调试、修改IL并重新保存程序集。
打开一个程序集后,展开命名空间找到目标类,点击方法名即可查看反编译结果。如果只想看IL,可以在方法体上右键选择Edit IL Instructions,或者切换到IL视图。例如下面这个带自动属性的类:
public class User
{
public string Name { get; set; }
}在dnSpy中查看IL,会看到编译器自动生成了get_Name和set_Name两个方法。这一点ILDASM同样能展示,但dnSpy还能进一步反编译出C#写法,帮助确认语法糖被翻译成了什么。分析第三方库时,如果某个方法行为异常,可以先用dnSpy反编译出源码级逻辑,再切到IL视图检查关键指令,甚至直接修改IL来验证猜想。需要注意的是,修改IL属于较危险的操作,建议先备份原程序集。
dnSpy的另一个实用功能是调试。可以在目标方法入口设置断点,然后启动调试并传入参数,观察局部变量和调用栈。这在无法获得原始项目源码、只能分析线上dll时非常有用。不过dnSpy的界面比ILDASM复杂,习惯轻量工具的人可能仍然倾向先用ILDASM做快速确认。两者结合使用,基本能覆盖大多数IL查看需求。
三、阅读IL指令:从常见指令到分析技巧
IL指令数量不少,但日常分析并不需要全部记住。按照用途分几类可以快速上手:加载参数用ldarg系列,局部变量用ldloc和stloc,常量用ldc,方法调用用call或callvirt,装箱用box,类型转换用castclass,返回用ret。掌握这些指令后,再回头看IL代码就不会觉得杂乱。
以装箱为例,源码中写下object obj = 123;这行代码,IL会清楚地显示出装箱动作:
IL_0000: ldc.i4.s 123 IL_0002: box int32 IL_0007: stloc.0
看到box指令就能确认这里发生了装箱,会产生堆分配,属于性能敏感点。如果在一个循环里反复出现box,就说明代码可能在积累GC压力。类似地,字符串拼接在IL中通常表现为对string.Concat的调用,而不是简单的加号。借助IL可以验证编译器是否真的对某些语法做了优化,也可以反向确认某些看似简单的代码其实隐藏了较多开销。
闭包和异步方法在IL中会更加复杂。编译器会生成带有特殊前缀的内部类,并把捕获变量提升为字段。如果直接在dnSpy中查看异步方法的反编译C#代码,通常会看到状态机结构,而切到IL视图则能看到MoveNext方法里大段的分支和状态跳转。这类IL虽然不直观,但通过定位ldarg.0、callvirt和关键字段,仍然可以梳理出执行路径。总体来说,把ILDASM当作轻薄查看器、把dnSpy当作深度分析工具,配合常见IL指令知识,分析程序集就会更有把握。