Windbg使用简明指南

时间:2022-08-25 19:57:22

第一章 准备

1.1.    环境配置

_NT_DEBUGGER_EXTENSION_PATH=C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727

_NT_SYMBOL_PATH=SRV*c:\Symbols*http://msdl.microsoft.com/download/symbols

Path add:

C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727

C:\Program Files\Debugging Tools for Windows (x86)

1.2 .Net CLR知识

Windbg使用简明指南

第二章 常用命令

2.1 基本命令

序号

命令

解释

.chain

显示有哪些调试扩展。

.load DLLName

!DLLName.load

加载调试扩展。DLLName要是全路径名,包括”.dll”

.loadby DLLName ModuleName

加载调试扩展。DLLName是短文件名,不包括”.dll”。

ModuleName是调试进程中的模块名,表示通过它所在的路径查找DLLName。

.unload DLLName

!DLLName.unload

卸载调试扩展。

.setdll DLLName

!DLLName.setdll

设置缺省的调试扩展。

![ext.]address

显示VM的分配状况

![sos.]vmmap

显示VM的分配状况

![uext.]vadump

输出虚拟地址映射信息

![uext.]vprot address

显示给出的虚拟地址所在内存的映射信息。

.logfle

检查是否有日志文件

.logopen

.logappend

打开日志文件。输出内容多时特别有用!

一个是新建,另一个是追加。

.logclose

关闭日志文件

dt

通过符号输出类型的结构拓扑信息。

![ntsdexts.]heap 0 0

察看Win32堆的运行状况。

使用!heap –stat察看heap的内存使用情况。

![ext.]dlls

可以察看进程内的动态链接库的信息。

![sos.]threads

可以列出所有的托管线程,并在栈顶给出异常对象的地址。

![sos.]ip2md addr

可以将IP地址转换成IL对应的方法名。

![sos.]BPMD <module name>   <method name>

![sos.]BPMD -md <MethodDesc>

可以在托管代码的指定位置设置断点。

method name是包含namespace.class.method的全名!

!bpmd myapp.exe MyApp.Main

Example for generics:

Given   the following two classes:

class   G3<T1, T2, T3>

{

...

public void F(T1 p1, T2 p2, T3 p3)

{ ... }

}

public   class G1<T> {

// static method

static public void G<W>(W w)

{ ... }

}

One   would issue the following commands to set breapoints on G3.F() and  G1.G():

!bpmd   myapp.exe G3`3.F

!bpmd   myapp.exe G1`1.G

![sos.]Name2EE module_name item_name

![sos.]Name2EE module_name!item_name

查找名称(类型/方法/属性)对应的CLR信息

Examples:

!Name2EE    mscorlib.dll System.String.ToString

!Name2EE *!System.String

Examples:

0:018> !name2ee mscorlib.dll   System.IntPtr

Module: 790c2000 (mscorlib.dll)

Token: 0x020000c1

MethodTable: 790fe160

EEClass: 790fe0d0

Name: System.IntPtr

![sos.]dumpclass

查找EEClass对应的信息

Examples:

0:018> !DumpClass 790fe0d0

Class Name: System.IntPtr

mdToken: 020000c1   (C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)

Parent Class: 790f9d24

Module: 790c2000

Method Table: 790fe160

Vtable Slots: 5

Total Method Slots: 1b

Class Attributes: 102109

NumInstanceFields: 1

NumStaticFields: 1

MT    Field   Offset                 Type VT     Attr      Value Name

79118260  40003e0        4                  PTR  0 instance           m_value

790fe160  40003e1        96c        System.IntPtr  0     shared   static Zero

>> Domain:Value    000da220:NotInit  000fde60:0   <<

![sos.]token2ee

查找mdToken对应的名称信息。

Examples:

0:018> !Token2EE mscorlib.dll 020000c1

Module: 790c2000 (mscorlib.dll)

Token: 0x020000c1

MethodTable: 790fe160

EEClass: 790fe0d0

Name: System.IntPtr

c rgBuf1 L 100 rgBuf2

比较内存

as

设置别名,例:

as Name EquivalentLine

aS Name EquivalentPhrase

aS Name "EquivalentPhrase"

as /e Name EnvironmentVariable

as /ma Name Address

as /mu Name Address

as /msa Name Address

as /msu Name Address

as /x Name Expression

aS /f Name File

as /c Name CommandString

ad

删除别名,例:

ad [/q] Name

ad *

al

列表别名。例:

al

${ }

解释(提取)别名,例:

Text ${Alias} Text

Text ${/d:Alias} Text

Text ${/f:Alias} Text

Text ${/n:Alias} Text

Text ${/v:Alias} Text

特别有用的MASM命令处理函数.

$fnsucc(FnAddress, RetVal, Flag)

$iment (Address)

$scmp("String1",   "String2")

$sicmp("String1",   "String2")

$spat("String",   "Pattern")

$vvalid(Address, Length)

~#s

设置当前线程上下文。例:

0: kd> ~1s

1: kd>

$<, $><, $$<, $$><, $$>a<

(Run Script File)

$<Filename

$><Filename

$$< Filename

$$>< Filename

$$>a< Filename arg1   arg2 arg3 ... argn

!runaway

The   !runaway extension displays information about the time consumed by each   thread.

查看线程信息

Flags

Specifies the kind of information to be displayed. Flags can be any   combination of the following bits. The default value is 0x1:

Bit 0 (0x1)

Causes the debugger to show the amount of user time consumed by each   thread.

Bit 1 (0x2)

Causes the debugger to show the amount of kernel time consumed by each   thread.

Bit 2 (0x4)

Causes the debugger to show the amount of time that has elapsed since   each thread was created.

0:001> !runaway 7

例:

User Mode Time

Thread         Time

0:55c        0:00:00.0093

1:1a4        0:00:00.0000

Kernel Mode Time

Thread         Time

0:55c        0:00:00.0140

1:1a4        0:00:00.0000

Elapsed Time

Thread         Time

0:55c        0:00:43.0533

1:1a4        0:00:25.0876

!envvar Variable

获取环境变量的值

0:000>   !envvar _nt_symbol_path

_nt_symbol_path =   srv*C:\mysyms*http://msdl.microsoft.com/download/symbols

.printf

类似C库的printf函数,可以接收的参数有:

%p, %N, %I, %ma, %mu, %msa, %msu, %y, %ly

除了文档中给出的参数,还可以接收C库中的格式化字符串,如:

%d, %c, %d, %f, %E等,功能强劲。

如果需要输出64位数字,可以使用"%I64x"。如:

0:000> .printf "%I64x",   0x00001234`00000123;

123400000123

n [Radix]

Set   Number Base

命令格式:

n [Radix]

Radix

Specifies the default number base that is   used for numeric display and entry. You can use one of the following values.

8      Octal

10     Decimal

16     Hexadecimal

![sos.]clrstack

!CLRStack [-a] [-l] [-p]

-a –l –p选项可以输出栈的参数,非常有用!

![sos.]DumpStack

!DumpStack [-EE] [top stack [bottom stack]]

比ClrStack的输出内容更为详尽,能同时显示native和.NET的堆栈信息!top stack/bottom stack是从k系列命令看到的栈顶和栈底地址。 -EE 限制只输出.NET堆栈。

如:

!DumpStack 77b1dae4 77b1dba4

![exts.]tp

!tp   pool Address [Flags]

!tp   tqueue Address [Flags]

!tp   ItemType Address [Flags]

!tp   ThreadType [Address]

!tp   -?

察看OS的thread pool信息

![sos.]threadpool

察看.NET使用的thread pool的统计信息。

x

x [Options] Module!Symbol

x [Options] *!*

检查加载的符号,特别有用!

注意,Module是模块名,不带.dll。例:

0:000> x mymodule!*spin*

0:000> x *!*

j

j   (Execute If - Else)

j   Expression Command1 ; Command2

j   Expression 'Command1' ; 'Command2'

If   this expression evaluates to a nonzero value, Command1 is executed. If this   expression evaluates to zero, Command2 is executed.例:

0:000> bp `mysource.cpp:143` "j   (poi(MyVar)>0n20) ''; 'gc' "

0:000> j (MySymbol=0) 'r eax'; 'r ebx; r ecx'

0:000> j (MySymbol=0) ''; 'r ebx; r ecx'

0:000> j (MySymbol=0)  ; 'r ebx; r ecx'

g

gc

如果在条件断点之中,应使用gc命令,这样如果通过step或trace进入断点,那么出断点gc维持step或trace命令;而g命令无论怎么进入断点都使用g命令执行。

0:000>   bp Address "j (Condition) 'OptionalCommands'; 'gc' "

0:000>   bp Address "j (Condition) 'OptionalCommands'; 'g' "

gu

The   gu command causes the target to execute until the current function is   complete.

User-Mode Syntax

[~Thread] gu

Kernel-Mode Syntax

gu

![sos.]findappdomain

!findappdomain <object address>

查找对象所在的AppDomain

sx, sxd, sxe,   sxi, sxn, sxr

sx,   sxd, sxe, sxi, sxn, sxr (Set Exceptions)

设置异常发生后调试器的行为。

sx

sx{e|d|i|n} [-c   "Cmd1"] [-c2 "Cmd2"] [-h] {Exception|Event|*}

sxr

sx查看系统异常处理列表;

sxr重置系统异常处理列表;

sxe

Break   (Enabled)  When this exception occurs,   the target immediately breaks into the debugger before any other error   handlers are activated. This kind of handling is called first chance   handling.

sxd

Second   chance break (Disabled). The debugger does not break for a first-chance   exception of this type (although a message is displayed). If other error   handlers do not address this exception, execution stops and the target breaks   into the debugger. This kind of handling is called second chance handling.

sxn

Output   (Notify). When this exception occurs, the target application does not break   into the debugger at all. However, a message is displayed that notifies the   user of this exception.

sxi

Ignore   When this exception occurs, the target application does not break into the   debugger at all, and no message is displayed.

#

#   (Search for Disassembly Pattern)

# [Pattern] [Address [ L   Size ]]

示例:

#  mov    0040116b

#  8945*    0040116b

#  116d    0040116b

# strlen main

例如:在反汇编“8b4510          mov     eax,dword ptr [ebp+10h]”查找[ebp+10h],命令如下:

#   \[ebp\+10h\] 7c810000

注意,这个命令进行的是形式匹配!因此要求先查看U命令的输出结果。

ld

ld   (Load Symbols)

ld   [ModuleName]

加载调试信息。在对付lazy load的dll时比较有用。

ln

ln   (List Nearest Symbols)

ln   Address

The   ln command displays the symbols at or near the given address.

ls, lsa

ls,   lsa (List Source Lines)

ls   [.] [first] [, count]

lsa   [.] address [, first [, count]]

.context

.context   (Set User-Mode Address Context)

.context   [PageDirectoryBase]

PDB地址来自.process命令

kd>   !process 0 0

PROCESS   fe3c0d60  SessionId: 0  Cid: 0208      Peb: 7ffdf000  ParentCid: 00d4

DirBase: 0011f000  ObjectTable: fe3d0f48    TableSize:  30.

Image: regsvc.exe

.process

.process   (Set Process Context)

The   .process command specifies which process is used for the process context.

Syntax

.process   [/i] [/p [/r] ] [/P] [Process]

.thread

.thread   (Set Register Context)

The   .thread command specifies which thread will be used for the register context.

Syntax

.thread   [/p [/r] ] [/P] [/w] [Thread]

.time

.time (Display System Time)

0:000> .time

Debug session time: Mon Apr 07 19:10:50 2003

System Uptime: 4 days   4:53:56.461   OS已经运行了多久

Process Uptime: 0 days   0:00:08.750  进程已经运行了多久

Kernel time: 0 days   0:00:00.015

User time: 0 days   0:00:00.015

.ttime

The   .ttime command displays the running times for a thread.

0:000>   .ttime

Created: Sat Jun 28 17:58:42 2003

Kernel:  0 days 0:00:00.131

User:    0 days 0:00:02.109

.writemem

.writemem   FileName Range

![exts.]peb

The   !peb extension displays a formatted view of the information in the process   environment block (PEB).

Syntax

!peb   [PEB-Address]

![exts.]teb

The   !teb extension displays a formatted view of the information in the thread   environment block (TEB).

Syntax

!teb   [TEB-Address]

![exts.]tls

The   !tls extension displays a thread local storage (TLS) slot.

Syntax

!tls Slot [TEB]

Parameters

Slot

Specifies   the TLS slot. This can be any value between 0 and 1088 (decimal). If Slot is -1, all slots are displayed.

TEB

Specifies   the thread environment block (TEB). If this is 0 or   omitted, the current thread is used.

![ntsdexts.]locks

!locks   [-v][-o]              - Dump all Critical   Sections in process

2.2.    常用命令

得到dump文件:

Adplus -hang -pn w3wp.exe -quiet (hang)   [-FullOnFirst]

adplus -crash -pn w3wp.exe (crash)

加载 sos 扩展功能dll:

.load sos.dll

.loadby sos mscorwks

自动分析

!analyze –v

!analyze -hang。这个扩展将会执行一个线程栈分析以确定是不是有哪些线程正在阻塞其他线程。

查看版本:

!eeversion

查看异常:

!dumpallexceptions

运行 help 命令:

!help

!help [command name]   //查找某个命令更多详细的内容, 如: !help eeversion

显示系统与时间有关的信息:

.time

dump文件时cpu的使用情况。同时可以得到其他的有用的信息(例如:等待请求队列的数量,已完成的线程和时间):

!threadpool

列出当前正在运行的线程和cpu使用情况

!runaway                 //比如查看哪个线程占用 CPU 时间过多

列表显示出应用程序所有正在运行的线程,当前应用程序域中后台正在运行的程序,等等线程相关内容

!threads

切换到特定的线程上查看相关内容

~[thread id]s  //例如: ~2s

列出当前线程中的调用栈信息

!clrstack     //如果想查看额外的信息,添加 “-p”选项, 如: !clrstack –p

!clrstack -a

显示指定地址对象的内容:

!dumpobj 0xf32fcc              //(!do 为简写)

继续查看对象的值

!dumpobj -v 0xf32fcc

!dumpvc 7910c878  01573774  //mt  value

查看当前特定线程的堆栈中所有托管对象

!dumpstackobjects (或 简称!dso )           //next:  !dumpobj 0x1d298ad8

得到当前线程对象中关于数组对象的详细信息

!dumparray [address]     (简称!da)         //!do [address]仅得到简单信息

!da –details [address]  //详细信息

得到对象的整个大小

!objsize 071bef70

!objsize poi(0x61b47d4+0xc)      //Address + Offset

.foreach (obj {!dumpheap -mt 0x0c2eaeb4 -short}){!objsize ${obj}}   //0x0c2eaeb4: mt

.foreach(myobj {!dumpheap -short -min 85000}) {!objsize myobj} //所有所有对象中大于85K内容

dump出所有的托管堆上的对象

!dumpheap –stat             //使用 –stat 参数来得到托管堆上的摘要信息

参数–mt (即:MethodTable):

!dumpheap -mt 793308ec      //next: 进一步查看对象,使用 !dumpobj [object address]即可

参数–mt:

!dumpheap –type System      //出许多包含 System 名称的对象

参数是 –min \ -max ,该参数接受一个最小\最大的对象字节数:

!dumpheap –stat –min 85000          //查看大于85000bytes字节的对象

!dumpheap -min 85000

!dumpheap -mt 790fd8c4 -min 20000 -max 25000

!dumpheap -type System.String -min 150 -max 200  //检查大小在 150 至 200 之间的所有字符串

!dumpheap -mt 790fd8c4 -strings   //只输出字符串

参数-short:

!dumpheap -type System.String -min 6500 –short  //仅仅查询出对象的地址信息

.foreach语句:

.foreach (myAddr {!dumpheap -type System.String -min 6500 -short}){!dumpobj myAddr;.echo **************************}        // .echo 命令打印分割符

.shell命令:

.shell -i - -ci "!iisinfo.clientconns" FIND /c "Request active"

.shell -i - -ci "!iisinfo.clientconns" FIND /c "<table"

!dumpobj 04aa1a90     // 查看对象的详细信息

!dumpclass 0x6c632e8  // 查看类型的详细信息

!dumpmt -md 0x09750a8 // 查看方法表的详细信息

!dumpmd 0x00975070    // 查看方法表项的方法描述的详细信息

u 0x79b7c4eb          // 反汇编指定地址的指令

[http://www.cnblogs.com/flier/archive/2004/07/08/22361.html]

查看进程程序集加载情况:

!dumpdomain

将进程程序集导出

!SaveModule 00992c5c z:\temp\a.dll

显示模块信息:

!dumpmodule [-mt] 1c5a1098   //1c5a1098: module address

查看native内存内容:

MetaData start address: 1d3b09e4 (4184 bytes)

dc 1d3b09e4 1d3b09e4+0n4184

du 3cd30038 3cd30038 +1000

[异常]

查看托管线程

!threads

查看所有的Exception:

!dumpheap –type Exception

命令将打印出当前堆栈上正在被抛出的exception

!pe/!PrintException

打印出相应命令的详细信息及堆栈:

!pe address

查看某个具体的异常的详细信息

!do [address]

!do –nofields [address] //string类型简单输出格式

查看对象的信息:

!gcroot [address]

打印出所有的同一个类型的Exception的信息

.foreach(myVariable {!dumpheap -type System.ArgumentNullException -short}){!pe myVariable;.echo **}

//OutOfMemoryException? *Exception? System.OutOfMemoryException?

列出了GC 堆的大小 和G0,G1,G2 ,LOH的开始地址:

!EEHeap [-gc] [-loader]

!eeheap

!eeheap –gc

!eeheap -loader

查看内存信息

!name2ee * WindbgDemo.Program

查看方法描述

!dumpmd 00993034

查看方法表信息

!dumpmt -md 0099304c

查看il

!dumpil 00993034

Dump文件

C:\Program Files\Debugging Tools for Windows (x86) adplus.vbs -hang -o C:\dump -p 6876

参数说明:

?-hang: 表示附加到目标进程,抓取 dump 镜像,然后解除。对应的参数是 -crash 崩溃模式,该参数会终止目标进程。

?-o: 指定 Dump 文件保存路径。

?-p: 指定目标进程 PID。

Jitted代码:

  1. sxe ld:mscorjit.dll
  2. 2.    .loadby sos mscorwks
  3. !name2ee test.exe System.Program.Main
  4. ba w4 975070+0x04 "bp poi(975070+0x04);g"
  5. !clrstack

Ngened代码:

http://blog.joycode.com/gangp/archive/2004/04/28/20417.joy

1、!analyze -v :用于分析挂掉线程的详细情形,错误原因。

2、!locks :列出全部资源使用情况。

3、!locks -v 0x???????? :特定地址的死锁分析。

4、!thread 0x????????:特定线程详情。

5、.thread 0x????????:转到某个线程堆栈。

2.3.    ~

查看系统当前线程,使用~*s命令切换线程,如需要切换到8号线程,可以使用命令:~8s

0:004> ~

0  Id: dd0.7c0 Suspend: 1 Teb: 7ffdf000 Unfrozen

1  Id: dd0.1230 Suspend: 1 Teb: 7ffde000 Unfrozen

2  Id: dd0.bc4 Suspend: 1 Teb: 7ffdd000 Unfrozen

3  Id: dd0.1424 Suspend: 1 Teb: 7ffdc000 Unfrozen

.  4  Id: dd0.ba8 Suspend: 1 Teb: 7ffdb000 Unfrozen

2.4.    .load sos

加载微软提供的调试扩展工具,可以更方便的调试托管代码。只有加载SOS之后,才能使用下面的这些命令。

0:004> .load sos

2.5.    !clrstack

查看当前线程的调用栈,如果想查看所有进程的调用栈情况, 使用~*e !clrstack

0:000> !clrstack

OS Thread Id: 0x7c0 (0)

ESP       EIP

002aee00 775564f4 [NDirectMethodFrameStandalone: 002aee00] System.Windows.Forms.SafeNativeMethods.MessageBox(System.Runtime.InteropServices.HandleRef, System.String, System.String, Int32)

002aee1c 66fffd28 System.Windows.Forms.MessageBox.ShowCore(System.Windows.Forms.IWin32Window, System.String, System.String, System.Windows.Forms.MessageBoxButtons, System.Windows.Forms.MessageBoxIcon, System.Windows.Forms.MessageBoxDefaultButton, System.Windows.Forms.MessageBoxOptions, Boolean)

002aeebc 66fff93e System.Windows.Forms.MessageBox.Show(System.String)

002aeec4 006603ab TestCode4AQTime.Form1.button1_Click(System.Object, System.EventArgs)

002aeee0 669d4170 System.Windows.Forms.Control.OnClick(System.EventArgs)

002af04c 66a023b0 System.Windows.Forms.Button.WndProc(System.Windows.Forms.Message ByRef)

002af058 66a084a0

002af218 007c09e4 [NDirectMethodFrameStandalone: 002af218] System.Windows.Forms.UnsafeNativeMethods.DispatchMessageW(MSG ByRef)

002af228 66a18aee System.Windows.Forms.Application+ComponentManager.System.Windows.Forms.UnsafeNativeMethods.IMsoComponentManager.FPushMessageLoop(Int32, Int32, Int32)

002af2c4 66a18757 System.Windows.Forms.Application+ThreadContext.RunMessageLoopInner(Int32, System.Windows.Forms.ApplicationContext)

002af318 66a185a1 System.Windows.Forms.Application+ThreadContext.RunMessageLoop(Int32, System.Windows.Forms.ApplicationContext)

002af348 669d5911 System.Windows.Forms.Application.Run(System.Windows.Forms.Form)

002af35c 006600ae TestCode4AQTime.Program.Main()

2.6.    !do(DumpObj)

查看class对象内容。!do [对象的内存地址]

0:000> !do 01e80d5c

Name: System.String

MethodTable: 68eb88c0

EEClass: 68c7a498

Size: 102(0x66) bytes

(C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)

String: system.serviceModel.activation/diagnostics

Fields:

MT    Field   Offset                 Type VT     Attr    Value Name

68ebab0c  4000096        4         System.Int32  1 instance       43 m_arrayLength

68ebab0c  4000097        8         System.Int32  1 instance       42 m_stringLength

68eb95a0  4000098        c          System.Char  1 instance       73 m_firstChar

68eb88c0  4000099       10        System.String  0   shared   static Empty

>> Domain:Value  003a5698:01e61198 <<

68eb94f0  400009a       14        System.Char[]  0   shared   static WhitespaceChars

>> Domain:Value  003a5698:01e6174c <<

!dumpvc(dv) 命令查看Value类型对象,对应c#的struct对象

2.7.    !dso(DumpStackObjects)

查看栈上对象。

0:000> !dso

OS Thread Id: 0x7c0 (0)

ESP/REG  Object   Name

002aede0 01e61198 System.String

002aede4 01e995c4 System.String    Finished

002aedf8 01e9760c System.Windows.Forms.MouseEventArgs

002aee4c 01e995c4 System.String    Finished

002aee90 01e9760c System.Windows.Forms.MouseEventArgs

002aee94 01e995c4 System.String    Finished

002aee98 01e87bfc System.Windows.Forms.Button

002aeeb8 01e61198 System.String

002aeebc 01e88af0 System.EventHandler

002aeec4 01e69a54 TestCode4AQTime.Form1

002aeed0 01e87bfc System.Windows.Forms.Button

002aeedc 01e9760c System.Windows.Forms.MouseEventArgs

002aeee0 01e87d98 System.ComponentModel.EventHandlerList

2.8.    !dumpheap

查看托管堆内存对象信息。

!dumpheap –stat  只输出统计信息(常用

!dumpheap –type <partial type name> 只输出类型名和给出(部分)类型名称匹配的对象

0:000> !dumpheap -stat

total 4843 objects

Statistics:

MT    Count    TotalSize Class Name

68ebaa5c       36         3776 System.Int32[]

68728c6c       95         5320 System.Configuration.FactoryRecord

68ebb010       32        10224 System.Collections.Hashtable+bucket[]

68eb94f0       41        10544 System.Char[]

68eba468      666        15984 System.Version

68ebb330       17        25528 System.Byte[]

68e94eec      231        41168 System.Object[]

68eb88c0     1102        89616 System.String

Total 4843 objects

2.9.    !objsize

查看对象实际占用内存大小

0:000> !objsize 01e80d5c

sizeof(01e80d5c) =          104 (        0x68) bytes (System.String)

2.10. !gcroot

查找对象引用关系

0:000> !gcroot 01e80d5c

Note: Roots found on stacks may be false positives. Run "!help gcroot" for

more info.

Scan Thread 0 OSTHread 7c0

Scan Thread 2 OSTHread bc4

DOMAIN(003A5698):HANDLE(Pinned):2f13e4:Root:02e65dd8(System.Object[])->

01e6d8cc(System.Configuration.ClientConfigurationSystem)->

01e6f22c(System.Configuration.RuntimeConfigurationRecord)->

01e7481c(System.Collections.Hashtable)->

01e7e760(System.Collections.Hashtable+bucket[])

2.11. !da(DumpArray)

查看数组对象

0:000> !da 02e65dd8

Name: System.Object[]

MethodTable: 68e94eec

EEClass: 68c7a8a0

Size: 4096(0x1000) bytes

Array: Rank 1, Number of elements 1020, Type CLASS

Element Methodtable: 68eb84dc

[0] null

[1] null

[2] 01e88f00

[3] null

[4] null

[5] 01e88ef4

[6] null

[7] null

[8] null

2.12. !threads

列出当前进程中的托管线程,可以查看托管线程和系统线程的对应关系。

0:004> !threads

ThreadCount: 2

UnstartedThread: 0

BackgroundThread: 1

PendingThread: 0

DeadThread: 0

Hosted Runtime: no

PreEmptive   GC Alloc           Lock

ID OSID ThreadOBJ    State     GC       Context       Domain   Count APT Exception

0    1  e6c 0026aeb8      6020 Enabled  018bd010:018bdfe8 00265698     0 STA

2    2  c08 002790f0      b220 Enabled  00000000:00000000 00265698     0 MTA (Finalizer)

2.13. !eeheap

通过!eeheap,可以查看CLR堆的使用情况。

!eeheap –gc:只查看GC堆的输出结果。

!eeheap –loader:查看和AppDomains关联的各种私有堆的输出结果。

0:004> !eeheap -loader

Loader Heap:

--------------------------------------

System Domain: 726ee1f8

LowFrequencyHeap: Size: 0x0(0)bytes.

HighFrequencyHeap: 00232000(8000:1000) Size: 0x1000(4096)bytes.

StubHeap: 0023a000(2000:2000) 00c50000(10000:3000) Size: 0x5000(20480)bytes.

Virtual Call Stub Heap:

IndcellHeap: Size: 0x0(0)bytes.

LookupHeap: Size: 0x0(0)bytes.

ResolveHeap: Size: 0x0(0)bytes.

DispatchHeap: Size: 0x0(0)bytes.

CacheEntryHeap: Size: 0x0(0)bytes.

Total size: 0x6000(24576)bytes

--------------------------------------

Shared Domain: 726edb48

LowFrequencyHeap: 00430000(2000:1000) Size: 0x1000(4096)bytes.

HighFrequencyHeap: 00432000(8000:1000) Size: 0x1000(4096)bytes.

StubHeap: 0043a000(2000:1000) Size: 0x1000(4096)bytes.

Virtual Call Stub Heap:

IndcellHeap: 00480000(2000:1000) Size: 0x1000(4096)bytes.

LookupHeap: 00485000(2000:1000) Size: 0x1000(4096)bytes.

ResolveHeap: 0048b000(5000:1000) Size: 0x1000(4096)bytes.

DispatchHeap: 00487000(4000:1000) Size: 0x1000(4096)bytes.

CacheEntryHeap: 00482000(3000:1000) Size: 0x1000(4096)bytes.

Total size: 0x7000(28672)bytes

--------------------------------------

Domain 1: 265698

LowFrequencyHeap: 00410000(2000:2000) 006c0000(10000:1000) Size: 0x3000(12288)bytes.

HighFrequencyHeap: 00412000(8000:5000) Size: 0x5000(20480)bytes.

StubHeap: 0041a000(2000:1000) Size: 0x1000(4096)bytes.

Virtual Call Stub Heap:

IndcellHeap: 00420000(2000:1000) Size: 0x1000(4096)bytes.

LookupHeap: 00426000(1000:1000) Size: 0x1000(4096)bytes.

ResolveHeap: 0042a000(6000:2000) Size: 0x2000(8192)bytes.

DispatchHeap: 00427000(3000:1000) Size: 0x1000(4096)bytes.

CacheEntryHeap: 00422000(4000:1000) Size: 0x1000(4096)bytes.

Total size: 0xe000(57344)bytes

--------------------------------------

Jit code heap:

LoaderCodeHeap: 004e0000(10000:1000) Size: 0x1000(4096)bytes.

Total size: 0x1000(4096)bytes

--------------------------------------

Module Thunk heaps:

Module 72741000: Size: 0x0(0)bytes.

Module 00432358: Size: 0x0(0)bytes.

Module 00432010: Size: 0x0(0)bytes.

Module 00412c5c: Size: 0x0(0)bytes.

Module 66701000: Size: 0x0(0)bytes.

Module 672e1000: Size: 0x0(0)bytes.

Module 67f61000: Size: 0x0(0)bytes.

Module 65ab1000: Size: 0x0(0)bytes.

Module 65571000: Size: 0x0(0)bytes.

Total size: 0x0(0)bytes

--------------------------------------

Module Lookup Table heaps:

Module 72741000: Size: 0x0(0)bytes.

Module 00432358: Size: 0x0(0)bytes.

Module 00432010: Size: 0x0(0)bytes.

Module 00412c5c: Size: 0x0(0)bytes.

Module 66701000: Size: 0x0(0)bytes.

Module 672e1000: Size: 0x0(0)bytes.

Module 67f61000: Size: 0x0(0)bytes.

Module 65ab1000: Size: 0x0(0)bytes.

Module 65571000: Size: 0x0(0)bytes.

Total size: 0x0(0)bytes

--------------------------------------

Total LoaderHeap size: 0x1c000(114688)bytes

=======================================

0:004> !eeheap -gc

Number of GC Heaps: 1

generation 0 starts at 0x01881018

generation 1 starts at 0x0188100c

generation 2 starts at 0x01881000

ephemeral segment allocation context: none

segment    begin allocated     size

01880000 01881000  018bdff4 0x0003cff4(249844)

Large object heap starts at 0x02881000

segment    begin allocated     size

02880000 02881000  02886de8 0x00005de8(24040)

Total Size   0x42ddc(273884)

------------------------------

GC Heap Size   0x42ddc(273884)

2.14. 转储进程

直接使用微软提供的aplus.vbs脚本转储w3wp.exe进程

Cscript adplus.vbs –hang –pn w3wp.exe –o <Dump存储目录> -quiet –do

使用Windbg命令转储进程

.dump /f <Dump存储文件>

第三章 常见问题

3.1. OutOfMemory

解决思路:

使用!dumpheap –stat命令检查托管堆对象。

使用gcroot查找数量/大小不正常对象的引用链,以明确对象没被GC回收的原因

3.2. 系统缓慢&HighCPU&单点效率异常

解决思路:

使用!clrstack命令,多次抓取系统调用栈。

比较调用栈停留位置,找到可疑处。

通过!do&!dso查看栈变量,判断代码调用栈停留位置效率低下原因。

第四章 应用示例

示例1: 缓存对象

1. 检查缓存大小

!dumpheap –stat –type System.Web.Caching.Cache  //得到了 System.Web.Caching.Cache 对象的方法表

!dumpheap –mt 1230494c     //得到1230494c方法表中所有对象

!objsize 03392d20       //得到当前地址对应对象的大小

2. 什么内容被缓存了?

!dumpheap –stat –type System.Web.Caching   //查看 CacheEntrys 对象

!dumpheap -mt 12306320  //查看CacheEntrys的方法表MT

!do 076b42dc          //检查对象的所有内容, 之后

示例2: 挂起

检查堆栈信息

检查本地的堆栈信息:

~* kb 2000

检查 dotnet 堆栈信息:

~* e!clrstack                   // Do you see any patterns or recognize any of the callstacks that suggests a thread is waiting for a synchronization mechanism?

看看有多少调用堆栈里有Monitor.Enter:

.shell -ci "~* e !clrstack" FIND /C Monitor.Enter

跟踪诊断挂起现象

检查等待锁的线程ID列表:

!syncblk                    //(提示:MonitorHeld = 1 代表拥有者,2为等待者)

查看一个等待线程的状态:

~5s         (切换到线程 5,用真实的线程ID替换5即可)
kb 2000   (检查本地堆栈信息)
!clrstack  (查看dotnet 堆栈信息)

!clrstack –p (查看dotnet 堆栈信息,包括参数内存地址)

!clrstack –a

示例3: 查看缓存占用情况

查看Cache占用内存情况:

!name2ee System.Web.dll System.Web.Caching.Cache

0:000> !name2ee System.Web.dll System.Web.Caching.Cache

Module: 65f21000 (System.Web.dll)

Token: 0x020000fa

MethodTable: 66148d24

EEClass: 65f86838

Name: System.Web.Caching.Cache

!dumpheap –mt [MethodTable]  //查看托管堆中对象类型

!objsize 06952248  //查看对象大小

示例4: 内存调试

首先看看GC heap的大小,和dump 文件比较一下。

这个 !eeheap 命令列出了GC 堆的大小 和G0,G1,G2 ,LOH的开始地址。

0:001> !eeheap –gc

generation 0 starts at 0x0110be64

generation 1 starts at 0x01109cd8

generation 2 starts at 0x01021028

segment    begin allocated     size

01020000 01021028 0110de70 000ece48(970312)

Total Size   0xece48(970312)

------------------------------

large block 0x11e1fc04(300022788)

large_np_objects start at 17b90008

large_p_objects start at 02020008

------------------------------

GC Heap Size 0x11f0ca4c(300993100)

在这里GC 堆是300M左右,dump文件是358M。

使用 !dumpheap –stat 命令来查看占用了空间的托管对象。

0:001> !dumpheap -stat

Bad MethodTable for Obj at 0110d2a4

Last good object: 0110d280

total 14459 objects

Statistics:

MT    Count TotalSize Class Name

3c6185c        1        12 System.Web.UI.ValidatorCollection

3c2e110        1        12 System.Web.Configuration.MachineKeyConfigHandler

3c29778        1        12 System.Web.Configuration.HttpCapabilitiesSectionHandler

3c23240        1        12 System.Web.SafeStringResource

… …

D12f28      1133     46052 System.Object[]

153cb0        88     76216 Free

321b278       85    178972 System.Byte[]

d141b0      6612    416720 System.String

Total 14459 objects

使用!gcroot 16220018从LOH上得到更多对象的信息:

0:001> !gcroot 16220018

Scan Thread 1 (4e8)

Scan Thread 5 (bb0)

Scan Thread 6 (d0)

Scan Thread 10 (43c)

Scan Thread 11 (308)

Scan Thread 12 (6e4)

Scan HandleTable 14e340

Scan HandleTable 150e40

Scan HandleTable 1a6fa8

HANDLE(Strong):37411d8:Root:020784d8(System.Object[])-

>0108b504(System.Web.HttpRuntime)->0108b9d0(System.Web.Caching.CacheSingle)-

>0108ca68(System.Web.Caching.CacheUsage)->0108ca78(System.Object[])-

>0108cb3c(System.Web.Caching.UsageBucket)-

>010f95fc(System.Web.Caching.UsageEntry[])-

>01109be8 (System.Web.Caching.CacheEntry)->00000000()

要找出System.Web.Caching.Cache的地址,请使用 !name2ee命令,这个命令接受2个参数 程序集的名字和全类名:

0:001> !name2ee System.Web.dll System.Web.Caching.Cache

--------------------------------------

MethodTable: 03887998

EEClass: 03768814

Name: System.Web.Caching.Cache

--------------------------------------

EEClass 是一个用来表示.net 类的内部结构。

取得托管堆中的某个对象的类型,使用 !dumpheap –mt MethodTable地址 的方式来获得:

0:001> !dumpheap -mt 03887998

Address       MT     Size

0108b8ac 03887998       12

Bad MethodTable for Obj at 0110d2a4

Last good object: 0110d280

total 1 objects

Statistics:

MT    Count TotalSize Class Name

3887998        1        12 System.Web.Caching.Cache

Total 1 objects

large objects

Address       MT     Size

total 0 large objects

查看 System.Web.Caching.Cache 的大小,使用 !objsize 0108b8ac:

0:001> !objsize 0108b8ac

sizeof(0108b8ac) = 300126128 (0x11e38fb0) bytes (System.Web.Caching.Cache)

修正代码,把缓存移除。重新装载 dump文件,加载模块,使用 !eeheap –gc 来看看托管堆的大小:

0:000> !eeheap -gc

generation 0 starts at 0x012cc0e4

generation 1 starts at 0x012afde8

generation 2 starts at 0x011c1028

segment    begin allocated     size

011c0000 011c1028 012d6000 00114fd8(1134552)

Total Size 0x114fd8(1134552)

------------------------------

large block 0x8060(32864)

large_np_objects start at 00000000

large_p_objects start at 021c0008

------------------------------

GC Heap Size 0x11d038(1167416)

dump 显示 GC 堆的大小是1M,不是222M,这表示除了1M其它的都被收集了。

示例5: 内存泄露跟踪

I.查看内存使用概要:

!address -summary

0:000> !address -summary

--------------------   Usage SUMMARY --------------------------

TotSize (      KB)     Pct(Tots) Pct(Busy)   Usage

373b7000 (    904924) : 21.58%    85.85%    : RegionUsageIsVAD

bfa89000 ( 3140132) : 74.87%    00.00%      : RegionUsageFree

76e6000 (    121752) : 02.90%    11.55%    : RegionUsageImage

67c000 (    6640) : 00.16%    00.63%      : RegionUsageStack

0 (       0) : 00.00%    00.00%      : RegionUsageTeb

144a000 (   20776) : 00.50%    01.97%      : RegionUsageHeap

0 (       0) : 00.00%    00.00%      : RegionUsagePageHeap

1000 (       4) : 00.00%    00.00%      : RegionUsagePeb

1000 (       4) : 00.00%    00.00%      : RegionUsageProcessParametrs

2000 (       8) : 00.00%    00.00%      : RegionUsageEnvironmentBlock

Tot: ffff0000 (4194240 KB) Busy:   40567000 (1054108 KB)

--------------------   Type SUMMARY --------------------------

TotSize (      KB)     Pct(Tots)  Usage

bfa89000 ( 3140132) : 74.87%   :

834e000 (    134456) : 03.21%   : MEM_IMAGE  [总共内存占用百分比]

95a000 (    9576) : 00.23%   : MEM_MAPPED

378bf000 (    910076) : 21.70%   : MEM_PRIVATE

--------------------   State SUMMARY --------------------------

TotSize (      KB)     Pct(Tots)  Usage

34bea000 (    864168) : 20.60%   : MEM_COMMIT

bfa89000 ( 3140132) : 74.87%   : MEM_FREE

b97d000 (    189940) : 04.53%   : MEM_RESERVE

Largest free   region: Base 80010000 - Size 7fefa000 (2096104 KB)

这里有非常多的信息,但所有的这些都不是这么明显的。相信我,让我们花一些时间在这里。在任何一个案例中,我用这个来帮助我指出我需要查看哪些地方,所以我不介意它要花费多少,即使就是一个概述的结果。

一些要注意的地方:

上面一屏显示了按照类型不同而分类显示的由进程使用的内存。第一部分是按照区域类型来划分的,它按照什么样子的分配类型告诉你信息。最常遇到的一个类型是VAD = Virtual Alloc, Image = dlls 和 exes,Heap = heaps the process owns,从WinDbg的帮助中可以得到更多的信息。接着下面是按IMAGE, MAPPED  或 PRIVATE 的类型来列出,最后一部分是按已提交(also committed,就是指实际已经分配的)或保留(reserved)的方式来列出它们。

RegionUsageheap,代表的是NT heaps;MEM_COMMIT和MEM_RESERVE加起来,是virtual   memory。

Tot: ffff0000 (4   194 240 kb) :的意思是我总共有4GB的虚拟内存地址空间提供给这个应用程序。32位系统上,你可以寻地4GB的空间,典型的是2GB的用户模式的内存空间,所以一般你会看到2GB而不是这里的4GB,在64位上,运行一个32位的进程会得到完全的4GB的空间,所以我这里看到的是4GB。

Busy: 40567000 (1   054 108 kb)  是我们已经使用的(已经分配的)。

MEM_PRIVATE是一个私有的内存,它不和其他进程共享内存,不是映射到文件的内存。不要把这个和性能计数器中的Private Bytes混淆。这里的MEM_PRIVATE 是保留+已提交(即已分配的)(reserved + committed)的字节数,另外那个Private   Bytes 是申请/已提交(allocated/committed)的字节数。

MEM_PRIVATE 是已经提交(已经分配)的内存(不一定是 private的),这个可能是最接近你得到的Private Bytes的。

MEM_RESERVE 是已经保留的,但没有实际分配的,未提交的内存。所有已经分配的内存也是定义为保留的,所以如果你查看所有保留的内存(最接近你得到的virtual bytes),你必须加上MEM_COMMIT和   MEM_RESERVE,它是显示在Busy 中的那个数字。你自己把数字加上后比对一下看看。

Q:哪个值最能代表如下两个指标?

·Private Bytes           A: MEM_COMMIT

·Virtual Bytes           A: Busy

Q:大部分的内存都去哪里了?(哪个区域)

A:在这里,大约904MB是为VAD保留的,VAD是dotnet对象存放的地方,因为GC堆是virtual   allocs 分配的。

Q:Busy,Pct(Busy),Pct(Tots)是什么意思?

A:Pct(Tots) 显示的是整个虚拟地址空间中分配给不同区域类型的百分比。Pct(Busy)显示的是保留的内存中分配给不同区域的百分比。Pct(busy) 很显然是我最关心的一个。

Q:MEM_IMAGE 是什么意思?

A:从帮助文件中我们知道:这个是表示从一个可执行的映射文件的一部分映射到的内存。换句话说 就是dll 或一个exe 文件的内存映射。

Q:哪个区域的.net 内存是适宜的,为什么?

A:在RegionUsageIsVAD,理由如上。

从性能计数器中我们看到#Bytes   in all Heaps 跟随着Private bytes的增长而增长,那说明了内存的增加几乎都是.net 的使用而增加的,进而我们转化为为什么.net 的GC堆(heap)始终在增长。

II.运行 !eeheap –gc 来查看.net GC 堆的大小

0:000> !eeheap   -gc

Number of GC   Heaps: 2

------------------------------

Heap 0 (001aa148)

generation 0   starts at 0x32f0639c

generation 1   starts at 0x32ae3754

generation 2   starts at 0x02eb0038

ephemeral segment   allocation context: none

segment      begin allocated     size

001bfe10   7a733370  7a754b98 0x00021828(137256)

001b0f10   790d8620  790f7d8c 0x0001f76c(128876)

…..

Large object heap   starts at 0x0aeb0038

segment      begin allocated     size

0aeb0000 0aeb0038  0aec0b28 0x00010af0(68336)

Heap Size  0x15fd1310(368907024)

------------------------------

Heap 1 (001ab108)

generation 0   starts at 0x36e665bc

generation 1   starts at 0x36a28044

generation 2   starts at 0x06eb0038

ephemeral segment   allocation context: none

segment      begin allocated     size

06eb0000   06eb0038  0aea58d4 0x03ff589c(67066012)

……

Large object heap   starts at 0x0ceb0038

segment      begin allocated     size

0ceb0000   0ceb0038  0ceb0048 0x00000010(16)

Heap Size  0x15ab1570(363533680)

------------------------------

GC Heap Size  0x2ba82880(732440704)

Q:总共有多少个Heap,为什么?

A:这里有两个堆,因为我们运行在多核进程模型中。

Q:有多少内存被保存在了.net GC 堆中?拿#Bytes   in all Heaps比较一下。

A:GC的堆大小是:GC Heap Size 0x2ba82880(732 440 704),它和性能计数器中的bytes in all heaps很接近。

Q:large object heap 上有多少内存?提示:把large   object heap段上的合计加起来,和性能计数器中的Large Object Heap Size 比较一下。

A:它是非常小的,所以LOH看起来不是问题所在,大小是68 336 + 16 bytes

III. 运行 !dumpheap –stat 来输出所有的以统计式样表示的.net 对象

Q:查看 5 到10个使用了大部分内存的对象,思考一下是什么泄露了?

A:

66424cf4       37        57276 System.Web.Caching.ExpiresEntry[]

663b0cdc     4001       192048   System.Web.SessionState.InProcSessionState

7912d8f8     3784       255028 System.Object[]

7912d9bc      820       273384   System.Collections.Hashtable+bucket[]

6639e4c0     4037       290664 System.Web.Caching.CacheEntry

0fe11cf4    36000       576000 Link

790fdc5c    36161       723220 System.Text.StringBuilder

001a90c0     1105        7413924      Free

790fd8c4    51311      721773112 System.String

Total 163943   objects

大部分的内存是被strings用掉了,这个不太正常,虽然strings 在应用中是最常见的,但是大约721MB的显然有点怪异,并且有3600个Links(无论它们是什么),看起来有点奇怪。特别是因为有差不多数量的stringbuilds 出现在dump中。

Q:“size”那个行显示了什么?例如,“size”这行包含了什么?

A:如果我们用命令!do 把Link 对象输出来,我们看到有一个指针指向stringbuilder(url)和一个指针指向string(name),link对象的大小是16B,这个大小仅仅包含了指针的大小和其他一些开销(methos table 等)。

如果你运行 !objsize ,你会看见大小是高达20144   B ,这个大小是包含成员变量的,比如Link对象的大小和它引用的所有对象。

你看到了什么通过!dumpheap 输出的16B的每一个link。它不包含成员变量的大小是由一些不同原因的:

1)它将要花费很长的时间去计算大小。

2) 一些对象(假如是A和B)可能都指向C对象,如果你使用 !objsize 计算A 和B的大小,他们都会包含C的大小,所以size这个列的值会变得很复杂难以计算。

3) 在这个例子中Link的大小size看起来似乎是正常的。因为一个link对象包含一个url和一个name。但是如果一个web 控件可能会包含一个成员变量 _parent ,如果你运运行   !objsize ,这样就会包含父对象(page)那就显然是不合适的。

0:000> !do   371d44cc

Name: Link

MethodTable:   0fe11cf4

EEClass: 0fde5824

Size: 16(0x10)   bytes

(C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\

Temporary ASP.NET   Files\buggybits\b27906cc\f5f91899\App_Code.wbwztx_4.dll)

Fields:

MT      Field   Offset                 Type VT     Attr      Value Name

790fdc5c  4000006        4 ...ext.StringBuilder  0 instance 371d44dc url

790fd8c4  4000007        8        System.String  0 instance 02f13cd8 name

0:000>   !objsize 371d44cc

sizeof(371d44cc)   =        20144 (      0x4eb0) bytes (Link)

通常,我不推荐立刻查看在你的这个非常简单的dump文件中,在该命令输出的底部的strings,因为:

· strings 这一行的“size”是实际的字符串string的有内容的真实大小。如果你和DataSet比较,这个“size”只是包含了行和列的指针,并没有包含行和列的内存。所以DataSet这个对象的大小几乎总是非常的小的。

· string 字符串在大部分的对象中几乎是叶子节点,例如,dataset包含字符串,aspx页面包含字符串,session 变量也包含字符串。所以,在一个应用中几乎都是字符串。

然而在这个例子中,字符串有这么多,占有了那么多的内存。如果我们不查到其他一些阻止了我们的东西,那我们可能就要沿着string 这条路走下去了。

IV. 把各种不同大小的string 都输出来

得到string的 MT(method table),!dumpheap –stat 的输出结果的第一列。

!dumpheap -mt <string MT> -min 85000 -stat

!dumpheap -mt <string MT> -min 10000 -stat

!dumpheap -mt <string MT> -min 20000 -stat

!dumpheap -mt <string MT> -min 30000 -stat

!dumpheap -mt <string MT> -min 25000 -stat

Q:大部分的string’在一个什么样的范围内?

A:在 20000 和 25000 字节之间。

V. 把那个范围内的string 输出来。

!dumpheap -mt <string MT> -min 20000 -max 25000

在这里,它们中的大部分是一模一样的大小的,这是一个指引我们向下前进的线索。

0:000> !dumpheap -mt 790fd8c4 -min 20000 -max 25000

------------------------------

Heap 0

Address       MT     Size

02f1412c   790fd8c4    20020

02f2d96c   790fd8c4    20020

02f327c4   790fd8c4    20020

02f3761c   790fd8c4    20020

02f3c474   790fd8c4    20020

02f412cc   790fd8c4    20020

02f46124   790fd8c4    20020

02f4af7c   790fd8c4    20020

02f4fdd4   790fd8c4    20020

02f54c2c   790fd8c4    20020

...

VI.把它们中的一些输出来看看里面是什么

!do <address of string>  ,地址是 !dumpheap -mt 输出的第一列。

0:000> !do 02f327c4

...

String: http://www.sula.cn

...

Q:这些string里面包含的是什么?

A:好像link.aspx 页面显示了link对象。

VII. 拣几个,看看它们被根化(rooted)到哪里(即为什么它们不会被回收)。注意你可能需要尝试不同的几个才行.

!gcroot <address of string>

0:000> !gcroot 02f327c4

Note: Roots found on stacks may be false positives. Run "!help gcroot" for

more info.

Scan Thread 16 OSTHread 1948

Scan Thread 20 OSTHread 1b94

Scan Thread 21 OSTHread 1924

Scan Thread 22 OSTHread 188c

Scan Thread 14 OSTHread 1120

Scan Thread 24 OSTHread 13f8

Finalizer queue:Root:02f327a0(Link)->

02f327b0(System.Text.StringBuilder)->

02f327c4(System.String)

Q:它们被根化到哪里?为什么?

A:这个string是一个string builder 类型的成员变量,它表现的是一个link的成员变量(url),link 对象被根化在终结器队列中,那就是说他正在等待被终结。

检查终结器队列(finalizer queue)和终结线程(finalizer thread

1)查看终结器队列

!finalizequeue

0:000> !finalizequeue

SyncBlocks to be   cleaned up: 0

MTA Interfaces to   be released: 0

STA Interfaces to   be released: 0

----------------------------------

------------------------------

Heap 0

generation 0 has   221 finalizable objects (0f44a764->0f44aad8)

generation 1 has   0 finalizable objects (0f44a764->0f44a764)

generation 2 has   45 finalizable objects (0f44a6b0->0f44a764)

Ready for   finalization 18009 objects (0f44aad8->0f45c43c)

------------------------------

Heap 1

generation 0 has   338 finalizable objects (0f45d840->0f45dd88)

generation 1 has   4 finalizable objects (0f45d830->0f45d840)

generation 2 has   36 finalizable objects (0f45d7a0->0f45d830)

Ready for   finalization 17707 objects (0f45dd88->0f46f234)

Statistics:

MT      Count    TotalSize Class Name

663a1fc8        1           12   System.Web.Configuration.ImpersonateTokenRef

79116758        1           20   Microsoft.Win32.SafeHandles.SafeTokenHandle

791037c0        1           20   Microsoft.Win32.SafeHandles.SafeFileMappingHandle

79103764        1             20 Microsoft.Win32.SafeHandles.SafeViewOfFileHandle

6639c104        1           20   System.Web.PerfInstanceDataHandle

663f6b5c        1           28   System.Web.Security.FileSecurityDescriptorWrapper

663a105c        1           32 System.Web.Compilation.CompilationMutex

7910b630        2           40   System.Security.Cryptography.SafeProvHandle

79112728        5          100   Microsoft.Win32.SafeHandles.SafeWaitHandle

790fe704        2          112 System.Threading.Thread

7910a5c4        2          120   System.Runtime.Remoting.Contexts

...

Q:在这个命令的输出中列出了什么对象?

A:所有具有终结/析构器的都被注册到终结器队列中,当对象被垃圾收集时,终结器会运行析构函数,否则在dispose函数中终结过程会挂起。

Q:有多少个对象是出于“ready for finalization”,它是什么意思?

A:大约有36000个,这些对象是要被垃圾收集的,正在等待被终结。如果ready for finalization大于0 但没有显示任何信息,这是一个说明终结器线程被堵塞的最好时机。所以这些对象被堵住了等待终结,他们消耗了大部分的内存。

2)  找出终结线程,了解它正在干什么,运行!threads ,在列出的线程中查找带有“(Finalizer)”的线程。

3)  切换到终结线程,检查托管的和本地(原生)的调用堆栈。

~5s   (把5 替换成真实的终结线程(finalizer thread)的ID号)

kb 2000

!clrstack

0:000>   !threads

...

20      2 1b94 001ac2c0   200b220   Enabled  00000000:00000000   001ccc80     0 MTA (Finalizer)

...

0:020>   !clrstack

OS Thread Id:   0x1b94 (20)

ESP       EIP

02a0f8fc 7d61cca8   [HelperMethodFrame: 02a0f8fc] System.Threading.Thread.SleepInternal(Int32)

02a0f950 0fe90ce8   Link.Finalize()

02a0fc1c 79fbcca7   [ContextTransitionFrame: 02a0fc1c]

02a0fcec 79fbcca7   [GCFrame: 02a0fcec]

Q:什么对象正在被终结?

A:看起来是一个link 对象。

Q:它正在干什么? 为什么这个会导致高内存使用率?

A:终结link对象的终结器线程因为sleep 被堵住了。意味着终结器被堵住,进程中没有东西可以被终结。因而等待终结的进程都会仍然在内存中直到终结器醒来它们被终结为止。

示例6: 线程状态

!threads命令看看当前CLR中有哪些线程正在执行

以下为引用:


0:004> !threads
ThreadCount: 2
UnstartedThread: 0
BackgroundThread: 1
PendingThread: 0
DeadThread: 0
                             PreEmptive   GC Alloc               Lock
       ID ThreadOBJ    State     GC       Context       Domain   Count APT Exception
  0   6ec 0014e708      6020 Enabled  00000000:00000000 00148a90     0 STA
  2   a68 00157618      b220 Enabled  00000000:00000000 00148a90     0 MTA (Finalizer)

前面5个计数器分别表示托管(managed)线程、未启动线程、后台线程、阻塞线程和僵死线程的数量。
    下面的列表是当前托管线程的详细信息:第一个域是WinDbg的线程编号;ID是Win32线程ID;ThreadObj是线程的对象;State是一个标志位,以后再详细介绍;PreEmptive GC表示GC是否与此线程协作;GC Alloc Context是GC的相关信息;Domain是线程所在AppDomain;Lock Count是线程拥有锁的计数器;APT是线程类型,沿用COM中STA/MTA/NTA(netural)的概念;最后的Exception表示线程类型,除了普通的用户线程外还有finalizer、GC、Theadpool Worker和Threadpool Completion Port,其功能与名字相符.

示例7: 调试.net代码

1. !name2ee SimpleSample.exe SimpleSample.Program.Main

显示方法相关地址

0:004> !name2ee SimpleSample.exe SimpleSample.Program.Main

Module: 00982c5c (SimpleSample.exe)

Token: 0x06000005

MethodDesc: 00983000

Name: SimpleSample.Program.Main()

JITTED Code Address: 01220070

2. !dumpil 00983000

显示方法被C#编译器编译之后的IL代码

0:004> !dumpil 00983000

ilAddr = 004020c4

IL_0000: nop

IL_0001: ldstr "Any key continue... ... "

IL_0006: call System.Console::WriteLine

IL_000b: nop

IL_000c: call System.Console::Read

IL_0011: pop

IL_0012: call SimpleSample.Program::getcharBuffer

IL_0017: stloc.0

IL_0018: ldloc.0

IL_0019: call SimpleSample.Program::changeto4p

IL_001e: nop

IL_001f: ldloc.0

IL_0020: call System.Console::WriteLine

IL_0025: nop

IL_0026: call System.Console::Read

IL_002b: pop

IL_002c: call System.Console::Read

IL_0031: pop

IL_0032: ret

3. !u 01220070

显示JIT编译了的方法的本地代码

Other:

!dumpmt -md 00983024       //得到类的成员函数详细信息

!dumpheap -stat              //显示程序中所有对象的统计信息

!dumpheap -mt 00983024     //该命令显示MethodTable的详细信息

!gcroot 012919b8            //来显示一个实例的所属关系

!dumpobj(do) 012a3904      //显示一个对象的具体内容

!ObjSize 012a1ba4          //对象实际在内存中的大小

!DumpArray

//查看数组信息 (http://www.pin5i.com/showtopic-15919.html)

!dumpheap -type Exception   //查看异常信息

示例8: 查看方法代码

!ip2md 05600dfd   --05600dfd: 表示EIP

    MethodDesc: 02429048

    Method Name: DataLayer.GetFeaturedProducts()

    Class: 055b18ac

    MethodTable: 0242905c

    mdToken: 06000008

    Module: 024285cc

    IsJitted: yes

    m_CodeOrIL: 05600dd0

I.根据md来看:!dumpil 02429048 (这个地址是上面步骤f中的输出的第一行

MethodDesc的值)

II. 根据native code来看:!u 05600dd0 (这个地址是上面步骤f中的输出的最后一行的m_CodeOrIL的值)

III.根据module来看:!dumpmodule 024285cc (这个地址是上面步骤分钟的输出的

倒数第三行的Module的值)

附: WinDbg / SOS Cheat Sheet

Environment

Attach to   process

F6

Detach from a   process

.detach

Break debugger   execution

Ctrl-Break

Continue   debugger execution

g

Exit WinDbg

q

Clear the screen

.cls

Getting   Help

Debugger commands

?

Debugger   commands

.help

Online help file

.hh command

Help on   extension on top of chain

!help

Help on specific   extension command

!help command

Issuing   Commands

Scroll through   command history

[up], [down],   [enter]

Paste into   command window

[right-click]

Examining   the Unmanaged Environment

List loaded   modules with full path

lmf

List loaded   modules with last modified timestamp

lmt

List unmanaged   threads

~

Select active   thread

~thread_id s

View call stack

k

View thread CPU   consumption

!runaway

Set a breakpoint

bp

Dump small   memory image

.dump path

Dump large   memory image

.dump /ma path

Loading   SOS

Load SOS for   .NET 1.x

.load clr10\sos

Load SOS for   .NET 2.0

.loadby sos   mscorwks

Examining   the Managed Environment

Dump runtime   type information

!dumpruntimetypes

View managed   threads

!threads

View managed   call stack

!clrstack

View combined   managed / unmanaged callstack

!dumpstack

View function   call arguments

!clrstack –p

View local   variables

!clrstack –l

View object dump

!do address

View array dump

!da address

View object size   (including children)

!objsize address

View heap usage   by type

!dumpheap -stat

View heap usage   filtered by type

!dumpheap -type   type

View GC roots of   object instance

!gcroot address

View managed   sync blocks

!syncblk

View managed   thinlocks (CLR 2.0)

!dumpheap   –thinlock

View information   on most recent exception

!printexception

Set a breakpoint

!bpmd module   method

Type

Explanation

ESP

ESP=Extended Stack Pointer, Object is in   use on a stack

DOMAIN(001CCE68):HANDLE(Strong)

Strong reference, Typically a static   variable

DOMAIN(001CCE68):HANDLE(WeakLn)

Weak Long Handle, A weak reference that   is tracked through finalization (can be resurrected)

DOMAIN(001CCE68):HANDLE(WeakSh)

Weak Short Handle, A weak reference,   can't be resurrected

DOMAIN(001CCE68):HANDLE(Pinned)

Pinned object, pinned at a specific   address, can't move around during garbage collection.

DOMAIN(001CCE68):HANDLE(RefCnt)

Reference count, referenced as long   as the reference count is > 0.

第五章 sos.dll 扩展命令

命令

描述

BPMD [<module name>   <method name>] [-md <MethodDesc>]

建立一个断点在指定模块的指定方法上。

如果指定模块和方法尚未被载入,该命令等到该模块被载入并且被即时(just-in-time)编译的通知后再建立断点。

CLRStack [-a] [-l]   [-p]

只提供托管代码的栈跟踪。

-p 选项显示托管函数的参数。

-l 选项显示在一个框架里局部变量的信息。SOS调试扩展无法检索局部变量的名字,所以局部变量的输出格式为<local   address> = <value>。

-a (all) 选项是-l-p组合的快捷方式。

在x64和基于IA-64的平台上,SOS调试扩展不显示过渡框架(Transition Frames)。

COMState

列出每个线程COM单元模型和可用的上下文指针。

DumpArray [-start   <startIndex>] [-length <length>] [-details] [-nofields]   <array object address>

-或者-

DA [-start <startIndex>]   [-length <length>] [-detail] [-nofields] <array   object address>

检查一个数组对象的元素。

-start 选项指定显示元素的起始索引号。

-length 选项指定要显示的元素数目。

-detail 选项按照DumpObjDumpVC格式显示元素的细节。

-nofields 选项使数组显示不包括字段。仅当指定   -detail 选项时该选项才可用。

DumpAssembly <Assembly   address>

显示一个汇编集的有关信息。

如果存在多个模块,DumpAssembly命令将它们全部列出。

你可以用DumpDomain命令得到汇编集地址。

DumpClass <EEClass   address>

显示与一个类型相关的EEClass结构这些信息。

DumpClass命令显示静态字段值而不显示非静态字段值。

使用DumpMTDumpObjName2EE、或Token2EE命令来获取一个EEClass结构地址。

DumpDomain [<Domain   address>]

枚举在指定AppDomain对象地址里面装载的每一个Assembly对象。当不带参数调用DumpDomain命令时,它列出一个进程中所有的AppDomain对象。

DumpHeap [-stat] [-min   <size>][-max <size>] [-thinlock] [-mt   <MethodTable address>] [-type <partial type name>][start   [end]]

显示关于垃圾收集堆的信息和有关对象的收集统计。

DumpHeap命令如果在垃圾收集器堆中检测到过多的碎片,它显示一个警告。

-stat 选项限制输出内容只有统计的类型摘要。

-min 选项忽略那些尺寸小于size参数的对象,以字节为单位。

-max 选项忽略那些尺寸大于size参数的对象,以字节为单位。

-thinlock 选项报告ThinLocks。更多信息请看SyncBlk命令。

-mt 选项只列出符合所指定MethodTable结构的那些对象。

-type 选项只列出类型名字子串匹配指定字符串的那些对象。

参数 start 指定开始列出的地址。

参数 end 指定停止列出的地址。

DumpIL [<DynamicMethod   address>] [<DynamicMethodDesc address>] [<MethodDesc address>]

显示与一个托管方法相关的中间语言(IL)。

注意,动态IL是发射来的(emitted),不同于从一个汇编集装载的IL。动态IL引用一个托管对象数组中的对象,而不是通过元数据标记引用对象。

DumpLog [<Filename>]

把一个内存里的重要日志的内容写入指定文件。如果你没有指定文件名,该命令在当前目录中创建一个名为Stresslog.txt的文件。

公共语言运行时提供一个内存里的重要日志,帮助你诊断重要失败。日志使你可以不使用锁或I/O就能诊断失败。若要启用重要日志,需要在HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework下面设置以下注册表项:

(DWORD) StressLog = 1

(DWORD) LogFacility = 0xffffffff

(DWORD) StressLogSize = 65536

DumpMD <MethodDesc   address>

显示的信息是在指定地址上的一个MethodDesc结构。

你可以用IP2MD命令得到一个托管函数的MethodDesc结构地址。

DumpMT [-MD]   <MethodTable address>

显示在指定地址上的一个方法表的有关信息。指定 -MD   选项显示列出该对象定义的所有方法。

每个托管对象包含有一个方法表指针。

DumpMethodSig <sigaddr>   <moduleaddr>

显示在指定地址上的一个MethodSig结构的有关信息。

DumpModule [-mt]   <Module address>

显示在指定地址上的一个模块的有关信息。-mt 选项显示在该模块中所定义的类型和被该模块引用的类型。

你可以用DumpDomainDumpAssembly命令检索一个模块的地址。

DumpObj <object   address>

-或者-

DO <object address>

显示在指定地址上的一个对象的有关信息。DumpObj命令显示字段、EEClass结构信息、方法表和该对象的尺寸。

你可以用DumpStackObjects命令检索一个对象的地址。

注意,因为类型CLASS的字段也是对象,所以你可以对它们执行DumpObj命令。

DumpRuntimeTypes

显示在垃圾收集器堆中的运行时类型对象,并列出与它们相关的类型名字和方法表。

DumpStack [-EE]   [top stack [bottom stack]]

显示一个栈跟踪(回溯)。

-EE 选项使DumpStack命令只显示托管函数。在x86平台上使用topbottom参数限制所显示的栈框架。

在x86平台上,DumpStack命令创建一个冗长的栈跟踪。

在x64和基于IA-64的平台上,DumpStack命令模仿调试器的 K 命令。在x64和基于IA-64的平台上topbottom参数被忽略。

DumpSig <sigaddr>   <moduleaddr>

显示在指定地址上的一个Sig结构的有关信息。

DumpStackObjects [-verify]   [top stack [bottom stack]]

-或者-

DSO [-verify] [top   stack [bottom stack]]

显示在当前栈范围内找到的所有托管对象。

-verify 选项验证对象字段的每一个非静态CLASS字段。

带有栈跟踪命令使用DumpStackObject命令,比如 K 命令和CLRStack命令确定局部变量和参数的值。

DumpVC <MethodTable   address> <Address>

显示在指定地址上的一个值类的字段信息。

MethodTable参数使DumpVC命令能够正确地解释字段。值类不以方法表作为它们的第一个字段。

EEHeap [-gc] [-loader]

显示被公共语言运行时内部数据结构使用的进程内存的有关信息。

-gc-loader 选项限制该命令的输出内容为垃圾收集器或者装载器的数据结构。

对于垃圾收集器,列出在托管堆里每一个节的范围信息。如果某指针是在EEHeap -gc给出的某个节范围内,那么该指针是一个对象指针。

EEStack [-short] [-EE]

对进程中所有线程执行DumpStack命令。

-EE 选项被直接传递给DumpStack命令。-short 参数限制输入内容为以下线程种类:

  1. 已经被锁定的线程。
  2. 为了允许垃圾收集已经被迟延的线程。
  3. 目前处于托管代码中的线程。

EEVersion

显示公共语言运行时版本。

EHInfo [<MethodDesc   address>] [<Code address>]

显示所指定方法里的异常处理块。这个命令显示子句块(try块)和处理者块(catch块)的代码地址及偏移量。

FinalizeQueue [-detail]

显示为终结(finalization)而登记的所有对象。

-detail 选项显示关于等待清除的任何SyncBlocks的附加信息和等待清除的任何RuntimeCallableWrappers (RCWs) 的额外信息。两个数据结构都是由终结器(finalizer)线程缓存和清除。

FindAppDomain <Object   address>

确定在指定地址上的一个对象的应用程序域。

GCHandles [-perdomain]

显示在进程中垃圾收集器句柄的统计。

如果传递-perdomain 选项,则按照应用程序域顺序排列统计。

使用GCHandles命令查找由垃圾收集器句柄泄漏引起的内存泄漏。例如,由于一个强健的垃圾收集器句柄指向代码的一个大数组成部分,而该句柄没有被释放就丢弃了,所以代码实际上还保留着这个数组,这时就出现一个内存泄漏。

GCHandleLeaks

在内存里搜索进程中对那些强健而且有麻烦的垃圾收集器句柄的任何引用,并且显示结果。如果找到某个句柄,GCHandleLeaks命令显示该引用的地址。如果在内存里没有找到某个句柄,这个命令显示一个通知。

GCInfo <MethodDesc   address><Code address>

显示数据指示何时寄存器或栈位置包含有托管对象。如果发生垃圾收集,收集器必须知道指向对象的引用的位置,如此它才可以用新的对象指针值更新它们。

GCRoot [-nostacks]   <Object address>

显示对在指定地址上的一个对象的引用(或根)信息。

GCRoot命令检查整个托管堆和在栈以及其他对象里面句柄的句柄表。然后,在每个栈和终结器队列中搜索指向对象的指针。

这个命令不确定一个栈根是有效的还是已丢弃的。为了确定栈根是否还在使用中,需要用CLRStackU命令反汇编局部变量或参数值所属的框架。

-nostacks 选项限制只搜索垃圾收集器句柄和终结器队列里的对象(freachable objects)。

help [<command>]   [<faq>]

当没有指定参数时显示所有可用命令,或者当指定命令为参数时显示其详细帮助信息。

faq 参数显示常问问题的答案。

IP2MD <Code address>

显示在已经即时编译(JIT)的代码里指定地址上的MethodDesc结构。

MinidumpMode [0] [1]

防止在使用一个小转储(minidump)时执行非安全命令。

传递 0 以禁用这个功能,或传递 1 以启用这个功能。默认地,MinidumpMode把值设置为 0

.dump /m 命令或者 .dump 命令创建的小转储已经限制为特定的CLR数据,并且让你只可以正确地运行SOS命令的一个子集。有些命令可能因不可预见的错误而失败,因为所必需的内存区域没有被映射或者只有部分被映射。这个选项让你避免对小转储执行非安全命令。

Name2EE <module   name> <type or method name>

-或者-

Name2EE <module   name>!<type or method name>

显示指定模块中指定类型或方法的MethodTable结构和EEClass结构。

指定模块必须被装入进程中。

可以使用MSIL反汇编器 (Ildasm.exe) 浏览模块,以取得适当的类型名字。你也可以传递 * 作为模块名字参数以搜索所有装入的托管模块。模块名字参数也可以是调试器给一个模块的名字,比如mscorlibimage00400000

这个命令支持Windows调试器句法<module>!<type>。该类型必须被完全限定。

ObjSize [<Object   address>]

显示指定对象的尺寸。若不带参数,则ObjSize命令显示在托管线程中找到的全部对象的尺寸,显示进程中全部的垃圾收集器句柄,并求出指向那些句柄的所有对象的尺寸总和。ObjSize命令把父对象全部子对象的尺寸也计算在内。

PrintException [-nested]   [<Exception object address>]

-或者-

PE [-nested]   [<Exception object address>]

编排格式并显示在指定地址上的任何Exception类派生对象的字段。如果你没有指定一个地址,PrintException命令显示当前线程上最近抛出的异常。

-nested 选项详细显示嵌套的异常对象。

你可以使用这个命令编排格式并查看_stackTrace字段,这是一个二元数组。

ProcInfo [-env] [-time]   [-mem]

显示针对该进程的环境变量、内核CPU时间和内存使用统计。

RCWCleanupList   <RCWCleanupList address>

显示在指定地址上的正等待清除的运行时可调用的包裹器列表。

SaveModule <Base   address> <Filename>

把装入在指定地址上的一个内存映像写入指定文件。

StopOnException [-derived]   [-create | -create2] <Exception> <Pseudo-register   number>

使调试器当指定异常被抛出时停止,而当其他异常被抛出时则继续运行。

-derived 选项捕获指定异常及其衍生的每个异常。

SyncBlk [-all |   <syncblk number>]

显示指定的SyncBlock结构或者所有的SyncBlock结构。如果你没有传递任何参数,SyncBlk命令显示一个线程所有对象相应的SyncBlock结构。

一个SyncBlock结构是一个附加信息的容器,不必为每个对象创建它。它能够容纳COM互用数据、散列码、和用于线程-安全操作的锁定信息。

ThreadPool

显示托管线程池的有关信息,包括在队列中工作请求的数目、完全端口线程的数目、和计时器数目。

Token2EE <module name>   <token>

把指定模块中指定的元数据标记转换成一个MethodTable结构或者MethodDesc结构。

你也可以把 * 作为模块名字参数,以使在每个被载入的托管模块中找出该标记的映射目标。模块名字参数也可以是调试器给一个模块的名字,比如 mscorlib image00400000

Threads [-live] [-special]

显示进程中所有的托管线程。

Threads命令显示 调试器简写ID号、公共语言运行时线程ID号、和正在操作中的系统线程ID号。此外,Threads命令显示 一个Domain栏指示线程运行所处在的应用程序域、一个APT栏显示COM单元的模式、和一个Exception栏显示线程最近抛出的异常。

-live 选项显示与某个活线程有关联的那些线程。

-special 选项显示CLR创建的所有特别线程。特别线程包括(并发GC和服务器GC中的)垃圾收集(GC)线程、调试器助手线程、Finalizer线程、AppDomain卸载线程、和线程池计时器线程。

TraverseHeap [-xml]   <filename>

遵照CLR简档器隐含的格式把堆信息写入到指定文件。-xml选项使TraverseHeap命令把该文件格式化为XML。

你能够从: http://www.microsoft.com/downloads/details.aspx?familyid=86ce6052-d7f4-4aeb-9b7a-94635beebdda   下载CLR简档器。

U [-gcinfo] [-ehinfo]   <MethodDesc address> | <Code address>

通过指定一个指向某个方法MethodDesc结构的指针或者指定其方法体里面的一个代码地址,显示一个托管方法有注释的反汇编。U命令显示整个方法,从开始到完成,并在注释里把元数据标记转换为名字。

-gcinfo 选项使U命令显示这个方法使用的GCInfo结构。

-ehinfo 选项显示这个方法的异常信息。你也可以用EHInfo命令来获取该信息。

VerifyHeap

检查垃圾收集器堆的崩溃标志,显示发现的任何错误。

堆崩溃能够由不正确地构成的平台援用(platform invoke)调用引起。

VMMap

横跨虚拟地址空间,显示加诸每区域的保护类型。

VMStat

按照加诸内存的保护类型(*的free、保留的reserved、约束的committed、私有的private、映射的mapped、映像image)顺序,提供虚拟地址空间的概览。TOTAL栏显示AVERAGE栏乘以BLK   COUNT栏的结果。

注:个人总结整理,如有指教问题请liudaoyu@outlook.com 谢谢!