日期:2013-07-29  浏览次数:20379 次

本来想按照 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\