WinDbg是微软发布的一款相当优秀的源码级(source-level)调试工具,可以用于Kernel模式调试和用户模式调试,还可以调试Dump文件。
WinDbg是微软很重要的诊断调试工具: 可以查看源代码、设置断点、查看变量, 查看调用堆栈及内存情况。
调试应用程序(用户模式 user mode)
调试操作系统及驱劢程序(内核模式 kernel mode)
调试非托管程序(native program)
调试托管程序(managed program)
实时调试 (JIT: Just in time)
事后调试 (postmortem debugging)
使用WinDbg可以解决线上.NET应用程序的如下问题:
◆ 内存高
◆ CPU高
◆ 程序异常
◆ 程序Hang死
在生产环境下进行故障诊断时,为了不终止正在运行的服务或应用程序,有两种方式可以对正在运行的服务或应用程序的进程进行分析和调试。
一、用WinDbg等调试器直接attach到需要调试的进程,调试完毕之后再detach即可。但是这种方式有个缺点就是执行debugger命令时必须先break这个进程,执行完debug命令之后又得赶紧F5让他继续运 行,因为被你break住的时候意味着整个进程也已经被你挂起。另外也经常会由于First Chance Excetpion而自动break,你得时刻留意避免长时间break整个进程。所以这样的调试方式对时间是个很大的考验,往往没有充裕的时间来做仔细分析。
二、在出现问题的时候,比如CPU持续长时间100%,内存突然暴涨等非正常情况下,通过对服务进程snapshot抓取一个dump文件,完成dump之后先deatch,让进程继续运行。然后用windbg等工具来分析这个抓取到的dump 文件。所以我们一般采用这种方式来进行调试排错。
设置符号文件目录
符号文件包含了相关二进制文件的调试信息以.pdb戒.dbg为扩展名。WinDbg使用符号文件来确定调用栈,堆及其他重要信息。
配置WinDbg的符号文件路径
WinDbg符号文件路径搜索的两个位置:环境变量中的_NT_SYMBOL_PATH设置及WinDbg中的"symblos file path";
设置srv*x:/symbols_folder*http://msdl.microsoft.com/download/symbols 路径是保证我们能快速正确使用windbg的法。
1、运行WinDbg->File->Symbol File Path->按照下面的方法设置_NT_SYMBOL_PATH变量:
在弹出的框中输入"C:\ Symbols; SRV*C:\MyLocalSymbols*http://msdl.microsoft.com/download/symbols"(按照这样设置,WinDbg将先从本地文件夹C:\ Symbols中查找Symbol,如果找不到,则自动从MS的Symbol Server上下载Symbols)。另一种做法是从这个Symbol下载地址中http://www.microsoft.com/whdc/devtools/debugging/symbolpkg.mspx,下载相应操作系统所需要的完整的Symbol安装包,并进行安装,例如我将其安装在D:\WINDOWS\Symbols,在该框中输入"D:\WINDOWS\Symbols"。(这里要注意下载的Symbols的版本一定要正确)
2、在控制板的系统中设置一个系统变量_NT_SYMBOL_PATH 为
SRV*c:\symbols*http://msdl.microsoft.com/download/symbols
dump文件获取
dump文件是进程的内存镜像。可以把程序的执行状态,即当时程序内存空间数据通过调试器保存到dump文件中。
1、利用WinDbg里的adplus来获取dump文件
Adplus.vbs 是一个Visual Basic Script 文件,Adplus 主要用来生成内存转储文件 (dump file),内存转储文件适用于不能实时调试的情况下。在WinDbg安装目录里可以找到adplus.vbs,使用adplus.vbs生成dump文件,
adplus -hang -o d:\dump -p 1234
其中hang表示附加到进程,如果是crash,则为目标进程崩溃的时候抓取,-o后面的参数表示dump文件存到位置,-p后面的数字为进程的PID,也可以是-pn后面跟进程名称,如:adplus.vbs -hang -pn ConsoleWindbg.exe -o D:\dump
2、使用Debug Diagnostic Tool(DebugDiag)工具获取dump文件
下载Debug Diagnostic Tool然后进行安装,打开该工具,Debug Diagnostic Tool可以选择不同的规则来进行dump文件。可以根据程序崩溃时捕获dump文件,也可以根据性能指标来进行捕获,如CPU过高,死锁,HTTP响应时间过程等参数。如下图:
也可以找到对应的进程,通过如下方法进行捕获。此种方式获取的dump文件放到C:\Program Files\DebugDiag\Logs\Misc下。
3、使用.dump命令
1) 打开WinDBG—>File—>Attach to a Process,然后选择将之要进行捕获的进程。如我们这里要对ConsoleWindbg.exe进程产生dump文件。选择后如图:
2)在上图红色区域的输入框内输入产生dump 文件的命令 .dump 。可以选择不同的参数来生成不同类型的dump文件。
选项(1): /m
命令行示例:.dump /m D:/dump/myapp.dmp
注解: 缺省选项,生成标准的minidump, 转储文件通常较小,便于在网络上通过邮件或其他方式传输。 这种文件的信息量较少,只包含系统信息、加载的模块(DLL)信息、 进程信息和线程信息。
选项(2): /ma
命令行示例:.dump /ma D:/dump/myapp.dmp
注解: 带有尽量多选项的minidump(包括完整的内存内容、句柄、未加载的模块,等等),文件很大,但如果条件允许(本机调试,局域网环境), 推荐使用这中dump。
选项(3):/mFhutwd
命令行示例:.dump /mFhutwd D:/dump/myapp.dmp
注解:带有数据段、非共享的读/写内存页和其他有用的信息的minidump。包含了通过minidump能够得到的最多的信息。是一种折中方案。
4、使用ProcDump工具
Procdump是一个轻量级的命令行工具, 它的主要目的是监控应用程序的CPU异常动向, 并在此异常时生成crash dump文件, 供研发人员和管理员确定问题发生的原因。你还可以把它作为生成dump的工具使用在其他的脚本中。有了它, 就完全不需要在同一台服务器上使用诸如32位系统上的Debug Diag 1.1或是64位系统上的ADPlus了。
Procdump下载:http://technet.microsoft.com/en-us/sysinternals/dd996900
procdump -ma -c 50% -s 3 -n 2 5844 (Process Name or PID) -o c:\dumpfile
-ma 生成full dump, 即包括进程的所有内存. 默认的dump格式包括线程和句柄信息。
-c 在CPU使用率到达这个阀值的时候, 生成dump文件。
-s CPU阀值必须持续多少秒才抓取dump文件。
-n 在该工具退出之前要抓取多少个dump文件。
-o dump文件保存目录。
技术术语
GC Heap:用于存储对象实例,受 GC 管理
Loader Heap:分为 High-Frequency Heap 、 Low-Frequency Heap 和 Stub Heap ,不同的 heap 又存储不同的信息。 Loader Heap 中最重要的信息是元数据 (MetaData) 相关的信息,也就是 Type 对象,每个 Type 对象在 Loader Heap 上体现为一个 Method Table , Method Table 中记录了存储的元数据信息,如基类型、静态字段、实现的接口、所有的方法等。 Loader Heap 的生命周期为从 AppDomain 创建到卸载。
MethodTable: 我们知道每种type可以有多个instance,每个instance,其每个field可以享有独立的space,而对于type的method提供一个公共的method入口地址。也就是说不管多少个相同类型的instance,其都指向了同一个同一的函数入口地址。在这个函数入口地址描述表中记录了各个函数的入口地址。而MethodTable就有点类似的作用。不过所有Assembly都是自描述的,因此我们可以从MethodTable中,可以知道相应的instance。因此通过相应的debug命令!dumpheap -mt MTAddress可以知道在MethodTable中相关联的所有instance了。
Finalization 原理
通过WinDbg分析dump文件
通过上面步骤,我们生成了dump文件,接下来我们就可以使用WinDbg工具对生成的dump文件进行分析。
案例:
建立控制台应用程序,代码如下:
namespace ConsoleWindbg
{
class Program
{
private static List<User> list =new List<User>();
static void Main(string[] args)
{
MemeryLeakProc();
Console.ReadLine();
}
private static void MemeryLeakProc()
{
string str = "aaa";
while (true)
{
for (int i = 0; i < 100 * 1024; i++)
{
str += "bbb" + i;
User u = new User();
u.Age = i;
u.Name = "UserName" + i;
list.Add(u);
}
Thread.Sleep(1000);
}
}
}
public class User
{
public int Age { set; get; }
public string Name { set; get; }
}
}
编译,运行,按照上面的步骤产生dump文件。然后使用WinDbg打开dump文件。
红色标注区域显示了dump文件获取的一些环境信息,如:当前系统信息,程序运行时间,符号文件的路径等。
WinDbg调试托管程序时需用SOS扩展(SOS.dll), SOS 调试扩展(SOS.dll) 通过提供有关内部公共语言运行时(CLR) 环境的信息,帮助您在WinDbg.exe 调试器和Visual Studio 中调试托管程序。SOS.dll安装在.Net Framewok 目录底下C:\Windows\Microsoft.NET\Framework\vx.x.xxxxx。WinDbg调用SOS.dll的语法:
SOS.dll 在.Net Framewok 目录底下,在WinDbg的命令行输入:
.load C:\Windows\Microsoft.NET\Framework\v4.0.30319\SOS.dll
拷贝SOS.dll 到windbg目录底下,注意拷贝的Framework版本必须和你要调试的目标程序所使用的版本一致,否则调试信息就不能正确显示出来。如果你同时工作在两个版本的Framework的话,可以SOS文件重命名为SOS<version>.dll或者直接将它们放入不同的文件夹下面。可以使用如下命令:
.load sos.dll
.loadby sos mscorwks [.Net 3.5版本及以下]
检查SOS.dll是否已经装载
.chain
内存过高问题
内存过高问题初判定,内存泄漏可以通过以下两种方式通过性能监视器来基本判断属于那种类型的内存泄露。
A、非托管程序的症状 (Perfmon工具)
Process\Private Bytes 增加
.NET CLR Memory\# Bytes in all heaps不增加
B、托管程序的症状 (Perfmon工具)
Process\Private Bytes增加
.NET CLR Memory\# Bytes in all heaps也增加
本例属于托管程序症状,通过Perfmon工具已经得到判断,此步骤略。
-
测试程序
下载后部署到本机,产生压力:
tinyget -srv:dbg.buggybit.com -uri:/Links.aspx -loop:4000
观察进程中w3wp进程,发现内存增长很快,大约该进程内存增长到700M时,抓取一个hang dump。
-
与内存操作相关命令
!eeheap –gc:正是查看GC堆得命令,查看GC堆上的内存占用是多大
!eeheap –loader:正是查看Loader堆得命令
!dumpheap –stat:就是GC堆上的统计,就看看GC堆上存活的对象是那些
!dumpheap -mt <<MethodTable address>>:查看该地址上的对象
!dumpheap –type:通过 type 参数查看内存中指定类型的对象
!gcroot +对象地址:这个命令就可以得到这个对象的"根"
!objsize +对象地址:查看对象占用多大的内存
!name2ee TestClass.exe TestClass.Program.test ://显示test方法相关的地址
!dumpmt -md 00976d48 ://得到类的成员函数详细信息
!dumpmt:找到相关MethodTable处的相关信息。
!dumpmd:根据MethodDesc找到相关模块信息,比如MethodTable.
!dumpdomain:显示所有域里的程序集,或者根据参数获取指定域。
!dumpil 00973028:// 显示这个方法被编译器编译之后的IL代码
!dumpobj(do) 012a3904: //显示一个对象的具体内容,看对象里面有什么,值是什么
!dumpmodule 1ee30010:查看某个模块的详细信息
!DumpArray: //查看数组信息
-
分析过程
3.1、运行命令!eeheap –gc查看GC堆的情况,发现GC Heap大小为720多兆,所以我们重点分析托管堆的情况。从运行结果可以看到GC Heap中g0,g1,g2和LOH的堆情况,以及该GC Heap中所分配的段情况。
可以运行!dumpheap -mt 0c3b0038 0c3b0048命令查看LOH堆中大对象的情况。从统计结果看,LOH堆中没有大的对象存在。同理我们也可以统计各个段上对象的情况。
3.2、接下来看下heap中对象的一些情况,运行命令!dumpheap –stat。统计堆上所有对象的情况。统计项包括MT(Method Table),Count对象个数,TotalSize对象所占用的大小。Count与TotalSize按照升序统计。
最终我们发现,System.Char[]占用内存最多,大概720M,同时有36088个System.Char[]对象。
这里我们做个推理,通过!dumpheap –stat统计到的System.Char[]的个数,应该与在3.1中显示的各个段中System.Char[]个数之和相等。即如果对3.1中统计到的各个段进行!dumpheap –stat <startAddress> <endAddress>统计,各个段中统计到的System.Char[]个数之和应该与3.2中统计到的结果相同,通过验证发现,结论正确。
3.3、过滤一下,看看10K以上大小的字符串,运行命令:!dumpheap -mt 6f021ee4 -min 10000。10K以上的有35996个。
3.4、随便找个对象看下引用关系,运行!gcroot 36278028,结果如下:
通过结果发现,Link引用了这个字符串。而且我们看到,link是在Finalizer Queue中的。有关Finalizer Queue可以参考.net Finalization原理。
3.5、通过运行命令! Finalizequeue 查看Finalizer Queue队列的情况。
00b740fc 35987 575792 Link
一共有35987个Link对象存在于Finalizer Queue中,因此可以判定,Link类一定是显示的实现了Finalize方法。
3.6、查看该方法,代码如下:
~Link()
{
//some long running operation when cleaning up the data
Thread.Sleep(5000);
}
3.7、接下来我们看下Link对象的结构,可以通过3.4步骤中运行出来的结果找到对应那个Link对象的地址,通过运行命令!do 36277ffc 来查看,当然也可以通过找到Link对象的MT,通过查看!dumpheap –mt <MTAddress>上的所有Link对象,找到其中一个地址,在通过!do <address>来查看。
据此发现,Link应该有url和name两个属性。通过!objsize 362a66ec查看url对象的大小为20k,且是StringBuilder类型的。
sizeof(362a66ec) = 20040 ( 0x4e48) bytes (System.Text.StringBuilder)
3.8、查看代码
public Link(string name, string url)
{
this.name = name;
this.url.Append(url);
}
会引起垃圾回收器托管堆速度的几个问题
1、分配太频繁
2、预先分配空间
3、太多的引用(pointers)和根(roots)
4、太多的对象实例有很长的生命期
5、太多的定位对象实例(pinned)
6、有终结函数的对象实例
占用更多资源
更长的生命期
两次才能回收
垃圾回收器(GC)只有一个线程来运行终结函数
有时这个线程会很慢戒堵塞(blocked)
CPU/异常操作相关命令
查看引起CPU过高命令比如:
!threadpool:查看线程池CPU使用量,我认为WEB的比如iis应用程序池进程w3wp如果CPU使用过高,那查看线程池命令肯定看的出来过高,这个是我自己的理解,c/s的就不一定了。
!threads:查看所有托管线程情况
!clrstack:到具体某个线程后,本线程托管代码的调用栈情况
~* e !clrstack:所有线程托管代码的调用栈情况
!runaway:查看线程占用CPU时间,可以从中找到哪个线程占用时间更高。
~number s:number为具体哪个线程的ID。
!dumpstackobjects(!dso):本线程调用栈所有对象实例
!dumpdomain:显示所有域里的程序集,或者根据参数获取指定域。
!savemodule:根据具体程序集地址,把当前程序集的代码生成到指定文件
!PrintException:显示在当前线程上引发的最后一个异常错误信息
!StopOnException:在指定异常错误信息停止运行
!VerifyHeap:检查垃圾回收器堆中是否有损坏迹象,并显示找到任何错误
!SyncBlk –all:显示所有SyncBlock 结构情况
4.1、产生压力
TinyGet.exe -srv:dbg.buggybit.com -uri:/AllProducts.aspx -threads:5 -loop:1
4.2、通过Procdump抓取三个dump文件
procdump -ma -c 50% -s 2 -n 3 w3wp.exe -o d:\dump
4.3、打开这三个dump,加载sos之后,分别查看!runaway的输出。
第一个dump输出:
0:023> !runaway
User Mode Time
Thread Time
23:14c8 0 days 0:00:06.817
22:1b74 0 days 0:00:06.084
31:ba8 0 days 0:00:02.823
30:680 0 days 0:00:02.823
33:25c 0 days 0:00:00.280
35:13c8 0 days 0:00:00.218
第二个dump输出:
0:023> !runaway
User Mode Time
Thread Time
23:14c8 0 days 0:00:12.792
22:1b74 0 days 0:00:11.918
31:ba8 0 days 0:00:04.009
30:680 0 days 0:00:03.712
35:13c8 0 days 0:00:01.965
33:25c 0 days 0:00:01.887
34:14e4 0 days 0:00:00.514
第三个dump输出:
0:023> !runaway
User Mode Time
Thread Time
23:14c8 0 days 0:00:18.969
22:1b74 0 days 0:00:17.160
31:ba8 0 days 0:00:05.382
30:680 0 days 0:00:04.804
35:13c8 0 days 0:00:03.151
33:25c 0 days 0:00:02.792
34:14e4 0 days 0:00:01.185
4.4、从上面三个输出结果发现只有22,23,30,31号线程一值在增长。且22,23线程增长的速度较快。查看三个dump的!threadpool基本都在90%以上。
4.5、运行!threads查看当前都有哪些线程。
不知道为什么并没有找到22,23线程,有30,31线程号。
4.6、切换到30线程中,运行命令~30s
4.7、查看当前线程的调用栈情况,运行!clrstack
发现System.String.Concat方法,这是典型的字符串拼接的函数,通过调用关系发现应该是在AllProducts.Page_Load(System.Object, System.EventArgs)方法中。
4.8、查看代码
protected void Page_Load(object sender, EventArgs e)
{
DataTable dt = ((DataLayer)Application["DataLayer"]).GetAllProducts();
string ProductsTable = "<table><tr><td><B>Product ID</B></td><td><B>Product Name</B></td><td><B>Description</B></td></tr>";
foreach (DataRow dr in dt.Rows)
{
ProductsTable += "<tr><td>" + dr[0] + "</td><td>" + dr[1] + "</td><td>" + dr[2] + "</td></tr>" ;
}
ProductsTable += "</table>";
tblProducts.Text = ProductsTable;
}
这里面有一个循环的方法,然后针对输出的DataTable,进行了大量的String.Concat操作。
1!address
!address 扩展显示目标进程或目标机使用的内存信息。
这个学习起来比较简单:我们直接使用!address -?就可以找到它的使用说明:
给个例子:
[cpp] view plaincopy
-
0:001> !address -?
-
!address - prints information on the entire address space
-
!address -? - prints this help
-
!address <address> - prints available information about the region
-
of the address space containing this address
-
!address -summary - prints only summary information
-
!address -RegionUsageXXX - fiters the output limiting the dispaly to one
-
of the following types:
-
RegionUsageIsVAD - `busy` region that could be charcterized better
-
this includes Virtual-Alloc-ed blocks, SBH heap,
-
memory from custom allocators, etc
-
RegionUsageFree - availalble (neither committed nor reserved) region
-
RegionUsageImage - region used by mapped images of binaries
-
RegionUsageStack - stack of threads
-
RegionUsageTeb - TEB of threads
-
RegionUsageHeap - region in used by a heap
-
RegionUsagePageHeap - region in use by full page-heap
-
RegionUsagePeb - PEB of the process
-
RegionUsageProcessParametrs - parameters of the process
-
RegionUsageEnvironmentBlock - environment block
那么一个个说明吧:
!address显示整个地址空间和使用摘要的信息
这个太长了,它会把从0-7ffefff的全打印出来,熟悉核心编程的应该知道,正常的2G用户地址空间是这样划分的:0-ffff为64K空指针区,1000-7ffeffff为用户模式分区
之后64K为禁入分区,之后就是内核模式分区,要看它们的信息,需要用到以下的表,
Filter 值 | 显示的内存区域 |
RegionUsageIsVAD | "busy" 区域。包括所有虚拟分配块、SBH堆、自定义内存分配器(custom allocators)的内存、以及地址空间中所有属于其他分类的内存块。 |
RegionUsageFree | 目标的虚拟地址空间中所有可用内存。包括所有非提交(committed)和非保留(reserved)的内存。 |
RegionUsageImage | 用来映射二进制映像的内存区域。 |
RegionUsageStack | 用作目标进程的线程的堆栈的内存区域。 |
RegionUsageTeb | 用作目标进程中所有线程的线程环境块(TEB)的内存区域。 |
RegionUsageHeap | 用作目标进程的堆的内存区域。 |
RegionUsagePageHeap | 用作目标进程的整页堆(full-page heap)的内存区域。 |
RegionUsagePeb | 目标进程的进程环境块(PEB)的内存区域。 |
RegionUsageProcessParametrs | 用作目标进程启动参数的内存区域。 |
RegionUsageEnvironmentBlock | 用作目标进程的环境块的内存区域。 |
下面这些Filter值按照内存类型来指定内存。
Filter 值 | 显示的内存类型 |
MEM_IMAGE | 映射的文件属于可执行映像一部分的内存。 |
MEM_MAPPED | 映射的文件不属于可执行映像一部分的内存。这种内存包含哪些从页面文件映射的内存。 |
MEM_PRIVATE | 私有的(即不和其他进程共享)并且未用来映射任何文件的内存。 |
下面的Filter 值按照状态来指定内存:
Filter 值 | 显示的内存状态 |
MEM_COMMIT | 当前已提交给目标使用的所有内存。已经在物理内存或者页面文件中为这些内存分配了物理的存储空间。 |
MEM_RESERVE | 所有为目标以后的使用保留的内存。这种内存还没有分配物理上的存储空间。 |
MEM_FREE | 目标虚拟地址空间中所有可用内存。包括所有未提交并且未保留的内存。该Filter 值和RegionUsageFree一样。 |