使用WinDbg调试程序 编程
WinDbg是微软发布的一款至关优秀的源码级(source-level)调试工具,能够用于Kernel模式调试和用户模式调试,还能够调试Dump文件。 数组
WinDbg是微软很重要的诊断调试工具: 能够查看源代码、设置断点、查看变量, 查看调用堆栈及内存状况。 服务器
调试应用程序(用户模式 user mode) 网络
调试操做系统及驱劢程序(内核模式 kernel mode) app
调试非托管程序(native program) dom
调试托管程序(managed program) 函数
实时调试 (JIT: Just in time) 工具
过后调试 (postmortem debugging) oop
使用WinDbg能够解决线上.NET应用程序的以下问题: post
◆ 内存高
◆ CPU高
◆ 程序异常
◆ 程序Hang死
在生产环境下进行故障诊断时,为了避免终止正在运行的服务或应用程序,有两种方式能够对正在运行的服务或应用程序的进程进行分析和调试。
1、用WinDbg等调试器直接attach到须要调试的进程,调试完毕以后再detach便可。可是这种方式有个缺点就是执行debugger命令时必须先break这个进程,执行完debug命令以后又得赶忙F5让他继续运 行,由于被你break住的时候意味着整个进程也已经被你挂起。另外也常常会因为First Chance Excetpion而自动break,你得时刻留意避免长时间break整个进程。因此这样的调试方式对时间是个很大的考验,每每没有充裕的时间来作仔细分析。
2、在出现问题的时候,好比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的法。
一、运行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的版本必定要正确)
二、在控制板的系统中设置一个系统变量_NT_SYMBOL_PATH 为
SRV*c:\symbols*http://msdl.microsoft.com/download/symbols
dump文件获取
dump文件是进程的内存镜像。能够把程序的执行状态,即当时程序内存空间数据经过调试器保存到dump文件中。
一、利用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
二、使用Debug Diagnostic Tool(DebugDiag)工具获取dump文件
下载Debug Diagnostic Tool而后进行安装,打开该工具,Debug Diagnostic Tool能够选择不一样的规则来进行dump文件。能够根据程序崩溃时捕获dump文件,也能够根据性能指标来进行捕获,如CPU太高,死锁,HTTP响应时间过程等参数。以下图:
也能够找到对应的进程,经过以下方法进行捕获。此种方式获取的dump文件放到C:\Program Files\DebugDiag\Logs\Misc下。
三、使用.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可以获得的最多的信息。是一种折中方案。
四、使用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.一、运行命令!eeheap –gc查看GC堆的状况,发现GC Heap大小为720多兆,因此咱们重点分析托管堆的状况。从运行结果能够看到GC Heap中g0,g1,g2和LOH的堆状况,以及该GC Heap中所分配的段状况。
能够运行!dumpheap -mt 0c3b0038 0c3b0048命令查看LOH堆中大对象的状况。从统计结果看,LOH堆中没有大的对象存在。同理咱们也能够统计各个段上对象的状况。
3.二、接下来看下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.三、过滤一下,看看10K以上大小的字符串,运行命令:!dumpheap -mt 6f021ee4 -min 10000。10K以上的有35996个。
3.四、随便找个对象看下引用关系,运行!gcroot 36278028,结果以下:
经过结果发现,Link引用了这个字符串。并且咱们看到,link是在Finalizer Queue中的。有关Finalizer Queue能够参考.net Finalization原理。
3.五、经过运行命令! Finalizequeue 查看Finalizer Queue队列的状况。
00b740fc 35987 575792 Link
一共有35987个Link对象存在于Finalizer Queue中,所以能够断定,Link类必定是显示的实现了Finalize方法。
3.六、查看该方法,代码以下:
~Link()
{
//some long running operation when cleaning up the data
Thread.Sleep(5000);
}
3.七、接下来咱们看下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.八、查看代码
public Link(string name, string url)
{
this.name = name;
this.url.Append(url);
}
会引发垃圾回收器托管堆速度的几个问题
一、分配太频繁
二、预先分配空间
三、太多的引用(pointers)和根(roots)
四、太多的对象实例有很长的生命期
五、太多的定位对象实例(pinned)
六、有终结函数的对象实例
占用更多资源
更长的生命期
两次才能回收
垃圾回收器(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.一、产生压力
TinyGet.exe -srv:dbg.buggybit.com -uri:/AllProducts.aspx -threads:5 -loop:1
4.二、经过Procdump抓取三个dump文件
procdump -ma -c 50% -s 2 -n 3 w3wp.exe -o d:\dump
4.三、打开这三个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.四、从上面三个输出结果发现只有22,23,30,31号线程一值在增加。且22,23线程增加的速度较快。查看三个dump的!threadpool基本都在90%以上。
4.五、运行!threads查看当前都有哪些线程。
不知道为何并无找到22,23线程,有30,31线程号。
4.六、切换到30线程中,运行命令~30s
4.七、查看当前线程的调用栈状况,运行!clrstack
发现System.String.Concat方法,这是典型的字符串拼接的函数,经过调用关系发现应该是在AllProducts.Page_Load(System.Object, System.EventArgs)方法中。
4.八、查看代码
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
那么一个个说明吧:
!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同样。 |