工欲善其事,必先利其器
--论语
在开发软件的时候都知道使用Visual Studio提高开发效率,而不是用命令行+NotePad(如果你是在学习那就另当别论了)。那么对于研究.Net一些底层东西,有哪些器呢?
我认为有三样
1、 ILDasm与MSIL
ILDasm,顾名思义,就是IL(MSIL,微软中间语言)的反编译器。各位看官应该都了解下面这张图
通过为不同的语言实现编译器,而这些编译器的目标代码都是MSIL(当然,编译器并不只生成MSIL,至于其余的东西,后面再做介绍)。
那么MSIL又是怎么样的一门语言呢?我在这里无意介绍MSIL的详细语法(关于是否应该学习IL的争论在博客园里已经讨论了两次了,在这里我不想再次引起纷争)。我也不推荐大家花费很多时间去学习MSIL的细节内容,除非你要实现一门在.NET平台运行的语言,如果你只想像我一样,了解一些语法糖或编译器在背后干的事儿,那你只需要阅读下面的几百字就可以了。
Start with ILDasm
开始菜单,程序->Visual Studio(不管哪个版本)->Visual Studio Tools->肯定有一个命令符的快捷方式,打开这个后在提示符后输入:ILDasm,就会启动ILDasm程序了。
上图是用ILDasm打开一个托管程序集后的图示,ILDasm会用不同的图标表示程序中不同的元素。用ILDasm我们不仅仅可以看到编译器生成的MSIL代码,还可以看到生成的元数据。双击一个节点可以弹出一个显示代码的窗口:
哦,你说不懂IL,但是你只要不深究上面这几行代码“所有”的意思,你应该能大致明白它是干啥的:定义一个类,该类继承自System.Object。不难吧。你会发现在这些代码中还穿插着很多前面带点的东西,比如这里的.class,这就是元数据(什么是元数据?哦,简单点说就是编译器除了IL代码外,还附赠一些更多信息,让CLR运行这些程序集时能对它了解的更多)。还有这里的public,auto,ansi,beforefieldinit,这些都是元数据,这些举足轻重的元数据,作用大的可以影响到CLR运行代码的方式。对于元数据的作用超出了本节的范围,在以后的章节中会涉及。
MSIL是基于栈的语言
MSIL的核心就是一个运算栈,提到栈,大家都知道FILO(First In,Last Out,先进后出)这个特性。在MSIL中,所有的方法、操作数的参数都来自于这个栈上,比如一个Add方法需要两个参数,那么栈顶的两个元素就会弹出。而Add运算完成后,会返回一个值,这个值又将被压到这个栈顶。调用这个Add方法的代码类似下面这样:
ldc.i4.5
ldc.i4.8
callvirt Yuyijq.StudyIL.Add(int,int)
哦?不懂那些命令是啥意思?没关系,猜猜就可以了。你只要知道MSIL是基于栈的语言,那差不多能猜出来ldc.i4.5是往栈顶push个5的。
在MSIL中只有类、方法、字段
不管高层的语言,比如C#、VB.NET提供多少绚丽多彩的程序元素,比如委托、事件、属性。但是在MSIL中只有类、方法、字段这三种程序元素。而这些“神奇”的元素最终都依靠语言各自的编译器生成这三个元素(当然还有一些元数据)。
MSIL难么?难,如果你要知道每条命令的意思是什么,真的很难。MSIL容易学么?容易,把上面的文字再仔细看看就OK了,对于日常分析足够了。
2、 Reflector
大名鼎鼎的Reflector不用多说了,是居家必备。
3、 Debuger(Visual Studio+SOS.dll)
上面两个工具虽然很重要,但是对于一些底层的东西就爱莫能助了,这个时候我们就需要Debuger了。在Windows里首推的调试器是WinDbg。WinDbg相当的强大,不仅仅可以进行User Mode的调试,还可以进行Knerl Mode的调试。但是功能强大,必定使用起来也很麻烦,直到现在我还不能熟练的使用WinDbg。幸运的是,Visual Studio也能提供Debug的功能。
要调试.Net的程序,我们还需要一个SOS.dll的扩展。全称是Son of Strike(不知道为啥要起这么一个奇怪的名字,Strike是微软内部使用的,而SOS.dll提供的是Strike的一个子集,但是现在SOS.dll的功能基本上与Strike提供的功能相当,所以这个名字也失去了它原来的意义 感谢装配脑袋的解释)。SOS可以帮助Visual Studio读取.Net的数据结构。在这一节里,我们就以实例的方式学习Visual Studio+SOS.dll的使用。
我们以一个非常简单的Console程序作为“解剖”的程序。在Visual Studio(我使用的是Visual Studio Team System 2008英文版,其他版本类似)创建一个Console类型的项目后,第一步在项目属性窗口的“调试(Debug)”选项卡里选中“允许非托管代码调试(Enable unmanaged code debuging)”
输入以下代码:
1: using System;
2: namespace Yuyijq.DotNet.Chapter2
3: {
4: class StudyDebuger
5: {
6: static void Main()
7: {
8: int[] intArr = new int[5];
9: intArr[0] = 3;
10: intArr[1] = 5;
11: }
12: }
13: }
在Main方法第二行设置断点,F5启动调试。命中断点后,我们在立即窗口(Immediate Window)(打开立即窗口:菜单栏->调试(Debug)->立即窗口(Immediate))里输入命令:
.load sos.dll
这个命令用于加载sos扩展,sos.dll位于.Net Framework安装的目录中,如果你没有设置环境变量,那就需要在这里输入sos.dll的完整路径。
加载sos扩展之后,就可以享用sos的诸多命令了,sos的所有命令都已感叹号“!”开头,在这里我不准备介绍sos的命令,你可以使用!help获得sos所有命令的列表,然后你还可以通过!help 命令名的方法时获得每个命令的详细介绍,介绍中不仅仅有使用方法,还有示例。不需要弄明白所有命令的使用方法,常用的就这么几个:
!U,!DumpStackObjects(dso),!DumpObject(do),!DumpHeap,!DumpMT,!DumpMD,!DumpStack,!ObjSize,
!DumpDomain,!Name2EE,!DumpClass,!Threads
使用这些命令,基本上就可以印证很多书上跟你说的事儿是不是真的,看看他是不是在“胡扯”,而且你这么一动手,自己亲自印证了以后跟看书获得的资讯完全不是一回事儿。
在后面的内容中,我会常常使用这些命令来探索一些细节内容。
除了在Visual Studio的立即窗口输入命令,Visual Studio还有一个内存窗口(Debug->Window->Memory),微软想得很周到,有四个内存窗口,你可以同时看好几块内存。
还是上面那块示例代码,我们来看看内存窗口的作用
.load sos.dll
extension C:\Windows\Microsoft.NET\Framework\v2.0.50727\sos.dll loaded
!dso
PDB symbol for mscorwks.dll not loaded
OS Thread Id: 0xa2c (2604)
ESP/REG Object Name
002df05c 01b928a4 System.Int32[]
通过上面的命令,我们发现intArr数组的首地址为01b928a4。打开内存窗口,在Address一栏输入:0x01b928a4(千万记住,这里的0x不能掉了),显示如下:
为了观察方便,我们把内存窗口的Columns设为8,这样就比较对齐了。知道Object Layout的童鞋(我以前的文章也有介绍),应该知道每个Object有两个附加字段:同步块索引和方法表指针,这个01b928a4应该指向的就是方法表指针的位置,那上面内存窗口显示的5c aa c9 6f那就应该是方法表指针了,这里我们不关注这个。但是看到后面居然有一个5,这个难道跟数组的大小有关系么?(猜测,只是猜测)。
F10继续执行,我们发现内存窗口将内存有变化的地方用红色标识出来了,真是太方便了(我想骂一句娘)。
(这个3不是数组第一个元素的值么)
F10再执行
(这个5不是数组元素的第二个值么)
聪明的你应该有这样的猜测:
数组的内存布局是这样的,紧跟在方法表指针后面的是数组的大小,然后是数组的元素的值(按顺序排列)。
唔,我不知道这是不是正确的,那好,我多试几次,发现我的猜测貌似是正确的。
你看,上面这种探索的过程,一定会让你难忘,比你看书,看作者在那里“胡侃”要记忆深刻得多。
除了内存查看窗口,Visual Studio还有寄存器查看窗口(Debug->Window->Registers),还有Threads,Modules等等,等着你去尝试。
后记
有了这三个工具,我相信对你的学习和帮助一定是如虎添翼。要详细的介绍这些工具,需要大量的篇幅,但是我觉得我只给一个引子,剩下的留给你自己去探索,岂不是更有意思。由于时间和水平有限,难免有不对之处,如有发现请一定要告诉我,谢谢。