用WinDbg探索CLR世界 [3] 跟蹤方法的 JIT 過程

http://www.blogcn.com/blog/cool/main.asp?uid=flier_lu&id=1678453

本來想按照 sos 的幫助文件上命令的分類逐步介紹 WinDbg 下使用 sos 調試 CLR 程序,但發現這樣實在不夠直觀。索性改成根據我分析 CLR 的實際案例,step by step 介紹功能,這樣結構上雖然混亂一點,但更加直觀,也易於上手 :P

前面兩篇文章裏面分別介紹了 WinDbg 的調試配置和線程的基本概念,這篇文章將針對 JIT 編譯對象方法的流程進行分析,逐步介紹如何使用 WinDbg 調試 CLR 程序。

用WinDbg探索CLR世界 [1] - 安裝與環境配置
用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 後中斷執行。此時可以使用 .load sos 命令加載 sos.dll 命令擴展,並用 .chain 驗證加載是否成功;然後用 ld demo 命令加載 demo.exe 的調試符號文件,用 lm 命令驗證加載是否成功。
然後用 ld kernel32 加載 Kernel32 的調試符號文件,並用 bp kernel32!LoadLibraryExW "du poi(esp+4)" 命令在載入 DLL 的函數入口加上斷點。接下來就是一路 g 指令,直到 mscorwks.dll 被加載。這個 mscorwks.dll 就是類似 JVM 中 jvm.dll 的虛擬機實現代碼,我們要了解的大部分功能都在其中。詳細的解釋可以參看我以前的一篇文章《.Net平臺下CLR程序載入原理分析》

在 mscorwks.dll 被載入後用 ld mscorwks 命令載入其調試符號庫,就可以正式開始我們的探索工作了 :D

目前使用到的 WinDbg 命令如下

以下爲引用:

.load sos // 加載 sos 調試擴展模塊,可使用 .chain 命令驗證

ld demo // 加載 demo.exe 調試符號庫,可使用 lm 命令驗證

ld kernel32 // 加載 kernel32.exe 調試符號庫

bp kernel32!LoadLibraryExW "du poi(esp+4)" // 設置斷點監視何時 mscorwks.dll 被載入

g // 執行直到 mscorwks.dll被加載

bd 0 // 清除前面設置的斷點,開始對 mscorwks.dll 進行處理

ld mscorwks // 加載 mscorwks.dll 調試符號庫




Don Box《.NET本質論 第1卷:公共語言運行庫》的第六章介紹了方法調用的內部實現流程。其中提到方法表在 JIT 之前,保存的都是 call mscorwks.dll!PreStubWorker 調用,直到第一次使用時,纔會對目標 IL 代碼進行 JIT 編譯,並調用之。因此我們第一步可以在此函數上設置斷點(bp mscorwks!PreStubWorker),看看系統是如何調用此函數的。

以下爲引用:

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



斷點被激活就代表函數被調用。我們先使用 k 看看函數被調用時的上下文環境。

以下爲引用:

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 一路執行下來的步驟,而那個警告說明這個 stack frame 不在任意一個已知模塊中。這是很正常的,因爲這個棧幀實際上是指向由 JIT 動態生成的代碼。我們監視的 mscorwks!PreStubWorker 函數只是作爲方法表中函數的入口 stub,系統啓動時還會通過其他方式調用 JIT 完成代碼的編譯執行。
接下來用 SOS 的 !clrstack 命令看看 CLR 的調用堆棧,顯示如下:

以下爲引用:

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]



如果需要更爲詳細的詳細,可以使用 -p, -l 或 -r 參數分別顯示參數、局部變量和寄存器,當然前兩者需要調試符號庫的支持才行。

如此一路 g; !clrstack 執行下去,直到 flier.EntryPoint.m1 函數需要被處理爲止:

以下爲引用:

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 命令可以查看當前線程堆棧中使用的所有對象

以下爲引用:

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 就是我們要分析的對象在內存中的位置。

這一階段使用到的 WinDbg 命令如下:

以下爲引用:

bp mscorwks!PreStubWorker // 設置代碼斷點

g // 繼續運行至斷點

k // 查看函數調用時的 Native 堆棧調用

!clrstack // 查看函數調用時的 CLR 堆棧調用

!dumpstackobjects // 查看線程堆棧中使用到的所有對象




知道地址後,就可以用 !dumpobj 命令查看對象的詳細信息

以下爲引用:

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) 正是我們分析方法調用的目標。我們可以用 !dumpclass 命令先進一步查看對象的類型信息:

以下爲引用:

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



可以發現其信息與對象信息有很多符合之處,正如 Don Box 所說,一個對象引用指向一個類型 EEClass 實例,而方法表爲類型所有,其對象共有。我們可以使用 !dumpmt 命令進一步查看方法表的信息,-md 參數表示需要查看每個方法描述 (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個表項,其中前4個已經綁定到使用 ngen 預編譯好的靜態函數上

以下爲引用:

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 的原因。

對方法表的每個項目,可以用 !DumpMD 命令查看詳細描述,如

以下爲引用:

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 代碼形式存在。對於已經完成 JIT 的方法,將顯示其 JIT 後函數體代碼的虛擬地址(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 // 查看對象的詳細信息

!dumpclass 0x6c632e8 // 查看類型的詳細信息

!dumpmt -md 0x09750a8 // 查看方法表的詳細信息

!dumpmd 0x00975070 // 查看方法表項的方法描述的詳細信息

u 0x79b7c4eb // 反彙編指定地址的指令




我們反彙編一下 !DumpMT 命令列出的幾個方法,就會發現正如 Don Box 所說,已經被 JIT 的代碼指向一個jmp指令,直接跳轉到編譯後的方法體,如:

以下爲引用:

0:000> u 0097509b
0097509b e908b04106 jmp 06d900a8



而沒有被 JIT 的函數,則指向一個call指令,調用一個 prolog 代碼,間接調用 mscorwks!PreStubWorker 函數完成實際 JIT 工作,如:

以下爲引用:

0:000> u 0x0097506b
0097506b e878427dff call 001492e8

0:000> u 0x0097507b
0097507b e868427dff call 001492e8



這個 prolog 代碼很簡單,負責構造 mscorwks!PreStubWorker 所需的調用堆棧

以下爲引用:

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 函數(vm/i386/cgenx86.cpp:1829) 動態生成的,完成對 PreStubWorker 函數調用的封裝。而 PreStubWorker 函數會調用 JIT 完成真正的函數編譯工作,並將方法表的入口改爲指向編譯後函數體的 jmp 指令。具體的流程請參考Don Box《.NET本質論 第1卷:公共語言運行庫》的第六章中的介紹,這裏就不再羅嗦了。以後有機會再寫篇文章詳細分析一下 JIT 的工作流程。

在 JIT 處理 flier.EntryPoint.m1 時,用 g 命令執行,再回頭來分析 m1 函數的入口,就會發現如前面所述,調用 JIT 過程的 call 指令變成了直接調用 Native 函數體的 jmp 指令。:D


這一小節,我們介紹了使用 WinDbg 跟蹤調試 CLR 程序的一遍流程,並瞭解了對堆棧、對象和類信息進行分析的 SOS 命令,希望大家能夠藉此開始探索 CLR 內部世界的旅程。 :P

Jason Zander在其 BLog 的一篇文章,SOS Debugging with the CLR (Part 1),裏面也詳細介紹了使用 WinDbg 和 SOS 調試 CLR 程序的部分方法,值得一看。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章