在运行时和设计时调试 .NET Framework 和 MS Visual Studio 托管类






4.93/5 (27投票s)
2003年10月16日
9分钟阅读

267196

924
本文介绍了如何无缝地为 .NET 框架类以及任何其他托管程序集设置断点、单步执行、设置监视和检查局部变量。
引言
.NET 平台为开发人员提供了许多功能,但其中一个令人沮丧的问题是框架本身的“黑盒”性质。
虽然 Delphi 总是附带整个 VCL 的源代码,但微软选择不提供框架的源代码——当通常零散的文档不足时,这让试图进行高级“操作”的开发人员感到茫然。
本文试图展示如何创建框架程序集(以及 Visual Studio 本身使用的任何托管程序集)的调试版本,以及如何在 IL 代码中设置断点并从自己的代码单步执行到框架代码中。
背景
我的挫败感和探索始于我试图创建一个带有 3 个面板的用户控件。我希望最终用户能够将用户控件拖放到窗体上,然后使用 Visual Studio 设计器将控件添加到各个面板中。然而,Windows 窗体设计器不提供此功能,只能将控件放置在用户控件本身上,而不能放置在其子控件上。
我觉得,了解 Visual Studio 和框架在幕后的工作方式将有助于我理解如何编写提供此功能的自定义设计器。
Reflector .NET 是 Lutz Roeder 编写的一款出色的免费工具,它帮助我查看了相关程序集(Microsoft.VisualStudio.dll 和 System.Design.dll)的反编译源代码,并弄清楚了设计器是如何创建以及如何选择单个控件等。
然而,层次结构相当复杂,许多类的方法的精确调用顺序以及变量的内容仍然模糊不清。只有单步执行 Visual Studio 代码才能真正帮助我很好地理解其操作。
浏览网页并没有找到关于这个主题的文章,许多提交都声称无法单步执行框架类。
然而,仔细查阅 .NET Framework 开发人员指南和一点运气帮助我找到了一种实现目标的方法。
由于调试设计时行为更加困难且文档不完善,本演练演示了如何调试用户控件的自定义设计器。当然,调试运行时行为不需要 Visual Studio 的第二个实例,只需在 Visual Studio 2003 的主实例中设置源代码中的断点即可。
分步演练
-
要求 JIT 编译器不要剥离调试信息
这需要创建一个 INI 文件,其名称与要调试的进程相同——由于我们将在设计时模式下调试 Visual Studio,因此该文件的名称将是 devenv.ini,它将位于与 devenv.exe 相同的目录中,通常是 \Program Files\Microsoft Visual Studio .NET 2003\Common7\IDE。
INI 文件很简单,如下所示
[.NET Framework Debugging Control] GenerateTrackingInfo=1 AllowOptimize=0
可以从 .NET Framework 开发人员指南文章 使映像更容易调试 获取更多信息。
-
禁用 .NET 框架目录的系统文件保护
如果不这样做,任何修改过的程序集都会立即被 Windows 的系统文件保护机制替换为原始版本。
虽然有一些手动方法可以禁用系统文件保护(请参阅出色的 WinGuides 网站上的 禁用 Windows 文件保护),但 Collake Software 的共享软件 wfpAdmin 工具非常方便,可以从 Windows 文件保护中删除特定文件夹。最低要求是删除 <WINDIR>Microsoft.NET\Framework\v1.1.4322 目录及其子目录的文件保护。
Microsoft Visual Studio 程序集不受系统文件保护,不需要此措施。
-
创建程序集的调试版本
这涉及从零售 DLL -> ILDASM 到提取的资源和包含 MSIL 指令的 IL 文件 -> ILASM 到重新编译的 DLL,其中嵌入了调试信息并创建了附带的 PDB 文件。
当需要转换多个程序集时,一对批处理文件方便地自动化此过程。
第一个批处理文件简单地遍历指定的目录,并为每个与传入文件掩码匹配的文件调用执行实际处理的批处理文件。
REM DEBUGMAKEALL.BAT rem process each matching file for %%a in (%1\%2) do DebugMake.bat "%%a" :end
第二个批处理文件首先调用 ILDASM 来反编译程序集,然后调用 ILASM 来创建程序集的调试版本。IL 文件以程序集的名称保存,并带有 .il 后缀。
REM DEBUGMAKE.BAT rem delete any il file left over from a previous invocation, else output will be appended to it and compilation will fail del %1.il rem call ILDASM to create the il file ILDASM /OUT=%1.il /NOBAR /LINENUM /SOURCE %1 rem call ILASM to compile a debug version of the dll as well as a pdb file ILASM /DEBUG /DLL /QUIET /OUTPUT=%1 %1.IL
步骤如下
- 打开“Visual Studio .NET 2003 命令提示符”
- 更改到包含要反编译的程序集的目录
- 要创建 System.Design.dll 的调试版本,请输入
<Pathtothebatchfile>DebugMakeAll.bat . System.Design.dll
要创建所有 System.*.dll 的调试版本,请输入
<Pathtothebatchfile>DebugMakeAll.bat . System.*.dll
最终结果
往返的最终结果是包含 MSIL 的 <assemblyname>.il 文件,一个重新编译的程序集,其中设置了
DebuggableAttribute
,以及一个包含调试信息的 PDB 文件。还创建了一些 ico 和 BMP 文件,如果需要,可以删除它们。System.Design.dll 的 IL 文件的一小部分如下所示
.namespace System.Design { .class private auto ansi sealed beforefieldinit SRDescriptionAttribute extends [System]System.ComponentModel.DescriptionAttribute { .custom instance void [mscorlib]System.AttributeUsageAttribute::.ctor(valuetype [mscorlib]System.AttributeTargets) = ( 01 00 FF 3F 00 00 00 00 ) // ...?.... .field private bool replaced .method public hidebysig specialname rtspecialname instance void .ctor(string description) cil managed { // Code size 15 (0xf) .maxstack 8 IL_0000: ldarg.0 IL_0001: ldc.i4.0 IL_0002: stfld bool System.Design.SRDescriptionAttribute::replaced IL_0007: ldarg.0 IL_0008: ldarg.1 IL_0009: call instance void [System]System.ComponentModel.DescriptionAttribute::.ctor(string) IL_000e: ret } // end of method SRDescriptionAttribute::.ctor
为确保这些程序集未被任何进程加载,最好重新启动计算机,只打开命令提示符和可能的 Windows 资源管理器。
-
将框架程序集的调试版本安装到全局程序集缓存中
这仅适用于框架程序集。
框架附带的 GacUtil 很好地完成了这项工作,并且具有能够从命令行处理多个文件的优点,这与 Windows 资源管理器的 GUI 扩展不同。
这里,一对批处理文件再次完成任务。
REM GACInstallAll.bat rem process each matching file for %%a in (%1\%2) do GACInstall.bat "%%a" :end
REM GACInstall.bat rem call gacUtil, asking it to install the passed file gacutil /i %1
步骤如下
- 从同一个命令提示符,例如输入
<Pathtothebatchfile>GACInstallAll.bat . System.Design.dll
或
<Pathtothebatchfile>GACInstallAll.bat . System.*.dll
- 从同一个命令提示符,例如输入
-
启动将要调试的 Visual Studio 2003 实例(被调试器)
启动 Visual Studio 2003 并打开要调试的解决方案。源代码下载中包含一个示例解决方案 TestApp.sln。
-
初始化一个新的 Visual Studio 2003 实例,它将作为调试器
Visual Studio 2003 的第二个实例将作为调试器。
A. 创建新的空白解决方案
将其命名为 devenv.sln 并保存在 devenv.exe 所在的相同目录中。
B. 指定源文件和符号文件的路径
此选项通过 工具 -> 选项 访问。
C. 附加到被调试器
D. 打开包含要调试代码的源文件,并在将被 Visual Studio 调用的过程处设置断点。
示例文件中的示例项目 devenv.sln 可用于此。当然,源文件和符号文件的路径必须更改。在演练中,设计器的重写方法
Initialize
中设置了一个断点——当创建设计的控件实例时,Visual Studio 将调用此方法——无论是通过拖放到窗体上还是通过在设计视图中打开包含该控件的窗体。 -
切换到被调试器并启动要调试的操作
切换到 Visual Studio 2003 实例,并启动一个操作,该操作应导致在另一个实例中命中断点。
如果您正在使用 TestApp.sln,只需从解决方案资源管理器中打开 Form1.vb。
-
终于调试框架类
Visual Studio 一旦创建自定义控件,就会实例化自定义设计器并调用其
Initialize
方法。调试器实例将停止在设置的断点处执行,并自动切换到前台。如图所示,Visual Studio 已加载了许多框架以及 Visual Studio 程序集的符号。
调用堆栈还显示了来自框架和 Visual Studio 程序集的调用。黑色(而不是灰色)字体表示堆栈中更高的过程有可用的符号。如果堆栈显示“<非用户代码>”,请右键单击并选择“显示非用户代码”,以至少查看尚未创建调试版本的程序集的方法名称和参数。
双击来自
microsoft.visual.studio.dll!DesignerHost.Add
的调用方法,调试器将自动加载反编译的 IL 文件并显示返回行。尽管语言不熟悉,但调试器的所有功能也适用于此源文件,包括设置断点、单步进入和跳过等。此外,如图所示,局部变量显示了调用过程中的所有局部变量,任何变量也可以使用监视窗口进行检查。当然,语言是 IL,不如 VB 或 C# 容易理解。但在一些文章(例如 Bugslayer 中的 ILDASM 是你的新好朋友)的帮助下,仍然可以了解代码在做什么,局部变量窗口在理解发生的事情方面确实非常有用。
调试 System.dll 和 MSCorlib.dll
本节是为了回应一些用户关于是否也可以调试核心程序集,例如 System.dll 和 Mscorlib.dll 的查询。
准备 System.dll
只要关闭了系统文件检查,这相对简单。
下一个屏幕截图显示了给出的命令和结果(所有批处理文件都添加了 @echo off 以保持显示清晰)。
准备 Mscorlib.dll
这更复杂,因为 Windows 似乎在正常启动时加载此 DLL。准备它需要
1. 复制到另一个目录,反编译并重新编译
下一个屏幕截图显示了给出的命令和结果。
2. 在安全模式下重启,用调试版本替换 mscorlib.dll 并安装到 GAC
调试这些程序集
我们可以使用相同的示例应用程序。
Form1.vb 的 Sub main
已修改为实例化 CodeSnippetStatement
(位于 System.dll)和 StringBuilder
(位于 mscorlib.dll)。
以下屏幕显示了如何从用户代码单步执行到每个程序集中构造函数(在 MSIL 中称为 .ctor
)的 IL 代码。
关注点
我很好奇为什么微软选择不随 .NET Framework 提供代码——他们确实选择不混淆它(谢谢,微软),但考虑到许多反编译器(Salamander,RemoteSoft 的商业程序,似乎做得最好,并且可以反编译整个程序集)无论如何都做得很好,为什么不直接发布代码本身呢?
此外,单步执行 MSIL 代码非常慢(比单步执行自定义代码慢约 20 倍),而且 Visual Studio 2003 似乎存在内存泄漏,因为在我的 1 GB 内存机器上,经过 15 轮中断和恢复后,使用的虚拟内存上升到 800 MB。我想知道是否有办法解决这些问题?
注意
文章提到需要在与被调试进程相同的目录中创建与被调试进程同名的 INI 文件。针对 Scott 的疑问,进一步测试表明,如果使用重新编译以包含调试信息的程序集,则不需要此步骤。