<2024年4月>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

文章分类

导航

订阅

如何追踪进微软的源代码?

单步追踪是很常用的一种调试手段,跟随CPU的脚步不断追踪下去,追啊追,一直追到臭虫的老巢。但也经常会遇到阻碍,最常见的就是遇到系统函数或者库函数,不属于“自己”的代码,追不进去了,只能望洋兴叹。如何能让追不进去的情况也追的进呢?本文将教您如何追踪进入微软的.Net运行时模块的源代码。

 

微软也开源

提起开源,大家首先想到Linux,提起微软,很多人可能立刻想到不开源。但这不是绝对的,Linux系统中也有不开源的模块,而微软的源代码,也有一些是公开的。比如,访问下面这个网址,就可以看到很多微软源代码。

http://referencesource.microsoft.com/

在这个名为.Net参考源代码(Reference Source)的网站中,我们可以浏览100多个(目前是111个).Net运行时模块的源代码。为了便于检索,网站提供了多种搜索方式帮助我们快速定位要找的源代码,比如可以通过这样的链接搜索PresentationFramework.dll中包含Button单词的各种定义:

/#q=Button%20PresentationFramework

在阅读某个源文件时,对于其中的类型定义,点击可以”Go to Defintion”准到包含这个类型的源代码。对于局部变量或者参数,点击可以加量显示使用这个变量或者参数的地方。如果点击方法名,那么左侧的竖栏会显示出对这个方法的所有引用,如图1所示。

1 在线浏览.Net框架源代码

1右上角的三个图标用来切换左侧竖栏的三种视图,分别为:

n  Document Outline:类型摘要,属性、方法、事件等,类似Object  Browser中的显示。

n  Project Explorer:项目浏览,显示所属模块的文件列表,和Visual Studio中的类似。

n  Namespace Explorer:按命名空间浏览。

除了在线浏览外,也可以把公开的源文件全部下载到本地,得到一个50MBzip包,解压后可以使用Visual Studio打开其中的Solution文件,慢慢阅读,虽然是不可以编译的,但是本地搜索和浏览的速度都快很多。

 

跟踪阅读

虽然上述网站中公开的只是部分源代码,但其总量也是很巨大的。粗略统计一下,一共有77个项目,18552个文件。源代码行数呢?我先使用了一个名为CSharpLineCounter的小工具来统计,结果这个小工具数了一会便挂死了,试了几次都是没数完就挂死,或许这个小工具的作者从来没有使用这么大的项目测试过它。换了一个更专业一些的名为cloc的工具来数,终于得到了结果,如图2所示。

2 使用CLOC来统计.Net参考源代码的结果

从图2可以看出,总的代码代码行数是4539552,即453万多行,不包含空行和注释。据说,Windows NT的最初版本是6百万行源代码。虽然使用的语言不一样,一个主要是C,一个主要是C#,但无论如何, 450多万行源代码的复杂度已经不可等闲视之了。

450多万行源代码,这样的代码量,还适合逐行阅读么?大多数人可能都没有这个时间和毅力了。

如何理解这样的百万行级别的源代码呢?我的经验是调试它——在调试器中通过断点、栈回溯、单步跟踪等技术对活的代码做立体透视。

 

调试符号

前面我们提到过,下载的.Net参考源代码是不可以编译的。对于这样不能编译的源代码,如何才能跟踪调试它呢?答案是必须有对应版本的可执行文件,以及用于将可执行文件和源代码关联起来的调试符号文件。

       图
3 符号文件的纽带作用

3画出了调试所需的三种文件以及它们之间的相互关系。符号文件是将可执行文件中的信息与与文件信息联系起来的桥梁,它在调试活动中的重要性是不言而喻的。以眼下的调试.Net参考源代码为例,可执行文件可以通过下载.Net框架运行时获得,源文件也可以通过我们上面提到的网站获得。那么符号文件如何获取呢?这是调试成败的关键,也是本文讨论的关键问题。

符号文件是在编译过程中产生的,我们无法编译.Net参考源代码,而且使用的是从微软下载的可执行文件(.Net运行时)。这就意味着我们只能下载微软编译时产生的PDB。微软向我们提供PDB么?提供,但是不是所有版本。以下说明特别值得大家注意。

对于.Net运行时模块的符号文件,微软的两套符号服务器中都有它们的符号,一套是http://referencesource.microsoft.com/symbols,另一套是http://msdl.microsoft.com/download/symbols。前者提供的符号文件范围主要就是.Net运行时模块,而且版本很少(下文详述)。而后者提供的符号文件范围很广,包扩Windows系统和其它微软的产品,而且涵盖几乎所有版本。从提供符号文件的类型来看,前者提供的符号文件信息更多,是包含源代码行信息的私有符号,而后者提供的则是剥离私有信息后的公共符号文件。

因为我们希望可以单步跟踪.Net运行时的源代码,这要求我们必须得到包含源代码行信息的私有PDB文件,这样的PDB文件只能从上面说的参考源代码服务器下载,不能从公共符号服务器下载。但是,参考源代码服务器目前只提供4.5.1版本运行时的调试符号。这就要求我们调试时,必须使用4.5.1版本的.Net运行时。

 

确认4.5.1版本

如何确认自己的机器上是否已经安装好4.5.1版本的.Net运行时呢?这个看似非常简单的问题对很多人来说可能并不简单。笔者在这里就浪费了不少时间。我先是到磁盘上存放.Net运行时的目录中(图4)看了一下,没有看到包含v4.5的目录,于是就到微软的网站下载4.5.1运行时的安装包来安装。但是安装之后,再到这个目录观察,发现没有什么变化,还是没有包含v4.5的目录。

4 磁盘上的.Net运行时

我最初以为没有安装成功,于是删除再安装,但结果还是如此。在网络上搜索一番才发现,原来.Net 4.5.1使用的是所谓的“原位更新”(in-place update),就是在4.0的目录中更新本来的文件,没有新的目录。更让人的困惑的是,就连每个文件的版本号也没有更新。以图5中的.Net运行时核心模块clr.dll为例,其文件属性中的版本号仍然是老的4.0.30319

5 4.5.1CLR模块仍使用4.0的版本号

坦率地说,我很厌恶.Net的某些特征,如此混乱的版本号完全违背了软件的“易识别性”原则。

那么,有没有办法判断4.5.1呢?比较蹩脚的方法还是有的,一种是通过注册表,在注册表编辑器中找到图6所示的表键,观察Version键值,如果是图中所示的4.5.51209,那么就是已经安装好4.5.1了。

6 通过注册表识别4.5.1

第二种方法是在使用Visual Studio调试时,通过Debug > Windows > Modules 打开模块面板,然后观察其中的Version列,将其拉宽,看版本号后面的附加说明,如果像图7中那样包含有built by: FX451RTMGREL,那么当前使用的.Net运行时就是4.5.1版本。

7 通过模块列表识别4.5.1

 

设置Visual Studio

有了前面的基础后,接下来的工作就很简单了。建议大家可以创建一个.Net程序(比如WPF Application),然后通过菜单Tools -> Options -> Debugging调出图8所示的调试选项界面。先是选择Symbols子项,在其中新增一个符号文件位置,并输入本地缓存路径(C:\SymCache)。

8 设置符号文件位置

然后点击图8左侧的General子项,在其中做如下设置:

n  Disable just my code

n  Disable step over properties and operators

n  Disable require source files to exactly match the original version

n  Enable .NET framework source stepping

n  Enable source server support

 

诗意

做了如此多的准备,到时间享受一下收获的喜悦了。可以先在自己的源代码里设置一个断点,比如设置在某个按钮的处理函数上,然后点击这个按钮,断点命中后,选择Debug > Windows > Call Stack调出栈回溯窗口(图9右上方),然后用鼠标点击某个.Net运行时模块(比如PresentationCore)对应的栈帧,这时,你可能看到一个文件下载对话框,提示正在下载符号文件。喝一口茶或者静坐片刻,下载完毕后,如果顺利的话,眼前的景象会让你很兴奋,微软的.Net运行时源代码活生生的呈现在你面前(图9)。在栈回溯窗口或者源代码窗口设置断点,然后点击工具栏上的Continue让程序恢复执行,等断点命中后,你就可以单步追踪.Net运行时的源代码了。

9 调试.Net运行时代码

交互式调试的可以透视活生生的代码,比枯燥地阅读源代码不知高效多少倍,用上海话来说,真是诗意(适意)。

有了跟踪源代码这个便利设施,很多问题就可以迎刃而解了。比如,有同行询问为什么下面这段代码在执行到对Filer属性赋值时会出异常。

BindingListCollectionView view;

//…

view.Filter = delegate(object data)

其实只要单步跟踪一下上面的赋值语句,进入到.Net运行时的源文件CollectionView.cs中,就会看到里面代码写的清清楚楚,如果CanFilterfalse,确实是会抛异常的。

     set

     {

       if (!CanFilter)

     throw new NotSupportedException();

继续单步跟中上面的if语句,就会看到CanFilter属性是硬代码(hard code)成返回false的,如图10所示。

10 单步跟踪解疑惑

 

特别提示

看了上面的介绍之后,如果你也想试一试,那么这里还有一个特别的提示。前面提到微软有两套符号服务器可以下载符号。它们都包含.Net运行时的符号,但是文件的类型不同,一个是私有符号,一个是公共符号。图11给出了两个截图,它们是同一个运行模块(相同版本)的两个PDB文件,上面是公共PDB,文件比较小,下面一个是私有PDB,文件大的多,包含的信息更丰富。

11 公共PDB和私有PDB比较

 

特别值得提醒大家的是,如果你的机器上曾设置过环境变量来使用微软的公共符号服务器(使用WinDBG的同行可能这么做),那么VS调试器是可能从公共符号服务器下载公共符号文件的,而意外下载公共PDB可能导致源文件调试失败。笔者正是在这里被卡了两周时间。拨打了微软的技术支持电话,被自动语言系统折磨了好几次,却没有得到任何有用的信息。防止重蹈覆辙的方法就是删除环境变量,而且将本地缓存(也称为下游符号库)中残留的公共PDB删除干净,稳妥的方法是换一个下游符号库路径。

 

Java运行时调试

说到这里,有些读者可能很自然地想到了JavaOracle公司是否也支持调试Java运行时的源代码呢?答案是肯定的,做法略有不同。简单说,可以下载调试版本的Java运行时安装包,里面包含了对应的PDB文件(图12)。然后让Java程序使用这个版本的运行时,便可以调试Java运行时模块的源代码了。

12 调试版本的Java运行时

 

比较两种支持源代码调试的方法,笔者认为.Net的更好一些。第一个原因是,不需要更换运行时。Java的做法需要更换调试版本,更换不仅麻烦,而且可能导致要调试的故障无法复现。第二个原因是,在这样的网络时代,让用户从服务器动态下载所需的符号文件和源文件更方便,更具有灵活性。.Net参考源代码网站目前只支持单一的版本是个不足,但是这个不足改进起来很容易,只要将其它版本的文件也公开到服务器上就可以了。

posted on 2015年2月11日 20:16 由 Raymond

Powered by Community Server Powered by CnForums.Net