.NET / Rotor原始碼分析5 - 開始使用WinDbg+SOS除錯,sscoree.dll,載入SOS並設定JIT斷點

ATField發表於2007-05-21

準備工作

在經過一番準備之後,現在我們可以開始正式使用WinDbg+SOS來除錯託管程式碼了。如果你沒有看過前兩篇文章,那麼請先閱讀這兩篇文章以對WinDbg+SOS有一個大致的瞭解。這兩篇文章的連結在這裡:

.NET Rotor原始碼研究4 – 修改Rotor使其傳送CLR Notificationhttp://blog.csdn.net/ATField/archive/2007/05/21/1618535.aspx

.NET Rotor原始碼研究3 - 除錯Rotor託管程式碼的利器:WinDbgSOShttp://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的除錯環境,如果你還沒有BuildRotor的一個Debug版本,那麼請參照本系列的第一篇文章來設定你的環境並Build出一個除錯版本的Rotor。文章的連結在這裡:

.NET Rotor原始碼研究1 – Building Rotorhttp://blog.csdn.net/ATField/archive/2006/12/31/1471465.aspx

如果已經Build出來了一個Rotorx86除錯版本,那麼可以開始動手編譯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.exePAL_startup_main函式,第二條語句命令WinDbg繼續執行。執行g之後WinDbg很快在clixmain函式停下來,這裡的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執行。在WindbgF10F11仍然可以工作(當然命令列也可以)。一路執行到_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;

}

這個函式的實現非常有意思,直接定義了兩個巨集然後includesscoree_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共存的功能,並且返回載入的DLLHandle。這裡我們所需要的DLLmscorwks.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作為WinDbgExtension載入。如果你沒有看到任何提示資訊,那麼載入成功了。如果提示出錯,請檢查在binaries.x86dbg.rotor目錄下面確實存在SOS.dll,並且WinDbg已經被修改過或者MSVCR80D.dll在路徑中,具體可以參考本系列第3篇文章:

.NET Rotor原始碼研究3 - 除錯Rotor託管程式碼的利器:WinDbgSOShttp://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編譯成功,之後很快WinDbgHello.Hello.Main函式停下了,說明斷點設定成功。

OK,至此我們在Windbg中完整地跟蹤了CLIX的啟動過程和SSCOREE的函式轉發,併成功載入了SOS。下一篇文章將回歸mscorwks!_CorExeMain2函式,繼續我們在.NET / Rotor之中的探索過程,請繼續關注。

 

作者:      張羿/ATField
Blog:     
http://blog.csdn.net/atfield
轉載請註明出處

 

 

相關文章