摘要
本文介绍了使用 WinDbg + SOS扩展 进行非源代码级调试的一些基本尝试;IIS维护的站点配置被部分损坏时可能出现的一种情况及解决办法;以及解决该故障的思路。
问题
问题的由来是这样的:
考虑目前的需要,想尝试一下VersionOne。第一次安装的是它的一个早期版本,安装中包括两部分:数据库和IIS中的Web应用程序站点。后来我在IIS管理器中删除别的一些网站时,手抖了一下,结果把VersionOne的站点也给删除了。因为安装已经被破坏了,所以我打算连根拔了。但是运行卸载程序的时候弹出一个错误,错误的消息是很典型的.NET下的IndexOutOfRangeException异常:
既然没法通过卸载程序卸载,我决定手工删除相关的文件和注册表。找到了VersionOne的Web应用程序站点根目录——整个儿删除;又找到注册表里的一些条目——也删除了。自以为大功告成,于是,我开始运行VersionOne新版本的安装程序。但就在刚刚同意完用户协议之后,点了下“Next”,它就又弹出了包含IndexOutOfRangeException异常的提示信息——和之前的那个一模一样,然后程序就结束了。很是郁闷。
发信问VersionOne的客服,答非所问(也许是我在邮件里写得不够明确)。又看了点关于非源码级的托管程序调试的书,手里有点痒。于是决定自立更生,找出引发这异常的真正原因,这才有了下文。
调试器的配置
首先,VersionOne的安装包是Windows Installer打包过的,可以用WinRAR直接解压出里面的内容:
通过Reflector可以验证Setup.exe、V1Utility.dll和VersionOne.Setup.dll里都是托管代码,而且可以把反编译后的代码如数导出,组建成一个解决方案。用Visual Studio 2008(简称VS)将这个解决方案打开以后,更新对应的引用,就可以在各个符号之间跳转,查找相互间的关系了。等等,不是说“非源代码级的调试”吗?其实,虽然这里可以获得源代码,但是有很多字段以及少数的类和方法是由编译器产生的,这些符号的名称虽然在IL中是合法的,但C#不认,使得我编译的时候通不过,也就没法直接在源码上调试了。
本来还有一种想法是将VS附加到安装程序进程上进行调试,然后捕获进程中抛出异常。这时VS会尝试寻找源代码;而我呢,则可以在这个时候把反编译出来的源代码提供给VS。但是,VersionOne这个安装程序会捕获异常然后用MessageBox.Show呈现出来,VS就没有机会捕捉这个异常。有一种可能性是通过设置Debug菜单下的Exceptions里的内容来启用第一次异常捕获,但是我尝试了一下,似乎没有起作用(我钩选了整个CLR异常类别)。
根据“葡萄”(熊力)的《Windows用户态程序高效排错》一书的指导,我还需要两个东西:WinDbg和CLR 2.0的SOS.dll。前者去Windows硬件开发者中心找;后者可以在.NET 2.0运行时的目录下找到。我将SOS.dll复制到WinDbg安装目录下的(自己新建的)clr20目录了。然后启动WinDbg。
看信息是因为缺少mscorwks.dll而导致的,但是我把这个文件复制到与SOS.dll相同的目录下后,再走一遍步骤:1、2、3,结果仍然没变。我真的疑惑了:这个文件到底该放哪里?
后来我尝试用VS附加到进程上调试了一次,然后中途突发奇想,又让WinDbg也附加上去,然后试了一下“!clrstack”——嘿,成了!多试了几次后终于总结出来:只要等目标进程跑起来就可以了——实际上就是要等待目标进程加载完所需的模块。那个所谓的“无法加载模块”,应该是指目标进程还没有加载模块,和你的WinDbg、SOS的配置根本没有关系。
开始调试
配置完成后,就开始正式的调试工作了。本次的目标也很简单,就是在异常抛出的时候让程序暂停,好让我看清异常抛出时的堆栈。
假定是通过附加到进程的方式进行调试的,而且SOS扩展也成功加载了,这时你需要暂停应用程序:
输入命令“sxe clr”开启检测CLR的First Chance Exception的功能;默认情况下,WinDbg会忽略First Chance Exception,直到Second Chance Exception时才捕获。在这两次异常抛出的机会之间,就是用户程序插手处理异常的地方。而按照我们现在的设置,只要应用程序抛出了First Chance Exception,WinDbg就会让所有线程暂停,然后我们可以随意地检查程序状态。通过“sx”命令可以检查目前WinDbg对各种异常的处理方式,如果刚刚的设置对了,会有如下的纪录:
输入命令“g”,程序又继续运行。切换回应用程序的界面,我尝试重现异常。成功!WinDbg检测到了异常:
这时所有线程都已被暂停运行。输入命令“!Threads”查看当前的托管线程状态:
0号是主线程,2号似乎是终结器的线程。
输入命令“~0s”以切换到0号线程,然后输入“!clrstack”查看CLR上的堆栈:
根据命名空间,最初出现异常的地方竟然是Framework Class Library(简称FCL)!?而且倒数两层调用都是在FCL里,看来有可能是FCL的问题,与VersionOne无关。
但VersionOne的代码可以帮我定位问题。我找到IIS.GetWebSites方法里的代码,浏览了一下变量和类名称,似乎是一个通过目录服务取得当前IIS主机上所有Web站点列表的功能。将相关代码(两个静态函数,然后自己简单实现了WebSite类)复制到一个新的项目中,编译,再把得到的程序扔到服务器上。运行程序,果然触发了异常。
这里插一下,上述的调试工作以及其它杂七杂八都是在家里的主机上完成的。家里的那台机器主要用来提供一些简单的Web功能和Subversion服务,其它不必要的软件都没装(包括VS的C#组件),所以没法直接使用一个C#的VS项目开始调试。
本来是打算通过VS里的Remote Debugger功能开启一个调试服务器,我再从笔记本这里通过互联网连接到调试服务器,然后附加到目标进程进行调试。尝试了一下后,发现似乎没法跨越Internet进行调试工作,逼着我给家里的机器装上了VS。
解决问题
后面的就略述了。待VS安装完后,打开调试FCL源代码的功能,然后开始调试样例程序。程序在CollectionBase.cs(FCL的源代码)里停下来了,但是这时的VS无法显示局部变量等信息,说是“二进制文件已经被优化,不匹配源代码”。郁闷了,只好回去看在自己的代码上获得的信息。看到本地变量里有这么一个数据:site.Path = "IIS://localhost/W3SVC/196443281"。“196443281”应该是IIS中对Web Site的编号,但我通过IIS Manager检查本机的“Sites”并没有发现这个站点。于是开始怀疑IIS里有些信息没删干净,就跑到IIS.net上的论坛里去问。那里的回答说是IIS不操作注册表,也基本不操作文件,不应该出现这种情况。
整理下思路: DirectoryEntry 可以通过URI找到一些和IIS站点配置相关的信息;但是这一次,站点“196443281”里的配置信息不全或者损坏了,所以 DirectoryEntry 只取得了部分信息。而这个“196443281”,很可能就是我之前无意删除的站点。在网上搜索了“DirectoryEntry IIS”,找到了一篇文章:
使用System.DirectoryServices.DirectoryEntry来实现iis虚拟目录的管理(http://www.cnblogs.com/libiyang/archive/2005/12/10/294455.html)
文章里说到,可以通过这个类删除IIS中的站点。那我就可以把之前的代码稍微改动一下,当找到这个站点时就把它直接删除。这样VersionOne那边再枚举本机的所有站点时也不会得到错误的数据了。实践,成功。
后记
我认为VersionOne的安装程序应该能够处理这个异常,例如忽略损坏的站点信息,然后继续安装。所以我把这个问题反映给了与我联系的VersionOne客服。对于IIS那边,我还不能确定到底是不是IIS的问题,还在IIS.net的坛子上问。但因为我没有直接碰过目录服务,那若现在目录服务里提供的信息损坏了,则应该是IIS的问题,至于具体细节还在求证中。