.NET应用程序调试—原理、工具、方法

jopen 10年前

阅读目录:

  • 1.背景介绍
  • 2.基本原理(Windows调试工具箱、.NET调试扩展SOS.DLL、SOSEX.DLL)
    • 2.1.Windows调试工具箱
    • 2.2..NET调试扩展包,SOS.DLL、SOSEX.DLL
    • 2.3.调试系统的基本流程及架构(.NETDAC概念、mscordacwks.dll)
    • 2.4.VisualStudio中集成扩展调试(更加细粒度的调试程序)
    </li>
  • 3.调试程序类型(客户端程序、服务端程序)
  • 4.调试方式及场景
    • 4.1.本机调试(Attach Process,调试器启动)
    • 4.2.不中断调试或者称事后调试(对Dump文件进行调试)
    • </ul> </li>
    • 5.一般调试步骤
      • 5.1.设置符号文件(公有符号、私有符号)
      • 5.2.加载.NET程序扩展调试包(SOS.DLL、SOSEX.DLL)
      • 5.3.调试的三种命令类型(标准命令、元命令、扩展命令)
      • </ul> </li>
      • 6.调试扩展的几个比较常用的命令(SOS.DLL、SOSEX.DLL)
      • 7.简单示例,常见的线上两类问题
        • 7.1.内存问题(内存偏高,内存溢出)
        • 7.2.线程问题(CPU过高,线程死锁)
        • </ul> </li>
        • 8.获取Dump文件时的重要注意事项
        • 9.总结
        • </ul>

          1.背景介绍

          随着应用程序的复杂度不断上升,要想将好的设计思想稳定的落实到线上,我们需要具备解决问题的能力。需要具备对运行时的错误进行定位且快速的解决它的能力。本篇文章我将分享一下我对.NET应用程序调试方面的学习和使用总结。

          其实对调试程序的使用是不难的,关键是知道它的调试原理才行,因为调试一个程序或者dump文件,都需要了解一定 的.NET调试的原理才行,比如你在附加到进程调试时在执行某个SOS扩展命令是需要切换到指定线程上的,而调试dump文件就不需要,但是对Dump文 件的分析有些SOS扩展命令是不能用的,类似这样的问题,一旦出现你就一头雾水,所以花点时间学习一下原理是有必要的。

          2.基本原理(Windows调试工具箱、.NET调试扩展SOS.DLL、SOSEX.DLL)

          在Windows平台上调试应用程序首选Windows调试工具箱,该工具箱包含了一套专门用来针对Windows进行很多复杂场景调试所需要的工 具和组件。需要注意的是此工具箱是针对于非托管.NET平台用的,意思就是说此工具箱的所有工具和组件默认是不能够进行.NET应用程序调试的,只能用来 对原生Windows程序进行调试。

          那么.NET平台也并不是有自己一套专用的调试工具箱,毕竟.NET还是属于Windows平台的,所以很大部分的运行时原理还是基于 Windows的,要想在原生的调试器中对.NET这个具有虚拟运行时程序进行调试就需要专门的翻译器才能够执行。SOS.DLL、SOSEX.DLL这 两个就是用来对.NET程序在Windows调试工具中起到翻译作用的调试器扩展。简单讲就是,这两个组件是.NET项目组专门开发出来用来对.NET应 用程序进行方便调试用的,当然不用这两个扩展也能调试.NET程序,只不过就会很困难,会被很多细节束缚住。有了这个调试扩展之后,我们就可以让原生 Windows调试器正确的翻译出.NET相关概念。

          图1:(Windows调试工具执行流程)

          .NET应用程序调试—原理、工具、方法

          所有对.NET程序发起的调试会话都要经过.NET调试扩展组件进行翻译才行,也就是要使用.NET调试扩展的调试命令来调试.NET程序。上图 中,我们如果要想调试.NET程序就需要将.NET调试扩展组件加载到Windows调试工具中去,然后才能方便在Windows调试工具中使用。

          2.1.Windows调试工具箱

          Windows调试工具箱中包含了很多调试工具,都是用来辅助于我们进行方便调试用的。Windows调试工具箱分为两个执行版本,X86、X64 这两个版本是专门用来分析不同的运行时环境的,如果你的分析环境是32位的你就需要使用X86的版本,同理,如果是用64位的环境就需要使用X64的版 本。

          下载地址为:http://www.microsoft.com/whdc/devtools/debugging/default.aspx

          记住选择你需要的版本,建议你两个版本都下载,因为你随时需要针对Dump文件进行分析,而Dump文件是随时都有可能是两个版本。

          Windows工具箱中的默认使用WinDbg.exe作为调试首选,它是一个GUI程序。

          图2:(默认的Windows调试工具,WinDbg)

          .NET应用程序调试—原理、工具、方法

          安装过后的菜单中就只有WinDbg作为调试选择。

          这里需要注意的是,当你启动了WinDbg之后要留意程序的名字和标题,因为当你存在两个版本的WinDbg时会容易搞错,在调试时会有各种奇怪的问题出现,当你找了半天之后结果发现是因为用错了版本,那就正的无语了。

          图3:(注意运行WinDbg的环境版本)

          .NET应用程序调试—原理、工具、方法

          WinDbg是默认的调试工具,但是在工具箱中还有几个控制台调试工具,他们行必之下比较轻量简单,有些任务比较好执行,在配合cmd使用会很方便,比如工具箱中的tlist.exe用来查看进程信息的小工具就非常方便。

          图4:(方便查看进程ID)

          .NET应用程序调试—原理、工具、方法

          这样我们就可以很方便的attach到一个指定的进程进行调试。

          Windows调试工具箱中有很多其他的工具,需要用的话可以使用cmd切换到当前安装的目录下:C:\Program Files\Debugging Tools for Windows (x86),或者你直接到工具的安装目录运行也行,这就看此工具是不是支持手动无参数启动了。

          2.2..NET调试扩展包,SOS.DLL、SOSEX.DLL

          .NET调试扩展包分为两个,一个是SOS.DLL,该扩展包是.NET平台的一部分,属于官方版本。而SOSEX.DLL是微软的一名叫 “Steve Johnson”软件工程师开发,属于个人维护的,用来增强SOS.DLL功能的,在SOSEX.DLL有很多功能比较强大的扩展命令。

          下载地址为:

          32位:http://www.stevestechspot.com/downloads/sosex_32.zip

          64位:http://www.stevestechspot.com/downloads/sosex_64.zip

          具体的帮助文档可以查看该工程师的博客来了解详情。这两个版本用来调试不同环境的程序的,如果你的程序是运行在32位环境下,就用32位的SOSEX,同理,用在64位下就用64位SOSEX。

          而SOS.DLL扩展包是跟着.NETFramework一起安装的,地址位于:C:\Windows\Microsoft.NET\Framework\v4.0.30319。如果你是64位系统的话地址就是:

          C:\Windows\Microsoft.NET\Framework64\v4.0.30319。在这两个地址下面都可以找到SOS.dll文件,不同的目录下对应于调试不同机器类型的.NET程序。

          有了这两个扩展包之后就可以在WinDbg中对.NET程序进行分析了,具体使用我们后面会介绍。

          2.3.调试系统的基本流程及架构(.NETDAC概念、mscordacwks.dll)

          有一个很重要的原理我觉得很有必要讲一下,就是.NETDAC概念。

          其实.NETDAC也就是.NET Data Access .NET数据访问层,这个是专门用来提供给SOS.DLL\SOSEXDLL或者其他调试扩展包使用的,所有的调试扩展组件必须通过这个DAC才能访问 到.NET运行时的数据,所以在初次使用SOS的时候会经常碰见加载错误的mscordacwks.dll文件,此文件就是DAC的物理文件。

          这个文件和SOS扩展文件一样,都有这不同的版本,当加载不同类型的.NET程序时会使用到不同版本的mscordacwks.dll文件,当然大部分情况下此文件时自动加载的,只有出现你分析的文件与生成调试文件的环境不一致时才会出现头疼的问题。

          图5:(mscordacwks.dll位置)

          .NET应用程序调试—原理、工具、方法

          当你知道这个组件是工作于此位置时,当出现跟它相关的错误提示时你就不需要担心了,无非就是文件加载的位置或者版本不匹配而已。

          调试器会话、调试器注入线程

          还有一点我觉得也很有必要介绍的就是有关调试器如何调试.NET程序的,当我们在使用调试器启动被调试程序或者将调试器附加到被调试进程时,其实调 试器会注入一些线程到.NET程序中,让调试线程与.NET程序原本的线程在一个.NET执行环境中,这样的目的是能够起到最.NET程序在执行时的控 制,比如中断执行,设置断点。当我们需要执行某些跟线程上下文相关的扩展命令时就需要切换到正确的线程上去。

          图6:(调试器注入线程)

          .NET应用程序调试—原理、工具、方法

          此时,调试器使用一个注入线程将.NET程序在执行时中断,原理就是通过发送线程中断命令来达到控制目标线程,那么首先要能够与原线程通讯才行,所 以需要注入托管线程。(注意:注入的线程不一定就是托管.NET线程,严重它最好的方法就是查看所有所有的进程内线程和所有托管线程,对比一下就知道 了。),其实这个ID为3的线程是调试器会话线程。

          图7:(切换到原托管线程)

          .NET应用程序调试—原理、工具、方法

          我们通过~0s命令切换到我们需要调试的原托管线程中,比如,在执行!ClrStack命令时,就需要切换到当前线程上执行。

          我们需要验证它是否是注入了托管线程还是非托管线程。

          图8:(托管线程列表)

          .NET应用程序调试—原理、工具、方法

          使用!Threads命令可以查看进程内所有的托管线程,仅仅是托管线程,此命令是无法查看非托管线程的,接下来我们使用另外一个命令来查看所有的线程。

          图9:(所有的执行时线程)

          .NET应用程序调试—原理、工具、方法

          这样我们就可以判断出,调试器使用了ID位7的作为目前的调试会话线程。知道这些背后的原理很重要,当你在执行某个调试命令时你就会发现此命令是否 需要在.NET线程中执行,还是说可以在调试器会话线程中执行,一般dump类的命令都是可以远程执行的,也就是说在调试器会话中执行,当需要跟 踪.NET线程内部过程时就需要切换到.NET线程上去执行。

          2.4.VisualStudio中集成扩展调试(更加细粒度的调试程序)

          SOS扩展也是可以和VisualStudio进行集成的,这样真的方便了我们调试一些性能要求比较高的程序,当程序运行一段时间后我们用VS附加 到进程,然后查看一些重要的对象数据,但是此时我们看不到.NET运行时的一些数据,比如:对象的代龄,托管堆的大小,线程池的任务等。通过集成SOS扩 展会让我们对程序的运行时有了一个更加方便的跟踪。

          图10:(打开本地代码调试)

          .NET应用程序调试—原理、工具、方法

          设置断点,然后在”即时窗口“(调试->窗口->即时)中加载扩展SOS.DLL。

          图11:(在VisualStudio2012中加载SOS.dll扩展)

          .NET应用程序调试—原理、工具、方法

          这样的便利性大大提高我们在调试程序内存方面、线程方面的好处,我们可以适当的做压力测试,然后Attach process,执行SOS扩展命名来查看内存问题,当需要调试程序逻辑时在单步调式C#代码,一举两得。

          3.调试程序类型(客户端程序、服务端程序)

          .NET程序主要分为两类,一类是客户端程序,另一类是服务端程序。对于这两类程序来说前者调试时基本上可以通过附加进程的方式进行调试,而对于服 务端程序则不行,因为服务程序通常是运行在一个复杂的线上环境中,我们没有任何权限或机会去接触,此时是通过获取进程的dump文件来进行分析。

          客户端程序也大概分为控制台、Winform两种,服务端程序都是基于ASP.NET框架,宿主与IIS进程中。

          4.调试方式及场景

          针对不同类型的程序及场景需要使用不同的方式进行调试,客户端程序中的控制台程序基本上可以通过在调试器中启动的方式进行调试。如果是GUI程序则 需要附加进程方式。服务端程序如果在条件允许下也是可以使用附加进程的方式进行调试的,但是这一般不太可能,因为一旦附加进程将block住所有的线程活 动。

          4.1.本机调试(Attach Process,调试器启动)

          本机调试可以直接在调试器中启动程序,WinDbg打开后,在文件中有一个Open Executable,可以打开一个可执行文件。如果是使用NTSD控制台调试器,则需要在NTSD后面跟上程序的执行路径。

          图12:(ntsd.exe打开调试程序)

          .NET应用程序调试—原理、工具、方法

          同样,在WinDbg中也有一个附加进程的选项,NTSD也是一样,操作起来都比较简单,需要注意的是当你对进程进行附加时要清楚此进程是多少位的,然后你需要选择正确的调试器进行调试。

          4.2.不中断调试或者称事后调试(对Dump文件进行调试)

          在不能够对被调试程序直接调试时我们就需要此程序的进程镜像文件,此镜像文件就是进程在某一个时刻的快照,通过分析这个快照,我们也是可以定位出问 题的。首先我们需要使用适当的工具来获取进程的dump文件,操作系统本身的任务管理器就有这个功能,dump文件的存放位置默认在用户信息临时文件下 面,比如:XXX\Users\Administrator\AppData\Local\Temp,获取完dump文件后任务管理器会有提示路径的。

          图13:(使用任务管理器获取dump文件)

          .NET应用程序调试—原理、工具、方法

          图14:

          .NET应用程序调试—原理、工具、方法

          使用任务管理器获取dump文件固然很方便,但是有一个问题就是如果当前机器是64位的,并且你的进程是以32位方式运行的,那么此时你获取出来的 dump文件是64位的,当你通过32位的调试器无法进行分析,甚至会有各种其他的问题,这些问题就是因为获取dump文件的机器环境和你预想的不一致。 这个时候我们希望能够通过很明了的方式来获取dump文件,就是通过调试器来获取dump文件。

          通过调试器来获取dump文件有很多好处,可以设置很多选项,包括只获取进程的哪部分镜像数据等。

          先通过tlist.exe查看所有进程列表,会有一个进程ID号,有了ID号才能进行获取。

          图15:(tlist、ntsd 进入到指定进程中)

          .NET应用程序调试—原理、工具、方法

          进入到ntsd调试器中,然后使用.dump/mf d:\order.dmp 命令获取dump文件到D盘。

          图16:(使用NTSD.exe获取dump文件)

          .NET应用程序调试—原理、工具、方法

          此时我们就成功的获取到了dump文件。

          通过调试器获取dump文件比较稳定可靠,因为机器运行环境的不同,通过任务管理器获取的dump文件会存在一些无法预知的问题,你并不清楚,当前任务管理器是使用哪个版本的环境输出调试信息的。

          有了dump文件之后就是通过调试工具打开就行了,WinDbg就有一个菜单专门打开dump文件的,Open Crash Dump。使用ntsd需要使用命令ntsd -z d:\order.dmp。

          5.一般调试步骤

          知道了调试的一些原理和工具之后我们来看一下调试的基本步骤,这些步骤都具体是指的什么意思,有哪些好处。

          5.1.设置符号文件(公有符号、私有符号)

          设置符号文件的目的是为了能够在调试器中正确的对应到源代码的位置和一些元数据信息。符号文件都是*.pdb文件名。符号文件分为公有和私有两种,公有的都是公司公开出去用于帮助调试用的,而私有的是公司内部使用的,为什么要区分公有和私有,是为了防止逆向工程。

          图17:(设置符号文件路径)

          .NET应用程序调试—原理、工具、方法

          首先通过.sympath d:,设置了符号路径为D盘,然后又使用.symfix+ d:,是设置私有符号路径,并且使用d盘为缓存路径。在最后一个红线中我们能看出来。

          为什么使用.symfix 时要带上一个+号,其实是告诉调试器我们是多加一个符号位置,而不是覆盖原有符号位置。

          设置好了两个符号位置后需要使用.reload命令来重新加载模块,这样调试器才会去符号位置去加载这些符号。

          图18:(加载的符号文件)

          .NET应用程序调试—原理、工具、方法

          调试器会自动的将公有符号下载到你刚才设置的缓存目录中。

          5.2.加载.NET程序扩展调试包(SOS.DLL、SOSEX.DLL)

          对.NET程序分析当然是需要加载SOS扩展了。加载SOS扩展有两个命令可以使用,第一个是.load C:\Windows\Microsoft.NET\Framework\v4.0.30319\SOS.dll,.load命令是要给出sos.dll 绝对路径的。第二个是.loadby sos modulename,.loadby 命令是可以根据已经加载的模块名称来加载SOS.dll扩展。使用第一个命令有一个问题就是,我们需要人工的判断当前环境到底是需要什么版本的SOS扩 展,而使用.loadby是可以根据已经加载的模块来自动的查找对应的SOS扩展。

          0:000> .load C:\Windows\Microsoft.NET\Framework\v4.0.30319\SOS.dll

          0:000> .loadby sos.dll clrjit

          使用.loadby 命令很容易的就可以加载SOS扩展,而不需要自己去判断当前程序是.NET什么版本的。

          5.3.调试的三种命令类型(标准命令、元命令、扩展命令)

          在使用调试器调试程序时,所要使用的命令主要分为三类。

          第一类是标准命令,就是不带任何符号开始的命令,比如:pb、lmvm。这一类命令是所有Windows调试工具箱中的调试工具通用的,不管你是使用ntsd还是winDbg都可以。

          第二类命令是元命令,就是使用”.”号开始的命令,这一类命令并不是在所有调试工具中通用的。第三类是扩展命令,扩展命令就是各个调试器扩展出来的命令,也就是以”!”开始的命令,如:!dumpheap -stat,!dumpstatcobjects。

          6.调试扩展的几个比较常用的命令(SOS.DLL、SOSEX.DLL)

          当然这个纯粹是我的个人感觉,排名不分先后。

          !dumpheap -stat (查看托管堆统计信息)

          0:000> !dumpheap -stat
          Statistics:
          MT    Count    TotalSize Class Name
          65366e78        1           12
          System.Collections.Generic.EnumEqualityComparer`1[[System.Web.Compilation.FolderLevelBuildProviderAppliesTo,
          System.Web]]
          653667cc        1           12
          System.Collections.Generic.ObjectEqualityComparer`1[[System.Web.WebSockets.IAsyncAbortableWebSocket,
          System.Web]]
          65365f08        1           12
          System.Lazy`1+Boxed[[System.Web.Security.Cryptography.AspNetCryptoServiceProvider,
          System.Web]]
          65365a34        1           12
          System.Web.Security.Cryptography.HomogenizingCryptoServiceWrapper
          65361e20
          1           12 System.Web.Configuration.CustomErrorsMode

          !dumpheap -type  (查看某个类型在堆中的信息)

          0:000> !dumpheap -type System.String

          Address       MT     Size
          10731228 624aacc0       14
          107312c4 624aacc0 22
          107312dc 624aacc0       78
          10731370
          624aacc0       28

          可以一眼看出哪些对象过大,这里我是为了演示而用,一般在项目开发中,我们都大概知道哪些对象可能会有内存问题,比如:同步数据时的缓存对象。

          !dumpobj 10731228 (查看对象详情)

          0:000> !dumpobj 10731228
          Name:
          System.String
          MethodTable: 624aacc0
          EEClass:     620b486c
          Size:
          14(0xe) bytes
          File:
          C:\Windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
          String:

          Fields:
          MT    Field   Offset                 Type VT     Attr
          Value Name
          624ac480  40000aa        4         System.Int32  1 instance
          0 m_stringLength
          624ab6b8  40000ab        8          System.Char  1 instance        0 m_firstChar
          624aacc0  40000ac        c
          System.String  0   shared   static Empty
          >> Domain:Value
          00dbe558:NotInit  00e11c90:NotInit  00e5f040:NotInit  <<

          !threads(查看托管线程)

          0:000> !threads
          ThreadCount:      17
          UnstartedThread:  0
          BackgroundThread: 12
          PendingThread:    0
          DeadThread:       5
          Hosted Runtime: no

          Lock
          ID OSID ThreadOBJ    State GC Mode     GC Alloc Context  Domain   Count Apt Exception
          7    1 43a8 00dc2620     28220 Preemptive  1484CA40:00000000 00dbe558 0     Ukn
          15    2 4414 00dd38d0     2b220 Preemptive  00000000:00000000 00dbe558 0     MTA (Finalizer)
          17    3 441c 00e09e88   102a220 Preemptive  00000000:00000000 00dbe558 0     MTA (Threadpool
          Worker)
          18    4 4420 00e0ce80     21220 Preemptive  00000000:00000000 00dbe558 0     Ukn

          当然还有很多其他很不错的命令,这里我个人觉得这几个比较常用,要想了解所有的命令可是在调试器中使用扩展命令!help来查看所有的命令帮助。

          0:000> !help

          ——————————————————————————-

          SOS is a debugger extension DLL designed to aid in the debugging of managed programs. Functions are listed by category, then roughly in order of importance. Shortcut names for popular functions are listed in parenthesis. Type “!help <functionname>” for detailed info on that function.

          Object Inspection                  Examining code and stacks

          —————————–      —————————–

          DumpObj (do)                       Threads

          DumpArray (da)                     ThreadState

          DumpStackObjects (dso)             IP2MD

          DumpHeap                           U

          DumpVC                       DumpStack

          GCRoot                             EEStack

          ObjSize                            CLRStack

          FinalizeQueue                      GCInfo

          PrintException (pe)                EHInfo

          TraverseHeap                       BPMD
          COMState

           

          Examining CLR data structures      Diagnostic Utilities

          —————————–      —————————–

          DumpDomain                         VerifyHeap EEHeap                             VerifyObj Name2EE                            FindRoots SyncBlk                            HeapStat DumpMT                             GCWhere DumpClass                          ListNearObj (lno) DumpMD                             GCHandles Token2EE                           GCHandleLeaks EEVersion                          FinalizeQueue (fq) DumpModule                         FindAppDomain ThreadPool                         SaveModule DumpAssembly                       ProcInfo
          DumpSigElem                        StopOnException (soe) DumpRuntimeTypes                   DumpLog DumpSig                            VMMap RCWCleanupList                     VMStat DumpIL                             MinidumpMode
          DumpRCW                            AnalyzeOOM (ao) DumpCCW

           

          Examining the GC history           Other

          —————————–      —————————–

          HistInit                           FAQ HistRoot HistObj HistObjFind HistClear

          7.简单示例,常见的线上两类问题

          这里我们使用两个小示例直观的感受一下接触.NET运行时状态的感受,尽管真实的问题可能比这个复杂很多,但是解决问题的思路是一样的。

          7.1.内存问题(内存偏高,内存溢出)

          服务程序最怕的性能问题之一就是内存,当内存很高的情况下我们能够通过对dump文件进行查看,看哪些对象导致内存一直高。当内存一直高的情况下就 会容易导致内存溢出异常,甚至是GC频繁的执行,当GC一执行就会导致服务并发下降,因为它要挂起所有的线程(这里指的是服务器模式的.NETCLR,相 对应的还有工作站模式的.NETCLR)。

          namespace OrderManager
          {
          class Program
          {
          static void Main(string[] args)
          {
          Console.WriteLine(“app begin…”);
          Console.ReadLine();

          List<byte[]> l = new List<byte[]>();

          for (int i = 0; i < 9999999; i++)
          {
          byte[] b = new byte[1000];

          l.Add(b);

          Console.WriteLine(i);
          }

          Console.WriteLine(“end begin…”);
          Console.ReadLine();
          }
          }
          }

          这一段代码会一直分配内存直到最后内存溢出异常终止程序,我们在内存比较的情况下来获取一个dump文件,然后通过适当的命令来定位哪个对象占用内存过高。

          在不知道对象类型的情况下比较简单的方式就是使用:0:000> !dumpheap -stat,命令,该命令的意思是统计当前堆的信息,在这里就可以一眼找到哪个对象占用多少内存。

          0:000> !dumpheap -stat
          Statistics:
          MT    Count    TotalSize Class
          Name
          624ad6a8        1           12 System.Collections.Generic.GenericEqualityComparer`1[[System.String,
          mscorlib]]
          624ac480        1           12 System.Int32

          624aa58c        1           12 System.Collections.Generic.ObjectEqualityComparer`1[[System.Type,
          mscorlib]]
          624adec0        1           16 System.Security.Policy.AssemblyEvidenceFactory
          624ace34        1           16 System.Text.DecoderReplacementFallback
          624acde4        1           16 System.Text.EncoderReplacementFallback
          6247a840        1           16 System.IO.TextReader+SyncTextReader
          624ade0c        1           20 Microsoft.Win32.SafeHandles.SafePEFileHandle
          6245fe58        1           20 Microsoft.Win32.SafeHandles.SafeFileMappingHandle
          6245fe08        1           20 Microsoft.Win32.SafeHandles.SafeViewOfFileHandle
          6245fd74        1           20 System.Text.InternalEncoderBestFitFallback
          6245f714        1           20 System.IO.Stream+NullStream
          624ad3d4        1           24 System.Version
          6245fdc4        1           24 System.Text.InternalDecoderBestFitFallback
          6245fa8c        1           24 System.IO.TextWriter+SyncTextWriter
          00163170        1           24 System.Collections.Generic.List`1[[System.Byte[], mscorlib]]
          624ad4b4        1           28 System.Text.StringBuilder
          624ab0b4        1           28 System.SharedStatics
          6247c1b8        1           28 System.Text.DBCSCodePageEncoding+DBCSDecoder
          6245f94c        1           28 Microsoft.Win32.Win32Native+InputRecord
          6245f664        1           28 System.Text.EncoderNLS
          624ade68        1           32 System.Security.Policy.PEFileEvidenceFactory
          624acc10        1           32 System.Text.UnicodeEncoding
          624ab938        1           36 System.Security.PermissionSet
          624aced8        2           40 Microsoft.Win32.SafeHandles.SafeFileHandle
          624ab7b0        1           40 System.Security.Policy.Evidence
          624aaa64        1           44 System.Threading.ReaderWriterLock
          6247cd1c        1           44 System.Text.InternalEncoderBestFitFallbackBuffer
          624aab90        1           48 System.Collections.Hashtable+bucket[]
          620c2348        1           48 System.Collections.Generic.Dictionary`2[[System.String,
          mscorlib],[System.Globalization.CultureData, mscorlib]]
          620c2268
          1           48 System.Collections.Generic.Dictionary`2[[System.Type,
          mscorlib],[System.Security.Policy.EvidenceTypeDescriptor,
          mscorlib]]
          624acf98        1           52 System.Collections.Hashtable
          624ab8d8        1           52 System.Threading.Thread
          624acb20        2           56 System.Reflection.RuntimeAssembly
          6245f994        2           56 System.IO.__ConsoleStream
          624adaa8        1           60 System.IO.StreamWriter
          624ad7b4        1           60 System.Collections.Generic.Dictionary`2+Entry[[System.String,
          mscorlib],[System.Globalization.CultureData, mscorlib]][]
          6249fbec
          1           64 System.IO.StreamReader
          624ab4e4        1           68 System.AppDomainSetup
          6247c624        1           76 System.Text.DBCSCodePageEncoding
          624ad474        1           84 System.Globalization.CalendarData
          624ab060        7           84 System.Object
          624aafe4        1           84 System.ExecutionEngineException
          624aafa0        1           84 System.StackOverflowException
          624aaf5c        1           84 System.OutOfMemoryException
          624aae08        1           84 System.Exception
          624ab130        1          112 System.AppDomain
          624ad164        2          144 System.Globalization.CultureInfo
          624ab028        2          168 System.Threading.ThreadAbortException
          624ad82c        2          264 System.Globalization.NumberFormatInfo
          624aa9f8        1          284 System.Collections.Generic.Dictionary`2+Entry[[System.Type,
          mscorlib],[System.Security.Policy.EvidenceTypeDescriptor,
          mscorlib]][]
          624ac448        8          484 System.Int32[]
          624ad3a0        2          616 System.Globalization.CultureData
          624abe78       26          728 System.RuntimeType
          624ab680        7         2910 System.Char[]
          6245ab98       25        18064 System.Object[]
          624aacc0     3283        85972 System.String
          00363a78        7      2031754 Free
          624696f8        2      2097184 System.Byte[][]
          624acf54   301232    304844554 System.Byte[]

          最后一个显然内存占用比较高,占了304844554 bite,如果你想在此情况下知道对象的内存地址你就直接使用!dumpheap ,不带任何参数。由于此命令会导致很多输出,我这里就写出输出内容了。通过!dumpheap 会得到内存很高的对象地址,02d55368,这个地址就是System.Byte[]对象,为了找到对象在哪里分配的,我们需要使用!gcroot 02d55368,命令,查看对象的根在哪里。

          0:000> !gcroot 02d55368
          Thread 143310:    0028f364
          004f0100 OrderManager.Program.Main(System.String[])
          [e:\NETDebug\DebugDemoProject\OrderManager\Program.cs @ 22]
          ebp+18: 0028f380
          ->  01b746c0 System.Collections.Generic.List`1[[System.Byte[], mscorlib]]
          ->  02d55368 System.Byte[][]

          知道了根就好办多了,直接看源代码就能发现问题。如果你还不死心的话可以使用!dumpobj 查看List对象。

          0:000> !dumpobj 01b746c0
          Name:
          System.Collections.Generic.List`1[[System.Byte[], mscorlib]]
          MethodTable:
          00163170
          EEClass:     6211c8b0
          Size:        24(0×18) bytes
          File:
          C:\Windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
          Fields:
          MT    Field   Offset                 Type VT     Attr    Value Name

          6245ab98  4000c75        4      System.Object[]  0 instance 02d55368 _items

          624ac480  4000c76        c         System.Int32  1 instance   301229 _size
          624ac480  4000c77       10         System.Int32  1 instance   301229 _version
          624ab060 4000c78 8 System.Object 0 instance 00000000 _syncRoot

          6245ab98  4000c79        0      System.Object[]  0   shared   static _emptyArray

          >> Domain:Value dynamic statics NYI 00359520:NotInit  <<

          这里需要注意的是,如果你是想执行!Clrstack -a 命令的话,当你使用调试器启动或者是附加进程的方式的化,要记住切换到适当的线程上才能看行。

          7.2.线程问题(CPU过高,线程死锁)

          CPU过高也是线上比较棘手的问题之一,查看CPU过高的步骤一般分为两步,查看线程的执行时间,然后切换到线程上下文,执行!ClrStack -a,看当前线程在哪里工作,到底做什么操作呢。

          0:004> !runaway
          User Mode Time
          Thread       Time
          0:143310      0 days 0:00:01.934
          4:142ac0      0 days 0:00:00.046
          7:143874      0 days 0:00:00.000
          6:143870      0 days 0:00:00.000
          5:14386c      0 days 0:00:00.000
          3:1432ec      0 days 0:00:00.000
          2:143384      0 days 0:00:00.000
          1:143254      0 days 0:00:00.000

          测试线程ID为0的执行时间比较大,我们需要切换到线程0上去执行查看调用堆栈信息,~0s。

          0:000> !ClrStack -a

          0028f348 62b897f9 System.IO.TextWriter+SyncTextWriter.WriteLine(Int32)

          PARAMETERS:

          this (<CLR reg>) = 0x01b74258         value = <no data>

          0028f358 62a66313 System.Console.WriteLine(Int32)

          PARAMETERS:

          value = <no data>

          0028f364 004f0100 OrderManager.Program.Main(System.String[]) [e:\NETDebug\DebugDemoProject\OrderManager\Program.cs @ 22]     PARAMETERS:

          args (0x0028f38c) = 0x01b71fe4

          LOCALS:

          0x0028f380 = 0x01b746c0

          0x0028f388 = 0x000498ac

          0x0028f37c = 0x16a2e338

          0x0028f384 = 0×00000001

          0028f51c 63162952 [GCFrame: 0028f51c]

          我们会发现在Main方法中有一个本地变量0x0028f380 ,保存的值是0x01b746c0,它就是指向刚才分配很多内存的List<byte[]>对象。

          线程死锁比较复杂,这里只给我认为比较简单的命令,通过此命令可以一眼看出哪个线程持有了哪个锁,目前在等待哪个锁。

          0:000> !syncblk
          Index         SyncBlock MonitorHeld Recursion Owning Thread Info          SyncBlock Owner
          4 0021fb20            3         1  00221f98 14974c   3   01ae2394 OrderManager.ImportOrder
          5 0021fb54           3          1 002234a8 149754    4   01ae23a0 OrderManager.ImportOrder
          —————————–
          Total
          5
          CCW             0
          RCW             0
          ComClassFactory
          0
          Free            0

          这是两个锁,也就是两个对象同步块。进一步使用SOSEX.dll中的!dlk查看死锁的自动化检查信息。

          0:000> !dlk

          Examining SyncBlocks… Scanning for ReaderWriterLock instances… Scanning for holders of ReaderWriterLock locks… Scanning for ReaderWriterLockSlim instances… Scanning for holders of ReaderWriterLockSlim locks… Examining CriticalSections… Could not find symbol ntdll!RtlCriticalSectionList. Scanning for threads waiting on SyncBlocks… Scanning for threads waiting on ReaderWriterLock locks… Scanning for threads waiting on ReaderWriterLocksSlim locks… Scanning for threads waiting on CriticalSections… *DEADLOCK DETECTED* CLR thread 0×3 holds the lock on SyncBlock 0021fb20 OBJ:01ae2394[OrderManager.ImportOrder] …and is waiting for the lock on SyncBlock 0021fb54 OBJ:01ae23a0[OrderManager.ImportOrder] CLR thread 0×4 holds the lock on SyncBlock 0021fb54 OBJ:01ae23a0[OrderManager.ImportOrder] …and is waiting for the lock on SyncBlock 0021fb20 OBJ:01ae2394[OrderManager.ImportOrder] CLR Thread 0×3 is waiting at System.Threading.Monitor.Enter(System.Object, Boolean ByRef)(+0×17 Native) CLR Thread 0×4 is waiting at System.Threading.Monitor.Enter(System.Object, Boolean ByRef)(+0×17 Native)

          1 deadlock detected.

          注意我加粗的那段话,检测到死锁。

          8.获取Dump文件时的重要注意事项

          在获取dump文件方面我也要分享一下重要的注意事项。如果获取dump文件不正确的话是无法进行分析的,会出现任何奇怪的问题。

          第一个就是使用64位机器上的任务管理获取32位进程dump文件,这通常是发生在服务器上,由于服务器IIS默认的启动进程方式是64位的,但是也有些情况下会变成32位的。

          图19:

          .NET应用程序调试—原理、工具、方法

          如果进程是以32位方式运行的,那么这个时候获取出来的dump文件是不好分析的,此时应该使用调试器工具进行dump的获取。获取出来的dump文件和分析机器上的调试器环境不一致的情况下会出现如下几个错误。

          图20:

          .NET应用程序调试—原理、工具、方法

          这个问题是未能加载正确版本的mscordacwks.dll .NETDAC调式组件。

          图21:

          .NET应用程序调试—原理、工具、方法

          这个问题是当前SOS.dll和.NET程序所使用的.NET版本不一致,这个问题的出现一般都是我们通过.load xx\xx\SOS.dll,手动方式加载的。

          图22:

          .NET应用程序调试—原理、工具、方法

          这个问题出现有好几种可能性,对常见的问题就是未能使用正确的方法或者工具获取dump文件,导致dum文件获取的机器和本地调试的机器整个环境不一致。

          9.总结

          本篇文章分享我对.NET应用程序调试方面学习和实践的一些经验,供广大博友参考。如果想系统的学习一下这方面的知识可以参考《.NET高级调试》一书,此书非常底层,对.NET运行时原理讲的很透彻,可以作为深入学习.NET的一门参考书。

          原文来自: 王清培