.NET / Rotor原始碼分析5 - 開始使用WinDbg+SOS除錯,sscoree.dll,載入SOS並設定JIT斷點
準備工作
在經過一番準備之後,現在我們可以開始正式使用WinDbg+SOS來除錯託管程式碼了。如果你沒有看過前兩篇文章,那麼請先閱讀這兩篇文章以對WinDbg+SOS有一個大致的瞭解。這兩篇文章的連結在這裡:
.NET Rotor原始碼研究4 – 修改Rotor使其傳送CLR Notification:http://blog.csdn.net/ATField/archive/2007/05/21/1618535.aspx
.NET Rotor原始碼研究3 - 除錯Rotor託管程式碼的利器:WinDbg和SOS:http://blog.csdn.net/ATField/archive/2007/05/12/1606151.aspx
除此之外,還需要準備一個小程式來進行除錯,本文所使用的程式如下:(hello.cs)
namespace Hello { class Hello { public static void Main(string[] args) { System.Console.WriteLine("Your name please?"); string s = System.Console.ReadLine(); Welcome(s); Welcome(s); } public static void Welcome(string name) { System.Console.WriteLine("Hello " + name); } } } |
開啟命令提示符,進入sscli20目錄,鍵入:
env dbg |
進入Rotor的除錯環境,如果你還沒有Build出Rotor的一個Debug版本,那麼請參照本系列的第一篇文章來設定你的環境並Build出一個除錯版本的Rotor。文章的連結在這裡:
.NET Rotor原始碼研究1 – Building Rotor:http://blog.csdn.net/ATField/archive/2006/12/31/1471465.aspx
如果已經Build出來了一個Rotor的x86除錯版本,那麼可以開始動手編譯hello.cs (假定hello.cs位於binaries.x86dbg.rotor目錄下):
cd binaries.x86dbg.rotor csc hello.cs |
編譯之後,啟動偵錯程式。這裡我們不能直接除錯hello.exe,否則.NET將會執行hello.exe,這裡我們需要使用clix.exe來執行hello.exe,這樣才可以讓Rotor來執行hello.exe::
windbg clix hello.exe |
請保證WinDbg已經被安裝並且在其路徑在Path變數中。
程式的載入
啟動偵錯程式,我們停在程式載入的位置,Call Stack如下(如果你沒有Windows系統DLL所對應的Symbol,那麼你看到的會有所不同,這裡因為有Symbol,結果更加準確):
ntdll!DbgBreakPoint ntdll!LdrpDoDebuggerBreak+0x31 ntdll!LdrpInitializeProcess+0xffc ntdll!_LdrpInitialize+0xf5 ntdll!LdrInitializeThunk+0x10 |
在本系列的第二篇文章中曾經提到,用到PAL的程式的main實際是在PAL_startup_main,如果你還沒有看到第二篇文章的話,連線在這裡:
.NET Rotor原始碼研究2 - PAL :http://blog.csdn.net/ATField/archive/2007/01/12/1481538.aspx
在偵錯程式中輸入:
bp clix!PAL_startup_main g |
第一條語句的作用是設定斷點於clix.exe的PAL_startup_main函式,第二條語句命令WinDbg繼續執行。執行g之後WinDbg很快在clix的main函式停下來,這裡的main實際上就是PAL_startup_main,被#define過:
int __cdecl main(int argc, char **argv) { // 省略… nExitCode = Launch(pModuleName, pActualCmdLine); } DWORD Launch(WCHAR* pFileName, WCHAR* pCmdLine) { // 省略… nExitCode = _CorExeMain2(NULL, 0, pFileName, NULL, pCmdLine); return nExitCode; } |
這裡有不少無關的程式碼,大部分是分析命令列,直接來到Launch函式呼叫,Launch函式負責啟動ModuleName,也就是hello.exe,啟動工作由_CorExeMain2執行。在Windbg中F10和F11仍然可以工作(當然命令列也可以)。一路執行到_CorExeMain2然後F11,會發現來到了sscoree的_CorExeMain2函式,位於sscoree_shims.h之中:
SSCOREE_SHIM_RET ( __int32, STDMANGLE(_CorExeMain2,20), ( PBYTE pUnmappedPE, DWORD cUnmappedPE, LPWSTR pImageNameIn, LPWSTR pLoadersFileName, LPWSTR pCmdLine), ( pUnmappedPE, cUnmappedPE, pImageNameIn, pLoadersFileName, pCmdLine), -1) |
這個函式程式碼很奇怪,只是一些函式呼叫。仔細觀察一下這個標頭檔案,發現這個檔案是很有規律的由下面內容組成:
SSCOREE_LIB_START (mscorwks) SSCOREE_SHIM_RET ( HRESULT, STDMANGLE(MetaDataGetDispenser,12),SSCOREE_LIB_END (mscorwks) … … SSCOREE_LIB_END (mscorwks) SSCOREE_LIB_START (mscorpe) … SSCOREE_LIB_END (mscorpe) SSCOREE_LIB_START (mscordbi) … … |
這個提示我們SSCOREE.dll會負責將列表中的函式轉發到對應的DLL中的對應函式。實際上,這正是sscoree.dll所起到的作用之一,確定Rotor版本,載入對應版本的Rotor,並呼叫對應版本的Rotor的相應函式,因此sscoree(在.NET中則是mscoree)又被稱為Shim。這個SSCOREE_SHIM_RET只是一個巨集定義,如下:
#define SSCOREE_SHIM_BODY(FUNC,RET_COMMAND,SIG_RET,SIG_ARGS,ARGS) / do { / SSCOREE_SHIM_CUSTOM_INIT / FARPROC proc_addr = SscoreeShimGetProcAddress ( / SHIMSYM_ ## FUNC, / #FUNC); / _ASSERTE (proc_addr); / if (proc_addr) { / RET_COMMAND ((SIG_RET (STDMETHODCALLTYPE *)SIG_ARGS)proc_addr)ARGS; / } / } while (0) #define SSCOREE_SHIM_RET(SIG_RET,FUNC,SIG_ARGS,ARGS,ONERROR) / extern "C" / SIG_RET STDMETHODCALLTYPE FUNC SIG_ARGS / { / SSCOREE_SHIM_BODY (FUNC, return, SIG_RET, SIG_ARGS, ARGS); / return ONERROR; / } #define SSCOREE_SHIM_NORET(FUNC,SIG_ARGS,ARGS) / extern "C" / void STDMETHODCALLTYPE FUNC SIG_ARGS / { / SSCOREE_SHIM_BODY (FUNC, ; ,void, SIG_ARGS, ARGS); / } |
可以看到在sscoree中每個類似_CorExeMain2的函式大致作的事情都很類似,首先呼叫SscoreeShimGetProcAddress獲得在Rotor核心DLL中的地址,然後呼叫之。
回到偵錯程式,按下F11,直接進入SscoreeShimGetProcAddress函式:
FARPROC SscoreeShimGetProcAddress ( ShimmedSym SymIndex, LPCSTR SymName) { FARPROC proc; #ifdef TRACE_LOADS printf ("SscoreeShimGetProcAddress: Loading Symbol %d (%s)/n", SymIndex, g_Syms[SymIndex].Name); #endif _ASSERTE (SYM_INDEX_VALID (SymIndex)); _ASSERTE (SymName); _ASSERTE (g_Syms[SymIndex].Name); _ASSERTE (!strcmp (g_Syms[SymIndex].Name, SymName)); proc = g_Syms[SymIndex].Proc; if (proc == NULL) { proc = SetupProc(SymIndex, SymName); } return proc; } |
g_Syms是一個全域性的陣列,用於儲存每個函式的實際地址,如果地址=NULL,說明還沒有獲得此函式的地址,需要呼叫SetupProc:
static FARPROC SetupProc ( ShimmedSym SymIndex, LPCSTR SymName) { HMODULE lib_handle; FARPROC proc; ShimmedLib LibIndex = FindSymbolsLib (SymIndex); _ASSERTE (LIB_INDEX_VALID (LibIndex)); #ifdef TRACE_LOADS printf ("SscoreeShimGetProcAddress: Loading library %d (%S)/n", LibIndex, g_Libs[LibIndex].Name); #endif lib_handle = g_Libs[LibIndex].Handle; if (lib_handle == NULL) { lib_handle = SetupLib (LibIndex); if (lib_handle == NULL) return NULL; } _ASSERTE (lib_handle); proc = g_Syms[SymIndex].Proc; if (proc == NULL) { proc = GetProcAddress (lib_handle, SymName); if (!proc) { #ifdef _DEBUG fprintf (stderr, "SscoreeShimGetProcAddress: GetProcAddress (/"%s/") failed/n", SymName); #endif return proc; } g_Syms[SymIndex].Proc = proc; } return proc; } |
FindSymbols負責找到函式和DLL之間的對應關係:
ShimmedLib FindSymbolsLib ( ShimmedSym SymIndex) { // some trickery to figure out which library this symbol is in _ASSERTE (SYM_INDEX_VALID (SymIndex));
#define SSCOREE_LIB_START(LIBNAME) / if (SymIndex < SHIMLIB_ ## LIBNAME) { / return LIB_ ## LIBNAME; / } / if (SymIndex == SHIMLIB_ ## LIBNAME) { / return LIB_ ## MAX_LIB; / } #include "sscoree_shims.h"
return LIB_MAX_LIB; } |
這個函式的實現非常有意思,直接定義了兩個巨集然後include了sscoree_shims.h。實際上這是一個很有意思的技巧,sscoree_shims.h中以巨集的形式儲存了每個函式和每個DLL,這樣,通過定義巨集的內容,可以對同樣的sscoree_shims.h中的內容轉換成不同的程式碼,比如這裡就是把這個檔案轉換成了一系列的if語句,判斷函式Index的範圍,返回DLL(這裡稱之為LIB)的Index,避免了重複程式碼。
再回到SetupProc函式,這次需要注意的SetupProc在呼叫FindSymbolsLib之後接著呼叫了SetupLib函式:
HMODULE SetupLib ( ShimmedLib LibIndex) { HMODULE lib_handle; WCHAR FullPath[_MAX_PATH]; if (!PAL_GetPALDirectory (FullPath, _MAX_PATH)) { return NULL; } if (wcslen(FullPath) + wcslen(g_Libs[LibIndex].Name) >= _MAX_PATH) { SetLastError(ERROR_FILENAME_EXCED_RANGE); return NULL; } wcsncat(FullPath, g_Libs[LibIndex].Name, _MAX_PATH); lib_handle = LoadLibrary (FullPath); if (lib_handle == NULL) { #ifdef _DEBUG fprintf (stderr, "SscoreeShimGetProcAddress: LoadLibrary (/"%S/") failed/n", FullPath); DisplayMessageFromSystem(GetLastError()); #endif return lib_handle; } g_Libs[LibIndex].Handle = lib_handle; #ifdef _DEBUG // first time we've hit this library. Run some tests. SscoreeVerifyLibrary (LibIndex); #endif ROTOR_PAL_CTOR_TEST_RUN(SSCOREE_INT); return lib_handle; } |
這個函式不長,根據PAL所在目錄載入對應的DLL從而實現不同版本的Rotor共存的功能,並且返回載入的DLL的Handle。這裡我們所需要的DLL是mscorwks.dll,是.NET / Rotor 虛擬機器的工作站(WorkStation)版本的核心DLL。
執行到wcsncat語句之後,在偵錯程式中輸入:
dv FullPath |
這條命令作用是顯示FullPath區域性變數的值,結果為:
FullPath = wchar_t [260] "D:/usr/src/sscli20/binaries.x86dbg.rotor/mscorwks.dll" |
可以看到我們需要執行mscorwks!_CorExeMain。
再度回到SetupProc,這次SetupProc呼叫GetProcAddress獲得對應函式的地址並儲存,然後返回。下面的程式碼就不需要繼續執行了。在偵錯程式中輸入下面語句:
g mscorwks!_CorExeMain2 |
這條語句讓WinDbg執行程式直到遇見mscorwks!_CorExeMain2函式為止:
//***************************************************************************** // This entry point is called from the native entry piont of the loaded // executable image. The command line arguments and other entry point data // will be gathered here. The entry point for the user image will be found // and handled accordingly. //***************************************************************************** __int32 STDMETHODCALLTYPE _CorExeMain2( // Executable exit code. PBYTE pUnmappedPE, // -> memory mapped code DWORD cUnmappedPE, // Size of memory mapped code __in LPWSTR pImageNameIn, // -> Executable Name __in LPWSTR pLoadersFileName, // -> Loaders Name __in LPWSTR pCmdLine) // -> Command Line { |
載入SOS,設定斷點
對了,現在我們還需要載入SOS,因為SOS需要mscorwks,因此在這個時候載入SOS正合適。在偵錯程式中輸入:
.loadby sos mscorwks |
這條語句負責將和mscorwks在同一目錄下的sos.dll作為WinDbg的Extension載入。如果你沒有看到任何提示資訊,那麼載入成功了。如果提示出錯,請檢查在binaries.x86dbg.rotor目錄下面確實存在SOS.dll,並且WinDbg已經被修改過或者MSVCR80D.dll在路徑中,具體可以參考本系列第3篇文章:
.NET Rotor原始碼研究3 - 除錯Rotor託管程式碼的利器:WinDbg和SOS:http://blog.csdn.net/ATField/archive/2007/05/12/1606151.aspx
成功載入之後,為了驗證之前我們對IsDebuggerPresent的修改確實生效,輸入:
!bpmd hello.exe Hello.Hello.Main g |
前一條命令是SOS命令,負責對Hello.Hello.Main函式設定斷點,實際上在CLR中設定斷點要比一般程式中設定斷點要複雜的多,並且需要notification才可以工作,在後面我將會講到具體的過程。後面的g命令告訴WinDbg繼續執行,注意在WinDbg的輸出有如下內容:
(11fc.1088): CLR notification exception - code e0444143 (first chance) CLR notification: module 'sorttbls.nlp' loaded (11fc.1088): CLR notification exception - code e0444143 (first chance) (11fc.1088): CLR notification exception - code e0444143 (first chance) CLR notification: method 'Hello.Hello.Main(System.String[])' code generated (11fc.1088): CLR notification exception - code e0444143 (first chance) JITTED hello!Hello.Hello.Main(System.String[]) |
若干CLR Notification已經發出,最重要的是最後一個notification,通知Hello.Hello.Main已經被JIT編譯成功,之後很快WinDbg在Hello.Hello.Main函式停下了,說明斷點設定成功。
OK,至此我們在Windbg中完整地跟蹤了CLIX的啟動過程和SSCOREE的函式轉發,併成功載入了SOS。下一篇文章將回歸mscorwks!_CorExeMain2函式,繼續我們在.NET / Rotor之中的探索過程,請繼續關注。
作者: 張羿/ATField
Blog: http://blog.csdn.net/atfield
轉載請註明出處
相關文章
- .NET / Rotor原始碼研究3 – 除錯Rotor託管程式碼的利器:WinDbg和SOS原始碼除錯
- .NET / Rotor原始碼分析4 - 修改Rotor使其傳送CLR Notification原始碼
- 使用VS Code從零開始開發除錯.NET 5除錯
- 如何斷點除錯Tomcat原始碼斷點除錯Tomcat原始碼
- Chrome 中的 JavaScript 斷點設定和除錯技巧ChromeJavaScript斷點除錯
- ssis package 在除錯狀態中設定斷點,程式 不進入斷點 的解決方案Package除錯斷點
- 在 Python 除錯過程中設定不中斷的斷點Python除錯斷點
- AS斷點除錯斷點除錯
- 今天開始應該使用 5 個JavaScript除錯技巧JavaScript除錯
- skynet原始碼分析(1)--模組載入原始碼
- vscode除錯使用斷點VSCode除錯斷點
- 程式設計技巧 --- VS如何除錯.Net原始碼程式設計除錯原始碼
- 使用VS Code從零開始開發除錯.NET Core 1.0除錯
- 輕鬆兩步,搭建斷點除錯 PHP 原始碼環境斷點除錯PHP原始碼
- 【前端除錯】- 斷點除錯的正確開啟方式前端除錯斷點
- webstorm 斷點除錯WebORM斷點除錯
- 如何防止斷點除錯進入jquery庫斷點除錯jQuery
- VasSonic原始碼之並行載入原始碼並行
- 編譯除錯Net6原始碼編譯除錯原始碼
- Pycharm的斷點除錯PyCharm斷點除錯
- js斷點除錯心得JS斷點除錯
- 除錯——條件斷點除錯斷點
- [譯]如何停止使用 console.log() 並開始使用瀏覽器除錯程式碼瀏覽器除錯
- 嵌入式安卓開發使用LLDB進行斷點除錯C/C++程式碼安卓LLDB斷點除錯C++
- JVM 原始碼分析(二):搭建 JDK 8 原始碼除錯環境(Windows 上使用 CLion)JVM原始碼JDK除錯Windows
- Vscode斷點除錯VSCode斷點除錯
- 熔斷器 Hystrix 原始碼解析 —— 除錯環境搭建原始碼除錯
- 開始Tornado的原始碼分析之旅原始碼
- windbg除錯系列教程:sos擴充套件的介紹和使用除錯套件
- JVM類載入器-原始碼分析JVM原始碼
- [譯] 斷點:像專家一樣除錯程式碼斷點除錯
- 除錯spark原始碼除錯Spark原始碼
- VS - 打斷點/本地除錯/遠端除錯 問題斷點除錯
- 斷點設定列表斷點
- phpstorm + xdebug 斷點除錯PHPORM斷點除錯
- 除錯篇——斷點與單步除錯斷點
- phpStorm10斷點除錯PHPORM斷點除錯
- LLDB斷點除錯注意事項LLDB斷點除錯