用WinDbg探索CLR世界 [3] 跟踪方法的 JIT 过程

来源: http://www.blogcn.com/blog/cool/main.asp?uid=flier_lu&id=1678453

本来想按照sos的帮助文件上命令的分类逐步介绍WinDbg下使用sos调试CLR程序,但发现这样实在不够直观。索性改成根据我分析CLR的实际案例,stepbystep介绍功能,这样结构上虽然混乱一点,但更加直观,也易于上手:P

前面两篇文章里面分别介绍了WinDbg的调试配置和线程的基本概念,这篇文章将针对JIT编译对象方法的流程进行分析,逐步介绍如何使用WinDbg调试CLR程序。

用WinDbg探索CLR世界[1]-安装与环境配置
用WinDbg探索CLR世界[2]-线程

首先写一个简单的例子程序demo.cs并编译为demo.exe,使用配置好的WinDbg打开之:

以下为引用:
usingSystem;

namespaceflier
{
classEntryPoint
{
publicvoidm1()
{
System.Console.Write("EntryPoint.m1()");
}

publicvoidm2()
{
System.Console.Write("EntryPoint.m2()");
}

publicstaticvoidMain()
{
EntryPointep=newEntryPoint();

ep.m1();
ep.m2();
}
}
}



WinDbg会在载入demo.exe后中断执行。此时可以使用.loadsos命令加载sos.dll命令扩展,并用.chain验证加载是否成功;然后用lddemo命令加载demo.exe的调试符号文件,用lm命令验证加载是否成功。
然后用ldkernel32加载Kernel32的调试符号文件,并用bpkernel32!LoadLibraryExW"dupoi(esp 4)"命令在载入DLL的函数入口加上断点。接下来就是一路g指令,直到mscorwks.dll被加载。这个mscorwks.dll就是类似JVM中jvm.dll的虚拟机实现代码,我们要了解的大部分功能都在其中。详细的解释可以参看我以前的一篇文章《.Net平台下CLR程序载入原理分析》

在mscorwks.dll被载入后用ldmscorwks命令载入其调试符号库,就可以正式开始我们的探索工作了:D

目前使用到的WinDbg命令如下

以下为引用:
.loadsos//加载sos调试扩展模块,可使用.chain命令验证

lddemo//加载demo.exe调试符号库,可使用lm命令验证

ldkernel32//加载kernel32.exe调试符号库

bpkernel32!LoadLibraryExW"dupoi(esp 4)"//设置断点监视何时mscorwks.dll被载入

g//执行直到mscorwks.dll被加载

bd0//清除前面设置的断点,开始对mscorwks.dll进行处理

ldmscorwks//加载mscorwks.dll调试符号库




DonBox在《.NET本质论第1卷:公共语言运行库》的第六章介绍了方法调用的内部实现流程。其中提到方法表在JIT之前,保存的都是callmscorwks.dll!PreStubWorker调用,直到第一次使用时,才会对目标IL代码进行JIT编译,并调用之。因此我们第一步可以在此函数上设置断点(bpmscorwks!PreStubWorker),看看系统是如何调用此函数的。

以下为引用:
0:000>bpmscorwks!PreStubWorker
0:000>g
ModLoad:70ad000070bb6000E:\Windows\WinSxS\x86_Microsoft.Windows.Common-Controls_6595b64144ccf1df_6.0.100.0_x-ww_8417450B\comctl32.dll
ModLoad:7978000079980000e:\windows\microsoft.net\Framework\v1.1.4322\mscorlib.dll
ModLoad:7998000079ca6000e:\windows\assembly\nativeimages1_v1.1.4322\mscorlib\1.0.5000.0__b77a5c561934e089_ed6bc96c\mscorlib.dll
ModLoad:7951000079523000E:\WINDOWS\Microsoft.NET\Framework\v1.1.4322\mscorsn.dll
Breakpoint1hit
eax=0012f7c0ebx=00148c60ecx=04aa112cedx=00000004esi=0012f784edi=0012f9a8
eip=791d6a4aesp=0012f764ebp=0012f79ciopl=0nvupeiplzrnaponc
cs=001bss=0023ds=0023es=0023fs=0038gs=0000efl=00000246
mscorwks!PreStubWorker:

791d6a4a55pushebp

断点被激活就代表函数被调用。我们先使用k看看函数被调用时的上下文环境。

以下为引用:
0:000>k
ChildEBPRetAddr
0012f7600014930emscorwks!PreStubWorker
WARNING:FrameIPnotinanyknownmodule.Followingframesmaybewrong.
0012f79c791da4340x14930e
0012f8b4791dd2ecmscorwks!MethodDesc::CallDescr 0x1b6
0012f96c79240405mscorwks!MethodDesc::Call 0xc5
0012fa1879240520mscorwks!AppDomain::InitializeDomainContext 0x10f
0012fa7c7923d744mscorwks!SystemDomain::InitializeDefaultDomain 0x11c
0012fd60791c6e73mscorwks!SystemDomain::ExecuteMainMethod 0x120
0012ffa0791c6ef3mscorwks!ExecuteEXE 0x1c0
0012ffb07880a53emscorwks!_CorExeMain 0x59
0012ffc077e1f38cmscoree!_CorExeMain 0x30[f:\dd\ndp\clr\src\dlls\shim\shim.cpp@5426]
0012fff000000000KERNEL32!BaseProcessStart 0x23



这里可以看到从mscoree!_CorExeMain一路执行下来的步骤,而那个警告说明这个stackframe不在任意一个已知模块中。这是很正常的,因为这个栈帧实际上是指向由JIT动态生成的代码。我们监视的mscorwks!PreStubWorker函数只是作为方法表中函数的入口stub,系统启动时还会通过其他方式调用JIT完成代码的编译执行。
接下来用SOS的!clrstack命令看看CLR的调用堆栈,显示如下:

以下为引用:
0:000>!clrstack
succeeded
LoadedSonofStrikedatatableversion5from"E:\WINDOWS\Microsoft.NET\Framework\v1.1.4322\mscorwks.dll"
Thread0
ESPEIP
0012f784791d6a4a[FRAME:PrestubMethodFrame][DEFAULT][hasThis]VoidSystem.AppDomain.SetupDomain(ValueClassSystem.LoaderOptimization,String,String)
0012f9a8791d6a4a[FRAME:GCFrame]
0012fad0791d6a4a[FRAME:DebuggerClassInitMarkFrame]
0012fa94791d6a4a[FRAME:GCFrame]



如果需要更为详细的详细,可以使用-p,-l或-r参数分别显示参数、局部变量和寄存器,当然前两者需要调试符号库的支持才行。

如此一路g;!clrstack执行下去,直到flier.EntryPoint.m1函数需要被处理为止:

以下为引用:
0:000>!clrstack
Thread0
ESPEIP
0012f68c791d6a4a[FRAME:PrestubMethodFrame][DEFAULT][hasThis]Voidflier.EntryPoint.m1()
0012f69c06d90080[DEFAULT]Voidflier.EntryPoint.Main()
0012f9b0791da717[FRAME:GCFrame]
0012fa94791da717[FRAME:GCFrame]



此时用!dumpstackobjects命令可以查看当前线程堆栈中使用的所有对象

以下为引用:
0:000>!dumpstackobjects
ESP/REGObjectName
ecx04aa1a90flier.EntryPoint
0012f67804aa1a90flier.EntryPoint
0012f67c04aa1a90flier.EntryPoint
0012f68004aa1a90flier.EntryPoint



这里的flier.EntryPoint对象地址0x04aa1a90就是我们要分析的对象在内存中的位置。

这一阶段使用到的WinDbg命令如下:

以下为引用:
bpmscorwks!PreStubWorker//设置代码断点

g//继续运行至断点

k//查看函数调用时的Native堆栈调用

!clrstack//查看函数调用时的CLR堆栈调用

!dumpstackobjects//查看线程堆栈中使用到的所有对象




知道地址后,就可以用!dumpobj命令查看对象的详细信息

以下为引用:
0:000>!dumpobj04aa1a90
Name:flier.EntryPoint
MethodTable0x009750a8
EEClass0x06c632e8
Size12(0xc)bytes
mdToken:02000002(D:\Temp\demo.exe)



信息包括对象的类型名字(Name)和类型信息的地址(EEClass),以及对象的大小(Size)和Token(mdToken),而方法表(MethodTable)正是我们分析方法调用的目标。我们可以用!dumpclass命令先进一步查看对象的类型信息:

以下为引用:
0:000>!dumpclass0x6c632e8
ClassName:flier.EntryPoint
mdToken:02000002()
ParentClass:79b7c3c8
ClassLoader:00153850
MethodTable:009750a8
VtableSlots:4
TotalMethodSlots:8
ClassAttributes:100000:
Flags:1000003
NumInstanceFields:0
NumStaticFields:0
ThreadStaticOffset:0
ThreadStaticsSize:0
ContextStaticOffset:0
ContextStaticsSize:0



可以发现其信息与对象信息有很多符合之处,正如DonBox所说,一个对象引用指向一个类型EEClass实例,而方法表为类型所有,其对象共有。我们可以使用!dumpmt命令进一步查看方法表的信息,-md参数表示需要查看每个方法描述(MethodDesc):

以下为引用:
0:000>!dumpmt-md0x09750a8
EEClass:06c632e8
Module:0014e090
Name:flier.EntryPoint
mdToken:02000002(D:\Temp\demo.exe)
MethodTableFlags:80000
NumberofIFacesinIFaceMap:0
InterfaceMap:009750f4
SlotsinVTable:8
--------------------------------------
MethodDescTable
EntryMethodDescJITName
79b7c4eb79b7c4f0None[DEFAULT][hasThis]StringSystem.Object.ToString()
79b7c47379b7c478None[DEFAULT][hasThis]BooleanSystem.Object.Equals(Object)
79b7c48b79b7c490None[DEFAULT][hasThis]I4System.Object.GetHashCode()
79b7c52b79b7c530None[DEFAULT][hasThis]VoidSystem.Object.Finalize()
0097506b00975070None[DEFAULT][hasThis]Voidflier.EntryPoint.m1()
0097507b00975080None[DEFAULT][hasThis]Voidflier.EntryPoint.m2()
0097508b00975090None[DEFAULT]Voidflier.EntryPoint.Main()
0097509b009750a0None[DEFAULT][hasThis]Voidflier.EntryPoint..ctor()



可以看到方法表中共有8个表项,其中前4个已经绑定到使用ngen预编译好的静态函数上

以下为引用:
0:000>u79b7c4eb
mscorlib_79980000 0x1fc4eb:
79b7c4ebe8909cfeffcallmscorlib_79980000 0x1e6180(79b66180)
79b7c4f00000add[eax],al
79b7c4f20080d86206c0add[eax 0xc00662d8],al
79b7c4f806pushes
79b7c4f900fcaddah,bh
79b7c4fbe8809cfeffcallmscorlib_79980000 0x1e6180(79b66180)
79b7c50007popes
79b7c5010010add[eax],dl



后四个则作为可被覆盖的虚方法在方法表中,这也是为什么在查看类型信息时VtableSlots=4而TotalMethodSlots=8的原因。

对方法表的每个项目,可以用!DumpMD命令查看详细描述,如

以下为引用:
0:000>!DumpMD0x00975070
MethodName:[DEFAULT][hasThis]Voidflier.EntryPoint.m1()
MethodTable9750a8
Module:14e090
mdToken:06000001(D:\Temp\demo.exe)
Flags:0
ILRVA:00002050



ILRVA说明此方法的IL代码相对虚拟地址(ILRVA),也就是说此方法还没有被JIT,仍以IL代码形式存在。对于已经完成JIT的方法,将显示其JIT后函数体代码的虚拟地址(MethodVA):

以下为引用:
0:000>!DumpMD0x009750a0
MethodName:[DEFAULT][hasThis]Voidflier.EntryPoint..ctor()
MethodTable9750a8
Module:14e090
mdToken:06000004(D:\Temp\demo.exe)
Flags:0
MethodVA:06d900a8




这一阶段使用到的WinDbg命令如下:

以下为引用:
!dumpobj04aa1a90//查看对象的详细信息

!dumpclass0x6c632e8//查看类型的详细信息

!dumpmt-md0x09750a8//查看方法表的详细信息

!dumpmd0x00975070//查看方法表项的方法描述的详细信息

u0x79b7c4eb//反汇编指定地址的指令

我们反汇编一下!DumpMT命令列出的几个方法,就会发现正如DonBox所说,已经被JIT的代码指向一个jmp指令,直接跳转到编译后的方法体,如:

以下为引用:
0:000>u0097509b
0097509be908b04106jmp06d900a8



而没有被JIT的函数,则指向一个call指令,调用一个prolog代码,间接调用mscorwks!PreStubWorker函数完成实际JIT工作,如:

以下为引用:
0:000>u0x0097506b
0097506be878427dffcall001492e8

0:000>u0x0097507b
0097507be868427dffcall001492e8



这个prolog代码很简单,负责构造mscorwks!PreStubWorker所需的调用堆栈

以下为引用:
0:000>u0x001492e8
001492e852pushedx
001492e968f0301b79push0x791b30f0
001492ee55pushebp
001492ef53pushebx
001492f056pushesi
001492f157pushedi
001492f28d742410leaesi,[esp 0x10]
001492f651pushecx
001492f752pushedx
001492f8648b1d2c0e0000movebx,fs:[00000e2c]
001492ff8b7b08movedi,[ebx 0x8]
00149302897e04mov[esi 0x4],edi
00149305897308mov[ebx 0x8],esi
0014930856pushesi
00149309e83cd70879callmscorwks!PreStubWorker(791d6a4a)
0014930e897b08mov[ebx 0x8],edi
00149311894604mov[esi 0x4],eax
001493145apopedx
0014931559popecx
001493165fpopedi
001493175epopesi
001493185bpopebx
001493195dpopebp
0014931a83c404addesp,0x4
0014931d8f0424pop[esp]
00149320c3ret



而这段prolog代码是由类似ROTOR中的GeneratePrestub函数(vm\i386\cgenx86.cpp:1829)动态生成的,完成对PreStubWorker函数调用的封装。而PreStubWorker函数会调用JIT完成真正的函数编译工作,并将方法表的入口改为指向编译后函数体的jmp指令。具体的流程请参考DonBox在《.NET本质论第1卷:公共语言运行库》的第六章中的介绍,这里就不再罗嗦了。以后有机会再写篇文章详细分析一下JIT的工作流程。

在JIT处理flier.EntryPoint.m1时,用g命令执行,再回头来分析m1函数的入口,就会发现如前面所述,调用JIT过程的call指令变成了直接调用Native函数体的jmp指令。:D


这一小节,我们介绍了使用WinDbg跟踪调试CLR程序的一遍流程,并了解了对堆栈、对象和类信息进行分析的SOS命令,希望大家能够借此开始探索CLR内部世界的旅程。:P

JasonZander在其BLog的一篇文章,SOSDebuggingwiththeCLR(Part1),里面也详细介绍了使用WinDbg和SOS调试CLR程序的部分方法,值得一看。


你可能感兴趣的:(.net技术,CLR)