• 工欲善其事


    工欲善其事,必先利其器

                      --论语

    (本文是我正在写的开放电子书《.Net面面观》第二章第一小节,您可以这里和这里查看第一章第二章第二小节

    在开发软件的时候都知道使用Visual Studio提高开发效率,而不是用命令行+NotePad(如果你是在学习那就另当别论了)。那么对于研究.Net一些底层东西,有哪些器呢?

    我认为有三样

    1、 ILDasm与MSIL

    ILDasm,顾名思义,就是IL(MSIL,微软中间语言)的反编译器。各位看官应该都了解下面这张图

    clip_image002

    通过为不同的语言实现编译器,而这些编译器的目标代码都是MSIL(当然,编译器并不只生成MSIL,至于其余的东西,后面再做介绍)。

    那么MSIL又是怎么样的一门语言呢?我在这里无意介绍MSIL的详细语法(关于是否应该学习IL的争论在博客园里已经讨论了两次了,在这里我不想再次引起纷争)。我也不推荐大家花费很多时间去学习MSIL的细节内容,除非你要实现一门在.NET平台运行的语言,如果你只想像我一样,了解一些语法糖或编译器在背后干的事儿,那你只需要阅读下面的几百字就可以了。

    Start with ILDasm

    开始菜单,程序->Visual Studio(不管哪个版本)->Visual Studio Tools->肯定有一个命令符的快捷方式,打开这个后在提示符后输入:ILDasm,就会启动ILDasm程序了。

    clip_image004

    上图是用ILDasm打开一个托管程序集后的图示,ILDasm会用不同的图标表示程序中不同的元素。用ILDasm我们不仅仅可以看到编译器生成的MSIL代码,还可以看到生成的元数据。双击一个节点可以弹出一个显示代码的窗口:

    clip_image006

    哦,你说不懂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不能掉了),显示如下:

    clip_image002[8]

    为了观察方便,我们把内存窗口的Columns设为8,这样就比较对齐了。知道Object Layout的童鞋(我以前的文章也有介绍),应该知道每个Object有两个附加字段:同步块索引和方法表指针,这个01b928a4应该指向的就是方法表指针的位置,那上面内存窗口显示的5c aa c9 6f那就应该是方法表指针了,这里我们不关注这个。但是看到后面居然有一个5,这个难道跟数组的大小有关系么?(猜测,只是猜测)。

    F10继续执行,我们发现内存窗口将内存有变化的地方用红色标识出来了,真是太方便了(我想骂一句娘)。

    clip_image004[8]

    (这个3不是数组第一个元素的值么)

    F10再执行

    clip_image006[6]

    (这个5不是数组元素的第二个值么)

    聪明的你应该有这样的猜测:

    数组的内存布局是这样的,紧跟在方法表指针后面的是数组的大小,然后是数组的元素的值(按顺序排列)。

    唔,我不知道这是不是正确的,那好,我多试几次,发现我的猜测貌似是正确的。

    你看,上面这种探索的过程,一定会让你难忘,比你看书,看作者在那里“胡侃”要记忆深刻得多。

    除了内存查看窗口,Visual Studio还有寄存器查看窗口(Debug->Window->Registers),还有Threads,Modules等等,等着你去尝试。

    后记

    有了这三个工具,我相信对你的学习和帮助一定是如虎添翼。要详细的介绍这些工具,需要大量的篇幅,但是我觉得我只给一个引子,剩下的留给你自己去探索,岂不是更有意思。由于时间和水平有限,难免有不对之处,如有发现请一定要告诉我,谢谢。

  • 相关阅读:
    GIL 全局解释器
    线程
    队列 Queue 与 生产者消费模型
    进程 与 并发(进程打开方式,互斥锁)
    【JAVA设计模式】单例模式
    【排序】桶排序
    【排序】选择排序
    【排序】插入排序
    【排序】冒泡排序
    JVM 优化之逃逸分析
  • 原文地址:https://www.cnblogs.com/yuyijq/p/1583709.html
Copyright © 2020-2023  润新知