准备工作
在经过一番准备之后,现在我们可以开始正式使用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