本來想按照 sos 的幫助文件上命令的分類逐步介紹 WinDbg 下使用 sos 調(diào)試 CLR 程序,但發(fā)現(xiàn)這樣實(shí)在不夠直觀。索性改成根據(jù)我分析 CLR 的實(shí)際案例,step by step 介紹功能,這樣結(jié)構(gòu)上雖然混亂一點(diǎn),但更加直觀,也易于上手 :P
前面兩篇文章里面分別介紹了 WinDbg 的調(diào)試配置和線程的基本概念,這篇文章將針對 JIT 編譯對象方法的流程進(jìn)行分析,逐步介紹如何使用 WinDbg 調(diào)試 CLR 程序。
用WinDbg探索CLR世界 [1] - 安裝與環(huán)境配置 用WinDbg探索CLR世界 [2] - 線程
首先寫一個簡單的例子程序 demo.cs 并編譯為 demo.exe,使用配置好的 WinDbg 打開之:
以下為引用:
using System;
namespace flier { class EntryPoint { public void m1() { System.Console.Write("EntryPoint.m1()"); }
public void m2() { System.Console.Write("EntryPoint.m2()"); }
public static void Main() { EntryPoint ep = new EntryPoint();
ep.m1(); ep.m2(); } } }
WinDbg 會在載入 demo.exe 后中斷執(zhí)行。此時可以使用 .load sos 命令加載 sos.dll 命令擴(kuò)展,并用 .chain 驗(yàn)證加載是否成功;然后用 ld demo 命令加載 demo.exe 的調(diào)試符號文件,用 lm 命令驗(yàn)證加載是否成功。 然后用 ld kernel32 加載 Kernel32 的調(diào)試符號文件,并用 bp kernel32!LoadLibraryExW "du poi(esp+4)" 命令在載入 DLL 的函數(shù)入口加上斷點(diǎn)。接下來就是一路 g 指令,直到 mscorwks.dll 被加載。這個 mscorwks.dll 就是類似 JVM 中 jvm.dll 的虛擬機(jī)實(shí)現(xiàn)代碼,我們要了解的大部分功能都在其中。詳細(xì)的解釋可以參看我以前的一篇文章《.Net平臺下CLR程序載入原理分析》
在 mscorwks.dll 被載入后用 ld mscorwks 命令載入其調(diào)試符號庫,就可以正式開始我們的探索工作了 :D
目前使用到的 WinDbg 命令如下
以下為引用:
.load sos // 加載 sos 調(diào)試擴(kuò)展模塊,可使用 .chain 命令驗(yàn)證
ld demo // 加載 demo.exe 調(diào)試符號庫,可使用 lm 命令驗(yàn)證
ld kernel32 // 加載 kernel32.exe 調(diào)試符號庫
bp kernel32!LoadLibraryExW "du poi(esp+4)" // 設(shè)置斷點(diǎn)監(jiān)視何時 mscorwks.dll 被載入
g // 執(zhí)行直到 mscorwks.dll被加載
bd 0 // 清除前面設(shè)置的斷點(diǎn),開始對 mscorwks.dll 進(jìn)行處理
ld mscorwks // 加載 mscorwks.dll 調(diào)試符號庫
Don Box 在《.NET本質(zhì)論 第1卷:公共語言運(yùn)行庫》的第六章介紹了方法調(diào)用的內(nèi)部實(shí)現(xiàn)流程。其中提到方法表在 JIT 之前,保存的都是 call mscorwks.dll!PreStubWorker 調(diào)用,直到第一次使用時,才會對目標(biāo) IL 代碼進(jìn)行 JIT 編譯,并調(diào)用之。因此我們第一步可以在此函數(shù)上設(shè)置斷點(diǎn)(bp mscorwks!PreStubWorker),看看系統(tǒng)是如何調(diào)用此函數(shù)的。
以下為引用:
0:000> bp mscorwks!PreStubWorker 0:000> g ModLoad: 70ad0000 70bb6000 E:\WINDOWS\WinSxS\x86_Microsoft.Windows.Common-Controls_6595b64144ccf1df_6.0.100.0_x-ww_8417450B\comctl32.dll ModLoad: 79780000 79980000 e:\windows\microsoft.net\framework\v1.1.4322\mscorlib.dll ModLoad: 79980000 79ca6000 e:\windows\assembly\nativeimages1_v1.1.4322\mscorlib\1.0.5000.0__b77a5c561934e089_ed6bc96c\mscorlib.dll ModLoad: 79510000 79523000 E:\WINDOWS\Microsoft.NET\Framework\v1.1.4322\mscorsn.dll Breakpoint 1 hit eax=0012f7c0 ebx=00148c60 ecx=04aa112c edx=00000004 esi=0012f784 edi=0012f9a8 eip=791d6a4a esp=0012f764 ebp=0012f79c iopl=0 nv up ei pl zr na po nc cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246 mscorwks!PreStubWorker: 791d6a4a 55 push ebp
斷點(diǎn)被激活就代表函數(shù)被調(diào)用。我們先使用 k 看看函數(shù)被調(diào)用時的上下文環(huán)境。
以下為引用:
0:000> k ChildEBP RetAddr 0012f760 0014930e mscorwks!PreStubWorker WARNING: Frame IP not in any known module. Following frames may be wrong. 0012f79c 791da434 0x14930e 0012f8b4 791dd2ec mscorwks!MethodDesc::CallDescr+0x1b6 0012f96c 79240405 mscorwks!MethodDesc::Call+0xc5 0012fa18 79240520 mscorwks!AppDomain::InitializeDomainContext+0x10f 0012fa7c 7923d744 mscorwks!SystemDomain::InitializeDefaultDomain+0x11c 0012fd60 791c6e73 mscorwks!SystemDomain::ExecuteMainMethod+0x120 0012ffa0 791c6ef3 mscorwks!ExecuteEXE+0x1c0 0012ffb0 7880a53e mscorwks!_CorExeMain+0x59 0012ffc0 77e1f38c mscoree!_CorExeMain+0x30 [f:\dd\ndp\clr\src\dlls\shim\shim.cpp @ 5426] 0012fff0 00000000 KERNEL32!BaseProcessStart+0x23
這里可以看到從 mscoree!_CorExeMain 一路執(zhí)行下來的步驟,而那個警告說明這個 stack frame 不在任意一個已知模塊中。這是很正常的,因?yàn)檫@個棧幀實(shí)際上是指向由 JIT 動態(tài)生成的代碼。我們監(jiān)視的 mscorwks!PreStubWorker 函數(shù)只是作為方法表中函數(shù)的入口 stub,系統(tǒng)啟動時還會通過其他方式調(diào)用 JIT 完成代碼的編譯執(zhí)行。 接下來用 SOS 的 !clrstack 命令看看 CLR 的調(diào)用堆棧,顯示如下:
以下為引用:
0:000> !clrstack succeeded Loaded Son of Strike data table version 5 from "E:\WINDOWS\Microsoft.NET\Framework\v1.1.4322\mscorwks.dll" Thread 0 ESP EIP 0012f784 791d6a4a [FRAME: PrestubMethodFrame] [DEFAULT] [hasThis] Void System.AppDomain.SetupDomain(ValueClass System.LoaderOptimization,String,String) 0012f9a8 791d6a4a [FRAME: GCFrame] 0012fad0 791d6a4a [FRAME: DebuggerClassInitMarkFrame] 0012fa94 791d6a4a [FRAME: GCFrame]
如果需要更為詳細(xì)的詳細(xì),可以使用 -p, -l 或 -r 參數(shù)分別顯示參數(shù)、局部變量和寄存器,當(dāng)然前兩者需要調(diào)試符號庫的支持才行。
如此一路 g; !clrstack 執(zhí)行下去,直到 flier.EntryPoint.m1 函數(shù)需要被處理為止:
以下為引用:
0:000> !clrstack Thread 0 ESP EIP 0012f68c 791d6a4a [FRAME: PrestubMethodFrame] [DEFAULT] [hasThis] Void flier.EntryPoint.m1() 0012f69c 06d90080 [DEFAULT] Void flier.EntryPoint.Main() 0012f9b0 791da717 [FRAME: GCFrame] 0012fa94 791da717 [FRAME: GCFrame]
此時用 !dumpstackobjects 命令可以查看當(dāng)前線程堆棧中使用的所有對象
以下為引用:
0:000> !dumpstackobjects ESP/REG Object Name ecx 04aa1a90 flier.EntryPoint 0012f678 04aa1a90 flier.EntryPoint 0012f67c 04aa1a90 flier.EntryPoint 0012f680 04aa1a90 flier.EntryPoint
這里的 flier.EntryPoint 對象地址 0x04aa1a90 就是我們要分析的對象在內(nèi)存中的位置。
這一階段使用到的 WinDbg 命令如下:
以下為引用:
bp mscorwks!PreStubWorker // 設(shè)置代碼斷點(diǎn)
g // 繼續(xù)運(yùn)行至斷點(diǎn)
k // 查看函數(shù)調(diào)用時的 Native 堆棧調(diào)用
!clrstack // 查看函數(shù)調(diào)用時的 CLR 堆棧調(diào)用
!dumpstackobjects // 查看線程堆棧中使用到的所有對象
知道地址后,就可以用 !dumpobj 命令查看對象的詳細(xì)信息
以下為引用:
0:000> !dumpobj 04aa1a90 Name: flier.EntryPoint MethodTable 0x009750a8 EEClass 0x06c632e8 Size 12(0xc) bytes mdToken: 02000002 (D:\Temp\demo.exe)
信息包括對象的類型名字(Name)和類型信息的地址(EEClass),以及對象的大小(Size)和 Token (mdToken),而方法表 (MethodTable) 正是我們分析方法調(diào)用的目標(biāo)。我們可以用 !dumpclass 命令先進(jìn)一步查看對象的類型信息:
以下為引用:
0:000> !dumpclass 0x6c632e8 Class Name : flier.EntryPoint mdToken : 02000002 () Parent Class : 79b7c3c8 ClassLoader : 00153850 Method Table : 009750a8 Vtable Slots : 4 Total Method Slots : 8 Class Attributes : 100000 : Flags : 1000003 NumInstanceFields: 0 NumStaticFields: 0 ThreadStaticOffset: 0 ThreadStaticsSize: 0 ContextStaticOffset: 0 ContextStaticsSize: 0
可以發(fā)現(xiàn)其信息與對象信息有很多符合之處,正如 Don Box 所說,一個對象引用指向一個類型 EEClass 實(shí)例,而方法表為類型所有,其對象共有。我們可以使用 !dumpmt 命令進(jìn)一步查看方法表的信息,-md 參數(shù)表示需要查看每個方法描述 (MethodDesc):
以下為引用:
0:000> !dumpmt -md 0x09750a8 EEClass : 06c632e8 Module : 0014e090 Name: flier.EntryPoint mdToken: 02000002 (D:\Temp\demo.exe) MethodTable Flags : 80000 Number of IFaces in IFaceMap : 0 Interface Map : 009750f4 Slots in VTable : 8 -------------------------------------- MethodDesc Table Entry MethodDesc JIT Name 79b7c4eb 79b7c4f0 None [DEFAULT] [hasThis] String System.Object.ToString() 79b7c473 79b7c478 None [DEFAULT] [hasThis] Boolean System.Object.Equals(Object) 79b7c48b 79b7c490 None [DEFAULT] [hasThis] I4 System.Object.GetHashCode() 79b7c52b 79b7c530 None [DEFAULT] [hasThis] Void System.Object.Finalize() 0097506b 00975070 None [DEFAULT] [hasThis] Void flier.EntryPoint.m1() 0097507b 00975080 None [DEFAULT] [hasThis] Void flier.EntryPoint.m2() 0097508b 00975090 None [DEFAULT] Void flier.EntryPoint.Main() 0097509b 009750a0 None [DEFAULT] [hasThis] Void flier.EntryPoint..ctor()
可以看到方法表中共有8個表項(xiàng),其中前4個已經(jīng)綁定到使用 ngen 預(yù)編譯好的靜態(tài)函數(shù)上
以下為引用:
0:000> u 79b7c4eb mscorlib_79980000+0x1fc4eb: 79b7c4eb e8909cfeff call mscorlib_79980000+0x1e6180 (79b66180) 79b7c4f0 0000 add [eax],al 79b7c4f2 0080d86206c0 add [eax+0xc00662d8],al 79b7c4f8 06 push es 79b7c4f9 00fc add ah,bh 79b7c4fb e8809cfeff call mscorlib_79980000+0x1e6180 (79b66180) 79b7c500 07 pop es 79b7c501 0010 add [eax],dl
后四個則作為可被覆蓋的虛方法在方法表中,這也是為什么在查看類型信息時 Vtable Slots = 4 而 Total Method Slots = 8 的原因。
對方法表的每個項(xiàng)目,可以用 !DumpMD 命令查看詳細(xì)描述,如
以下為引用:
0:000> !DumpMD 0x00975070 Method Name : [DEFAULT] [hasThis] Void flier.EntryPoint.m1() MethodTable 9750a8 Module: 14e090 mdToken: 06000001 (D:\Temp\demo.exe) Flags : 0 IL RVA : 00002050
IL RVA 說明此方法的 IL 代碼相對虛擬地址(IL RVA),也就是說此方法還沒有被 JIT,仍以 IL 代碼形式存在。對于已經(jīng)完成 JIT 的方法,將顯示其 JIT 后函數(shù)體代碼的虛擬地址(Method VA):
以下為引用:
0:000> !DumpMD 0x009750a0 Method Name : [DEFAULT] [hasThis] Void flier.EntryPoint..ctor() MethodTable 9750a8 Module: 14e090 mdToken: 06000004 (D:\Temp\demo.exe) Flags : 0 Method VA : 06d900a8
這一階段使用到的 WinDbg 命令如下:
以下為引用:
!dumpobj 04aa1a90 // 查看對象的詳細(xì)信息
!dumpclass 0x6c632e8 // 查看類型的詳細(xì)信息
!dumpmt -md 0x09750a8 // 查看方法表的詳細(xì)信息
!dumpmd 0x00975070 // 查看方法表項(xiàng)的方法描述的詳細(xì)信息
u 0x79b7c4eb // 反匯編指定地址的指令
我們反匯編一下 !DumpMT 命令列出的幾個方法,就會發(fā)現(xiàn)正如 Don Box 所說,已經(jīng)被 JIT 的代碼指向一個jmp指令,直接跳轉(zhuǎn)到編譯后的方法體,如:
以下為引用:
0:000> u 0097509b 0097509b e908b04106 jmp 06d900a8
而沒有被 JIT 的函數(shù),則指向一個call指令,調(diào)用一個 prolog 代碼,間接調(diào)用 mscorwks!PreStubWorker 函數(shù)完成實(shí)際 JIT 工作,如:
以下為引用:
0:000> u 0x0097506b 0097506b e878427dff call 001492e8
0:000> u 0x0097507b 0097507b e868427dff call 001492e8
這個 prolog 代碼很簡單,負(fù)責(zé)構(gòu)造 mscorwks!PreStubWorker 所需的調(diào)用堆棧
以下為引用:
0:000> u 0x001492e8 001492e8 52 push edx 001492e9 68f0301b79 push 0x791b30f0 001492ee 55 push ebp 001492ef 53 push ebx 001492f0 56 push esi 001492f1 57 push edi 001492f2 8d742410 lea esi,[esp+0x10] 001492f6 51 push ecx 001492f7 52 push edx 001492f8 648b1d2c0e0000 mov ebx,fs:[00000e2c] 001492ff 8b7b08 mov edi,[ebx+0x8] 00149302 897e04 mov [esi+0x4],edi 00149305 897308 mov [ebx+0x8],esi 00149308 56 push esi 00149309 e83cd70879 call mscorwks!PreStubWorker (791d6a4a) 0014930e 897b08 mov [ebx+0x8],edi 00149311 894604 mov [esi+0x4],eax 00149314 5a pop edx 00149315 59 pop ecx 00149316 5f pop edi 00149317 5e pop esi 00149318 5b pop ebx 00149319 5d pop ebp 0014931a 83c404 add esp,0x4 0014931d 8f0424 pop [esp] 00149320 c3 ret
而這段 prolog 代碼是由類似 ROTOR 中的 GeneratePrestub 函數(shù)(vm\i386\cgenx86.cpp:1829) 動態(tài)生成的,完成對 PreStubWorker 函數(shù)調(diào)用的封裝。而 PreStubWorker 函數(shù)會調(diào)用 JIT 完成真正的函數(shù)編譯工作,并將方法表的入口改為指向編譯后函數(shù)體的 jmp 指令。具體的流程請參考Don Box 在《.NET本質(zhì)論 第1卷:公共語言運(yùn)行庫》的第六章中的介紹,這里就不再羅嗦了。以后有機(jī)會再寫篇文章詳細(xì)分析一下 JIT 的工作流程。
在 JIT 處理 flier.EntryPoint.m1 時,用 g 命令執(zhí)行,再回頭來分析 m1 函數(shù)的入口,就會發(fā)現(xiàn)如前面所述,調(diào)用 JIT 過程的 call 指令變成了直接調(diào)用 Native 函數(shù)體的 jmp 指令。:D
這一小節(jié),我們介紹了使用 WinDbg 跟蹤調(diào)試 CLR 程序的一遍流程,并了解了對堆棧、對象和類信息進(jìn)行分析的 SOS 命令,希望大家能夠借此開始探索 CLR 內(nèi)部世界的旅程。 :P
Jason Zander在其 BLog 的一篇文章,SOS Debugging with the CLR (Part 1),里面也詳細(xì)介紹了使用 WinDbg 和 SOS 調(diào)試 CLR 程序的部分方法,
|