使用 WinDbg 从 EventHandler 获取方法名






4.86/5 (6投票s)
本文介绍如何使用WinDbg从EventHandler获取方法名称。
引言
如果您正在使用WinDbg进行托管调试,我相信您应该非常熟悉!gcroot
命令输出中的这种引用关系。
07b3ebd4(System.EventHandler)->
0375891c(SomeWPFApp.MainWindow)->
SomeWPFApp
中的对象MainWindow
有很多事件,对象07b3ebd4(System.EventHandler)
究竟处理了哪个确切的事件?
我将向您展示如何使用WinDbg来获得答案。
背景
我经常帮助客户使用WinDbg和SQL Server Profiler查找应用程序性能问题。最近,我研究了一些WPF和SilverLight性能问题。在大多数情况下,开发人员忘记从事件中取消引用EventHandler
。因此,最重要的是找到哪个方法处理哪个事件?然后,我们可以简单地使用-=
在某些方法(通常是Dispose
方法)中删除此关系。
循序渐进
这是一个实时调试,而不是事后调试,这意味着我正在使用WinDbg附加我要研究的进程。
加载SOS
首先,这是一个.NET应用程序,所以我应该首先加载SOS扩展。对于不同的.NET Framework版本,我们必须使用不同的加载语法。
- 对于.NET 1.0和.NET 1.1,只需键入.load clr10\sos.dll
- 对于.NET 2.0,.NET 3.0和.NET 3.5,键入.loadby sos mscorwks或.loadby sos mscorsvr
- 对于.NET 4.0或更高版本,键入.loadby sos clr
那么,如何查找.NET Framework版本呢?我喜欢使用lmvm命令。
0:016> lmvm mscorwks start end module name
哦,什么也没有。也许我试试其他的DLL。
0:016> lmvm clr
start end module name
79140000 797ae000 clr (deferred)
Image path: C:\WINDOWS\Microsoft.NET\Framework\v4.0.30319\clr.dll
Image name: clr.dll
----------------------Skip--------------------------------
好的,这是一个.NET 4.0应用程序。因此,我将通过相应的.NET Framework路径加载SOS。
0:016> .loadby sos clr
检索排在Finalizable队列中的对象
如果您不使用本机资源(例如Win32 API调用或本机对象的托管包装器(DirectoryEntry
、FileStream
、GDI+对象等)),最好**不要**实现“~ClassName”方法。
即使您创建了一个空析构函数方法(在.NET世界中名为Finalize
),CLR也会以不同的方式管理此类对象的生命周期。
好的,让我们回到这篇文章。在这个演示中,我将从终结队列中找到一些东西。
0:016> !finalizequeue
SyncBlocks to be cleaned up: 0
MTA Interfaces to be released: 0
STA Interfaces to be released: 0
----------------------------------
generation 0 has 260 finalizable objects (0bfe14f4->0bfe1904)
generation 1 has 15 finalizable objects (0bfe14b8->0bfe14f4)
generation 2 has 6442 finalizable objects (0bfdb010->0bfe14b8)
Ready for finalization 0 objects (0bfe1904->0bfe1904)
Statistics for all finalizable objects (including all objects ready for finalization):
--------------SKIP----------------
79b99fbc 362353 5797648 System.WeakReference
--------------SKIP----------------
我感兴趣的是System.WeakReference
对象。到目前为止,内存中有362353个这样的对象,这太疯狂了!
为什么会有这么多WeakReference
对象?为什么GC不释放它们?
对于问题1,通常是分配太多而释放太少。对于问题2,通常某些东西引用了这个对象,而这些“东西”又引用了ROOT对象,例如CPU寄存器、静态变量。
让我们从托管堆转储对象。
0:017> !dumpheap -mt 79b99fbc
Address MT Size
----------Skip many objects----------------
04410400 79b99fbc 16
04410454 79b99fbc 16
044104a8 79b99fbc 16
044104fc 79b99fbc 16
04410550 79b99fbc 16
044105c4 79b99fbc 16
04410618 79b99fbc 16
----------Skip many objects----------------
然后随机选择一个对象,我在这里选择了“044104fc”。
这个对象的引用关系是什么?
我们可以使用SOS扩展命令“!gcroot
”找到引用路径。注意,有时命令不会向您显示任何内容。
0:000> !gcroot 044104fc
Note: Roots found on stacks may be false positives. Run "!help gcroot" for
more info.
Scan Thread 0 OSTHread 888
---------Skip too many root information-----------------------
ESP:12e9dc:Root: 0dbc9030(System.Windows.Threading.DispatcherOperation)->
0dbc9010(System.Windows.Threading.DispatcherOperationCallback)->
07b3eb98(System.Windows.Threading.DispatcherTimer)->
07b3ebd4(System.EventHandler)->
0375891c(SomeWPFApp.MainWindow)->
079e7a94(SomeWPFApp.PermissionListBox)->
07987f54(System.Windows.Controls.ControlTemplate)->
079884cc(System.Windows.TemplateContent)->
03700cf0(System.Windows.Baml2006.Baml2006SchemaContext)->
03700ea4(System.Windows.Baml2006.WpfSharedBamlSchemaContext)->
04409188(System.Collections.Concurrent.ConcurrentDictionary`2[[System.Reflection.Assembly, mscorlib],[System.Xaml.MS.Impl.XmlNsInfo, System.Xaml]])->
04411314(System.Object[])->
04411650(System.Collections.Concurrent.ConcurrentDictionary`2+Node[[System.Reflection.Assembly, mscorlib],[System.Xaml.MS.Impl.XmlNsInfo, System.Xaml]])->
04411578(System.Collections.Concurrent.ConcurrentDictionary`2+Node[[System.Reflection.Assembly, mscorlib],[System.Xaml.MS.Impl.XmlNsInfo, System.Xaml]])->
04411530(System.Collections.Concurrent.ConcurrentDictionary`2+Node[[System.Reflection.Assembly, mscorlib],[System.Xaml.MS.Impl.XmlNsInfo, System.Xaml]])->
044104d0(System.Xaml.MS.Impl.XmlNsInfo)->
044104fc(System.WeakReference)
---------Skip too many root information-----------------------
Command cancelled at the user's request.
好的,从输出底部,我们找到了我们的幸运儿044104fc。此对象由一个XmlNsInfo
持有,而该XmlNsInfo
由一个泛型字典持有,依此类推。然后,一个SomeWPFApp.MainWindow
实例由一个System.EventHandler
持有。
为什么EventHandler
会引用MainWindow
?EventHandler
是什么?MainWindow
中处理此事件的确切方法名称是什么?
转储方法名称
关键点是EventHandler
对象。
0:000> !do 07b3ebd4
Name: System.EventHandler
MethodTable: 79b92a30
EEClass: 79882d28
Size: 32(0x20) 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
79b9f744 4000076 4 System.Object 0 instance 0375891c _target
79b9f744 4000077 8 System.Object 0 instance 00000000 _methodBase
79b9ab88 4000078 c System.IntPtr 1 instance 7454518 _methodPtr
79b9ab88 4000079 10 System.IntPtr 1 instance 0 _methodPtrAux
79b9f744 400007a 14 System.Object 0 instance 00000000 _invocationList
79b9ab88 400007b 18 System.IntPtr 1 instance 0 _invocationCount
事实上,第一个粗体目标地址是0375891c;如果您向上滚动,您会发现这是MainWindow
对象。
第二个_methodPtr
会告诉我们一些信息。这是一个IntPtr
,指向某物的指针。地址是7454518。通常,这指向方法描述符。所以让我们使用SOS命令转储信息。
0:000> !ip2md 7454518
Failed to request MethodData, not in JIT code range
这是什么?这意味着该方法**可能尚未**进行JIT编译。或者,这是指向实际方法的入口点。让我们检查反汇编代码。
0:000> !u 7454518
Unmanaged code
07454518 e93336a703 jmp 0aec7b50
0745451d 5f pop edi
0745451e 0000 add byte ptr [eax],al
07454520 c45dc6 les ebx,fword ptr [ebp-3Ah]
07454523 0500000000 add eax,0
07454528 e8bbddce71 call clr!PrecodeFixupThunk (791422e8)
0745452d 5e pop esi
0745452e 0000 add byte ptr [eax],al
07454530 58 pop eax
07454531 5d pop ebp
第一行是jmp
,它跳转到地址0aec7b50。实际上,这个地址指向一个真实的托管方法。
我们可以使用!ip2md
转储描述符信息。
0:000> !ip2md 0aec7b50
MethodDesc: 05c65dc4
Method Name: SomeWPFApp.MainWindow.dispatcherTimer_Tick(System.Object, System.EventArgs)
Class: 06117abc
MethodTable: 05c6689c
mdToken: 06000054
Module: 03572ea4
IsJitted: yes
CodeAddr: 0aec7b50
Transparency: Critical
*** WARNING: Unable to verify checksum for C:\SomeFolder\SomeWPFApp.
Source file: E:\work\SomeFolder\SomeWPFApp\MainWindow.xaml. @ 1300
好了,我们现在得到了方法名称。在MainWindow
类中,我们有一个名为dispatcherTimer_Tick
的方法,此方法处理DispatcherTimer
对象的Tick
事件。开发人员应该在代码中的某些地方(例如Closing
事件或Dispose
方法)使用“-=”来删除此事件处理程序。然后,当GC开始工作时,这些对象就可以被回收了。
注意,您并非总是可以通过此方法找到方法描述符。如果代码尚未进行JIT编译,则此方法无效。您应该使用dd <address>检查托管堆,并尝试使用!dumpmd <3rd dd address>
使用第三个双字值。您可以参考Mark撰写的这篇文章,http://julmar.com/blog/mark/?p=79。