您的位置:首页 > 其它

.NET / Rotor源码分析5 - 开始使用WinDbg+SOS调试,sscoree.dll,加载SOS并设置JIT断点

2008-03-11 09:46 871 查看

准备工作

在经过一番准备之后,现在我们可以开始正式使用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
转载请注明出处

  
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐