分享
 
 
 

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

王朝system·作者佚名  2006-01-09
窄屏简体版  字體: |||超大  

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

本来想按照 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\Framework\v1.1.4322\mscorwks.dll"

Thread 0

ESP EIP

0012f784 791d6a4a [FRAME: PrestubMethodFrame] [DEFAULT] [hasThis] Void System.AppDomain.SetupDomain(ValueClass System.LoaderOptimization,String,String)

0012f9a8 791d6a4a [FRAME: GCFrame]

0012fad0 791d6a4a [FRAME: DebuggerClassInitMarkFrame]

0012fa94 791d6a4a [FRAME: GCFrame]

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

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

以下为引用:

0:000> !clrstack

Thread 0

ESP EIP

0012f68c 791d6a4a [FRAME: PrestubMethodFrame] [DEFAULT] [hasThis] Void flier.EntryPoint.m1()

0012f69c 06d90080 [DEFAULT] Void flier.EntryPoint.Main()

0012f9b0 791da717 [FRAME: GCFrame]

0012fa94 791da717 [FRAME: GCFrame]

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

以下为引用:

0:000> !dumpstackobjects

ESP/REG Object Name

ecx 04aa1a90 flier.EntryPoint

0012f678 04aa1a90 flier.EntryPoint

0012f67c 04aa1a90 flier.EntryPoint

0012f680 04aa1a90 flier.EntryPoint

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

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

以下为引用:

bp mscorwks!PreStubWorker // 设置代码断点

g // 继续运行至断点

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

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

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

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

以下为引用:

0:000> !dumpobj 04aa1a90

Name: flier.EntryPoint

MethodTable 0x009750a8

EEClass 0x06c632e8

Size 12(0xc) bytes

mdToken: 02000002 (D:\Temp\demo.exe)

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

以下为引用:

0:000> !dumpclass 0x6c632e8

Class Name : flier.EntryPoint

mdToken : 02000002 ()

Parent Class : 79b7c3c8

ClassLoader : 00153850

Method Table : 009750a8

Vtable Slots : 4

Total Method Slots : 8

Class Attributes : 100000 :

Flags : 1000003

NumInstanceFields: 0

NumStaticFields: 0

ThreadStaticOffset: 0

ThreadStaticsSize: 0

ContextStaticOffset: 0

ContextStaticsSize: 0

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

以下为引用:

0:000> !dumpmt -md 0x09750a8

EEClass : 06c632e8

Module : 0014e090

Name: flier.EntryPoint

mdToken: 02000002 (D:\Temp\demo.exe)

MethodTable Flags : 80000

Number of IFaces in IFaceMap : 0

Interface Map : 009750f4

Slots in VTable : 8

--------------------------------------

MethodDesc Table

Entry MethodDesc JIT Name

79b7c4eb 79b7c4f0 None [DEFAULT] [hasThis] String System.Object.ToString()

79b7c473 79b7c478 None [DEFAULT] [hasThis] Boolean System.Object.Equals(Object)

79b7c48b 79b7c490 None [DEFAULT] [hasThis] I4 System.Object.GetHashCode()

79b7c52b 79b7c530 None [DEFAULT] [hasThis] Void System.Object.Finalize()

0097506b 00975070 None [DEFAULT] [hasThis] Void flier.EntryPoint.m1()

0097507b 00975080 None [DEFAULT] [hasThis] Void flier.EntryPoint.m2()

0097508b 00975090 None [DEFAULT] Void flier.EntryPoint.Main()

0097509b 009750a0 None [DEFAULT] [hasThis] Void flier.EntryPoint..ctor()

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

以下为引用:

0:000> u 79b7c4eb

mscorlib_79980000+0x1fc4eb:

79b7c4eb e8909cfeff call mscorlib_79980000+0x1e6180 (79b66180)

79b7c4f0 0000 add [eax],al

79b7c4f2 0080d86206c0 add [eax+0xc00662d8],al

79b7c4f8 06 push es

79b7c4f9 00fc add ah,bh

79b7c4fb e8809cfeff call mscorlib_79980000+0x1e6180 (79b66180)

79b7c500 07 pop es

79b7c501 0010 add [eax],dl

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

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

以下为引用:

0:000> !DumpMD 0x00975070

Method Name : [DEFAULT] [hasThis] Void flier.EntryPoint.m1()

MethodTable 9750a8

Module: 14e090

mdToken: 06000001 (D:\Temp\demo.exe)

Flags : 0

IL RVA : 00002050

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

以下为引用:

0:000> !DumpMD 0x009750a0

Method Name : [DEFAULT] [hasThis] Void flier.EntryPoint..ctor()

MethodTable 9750a8

Module: 14e090

mdToken: 06000004 (D:\Temp\demo.exe)

Flags : 0

Method VA : 06d900a8

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

以下为引用:

!dumpobj 04aa1a90 // 查看对象的详细信息

!dumpclass 0x6c632e8 // 查看类型的详细信息

!dumpmt -md 0x09750a8 // 查看方法表的详细信息

!dumpmd 0x00975070 // 查看方法表项的方法描述的详细信息

u 0x79b7c4eb // 反汇编指定地址的指令

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

以下为引用:

0:000> u 0097509b

0097509b e908b04106 jmp 06d900a8

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

以下为引用:

0:000> u 0x0097506b

0097506b e878427dff call 001492e8

0:000> u 0x0097507b

0097507b e868427dff call 001492e8

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

以下为引用:

0:000> u 0x001492e8

001492e8 52 push edx

001492e9 68f0301b79 push 0x791b30f0

001492ee 55 push ebp

001492ef 53 push ebx

001492f0 56 push esi

001492f1 57 push edi

001492f2 8d742410 lea esi,[esp+0x10]

001492f6 51 push ecx

001492f7 52 push edx

001492f8 648b1d2c0e0000 mov ebx,fs:[00000e2c]

001492ff 8b7b08 mov edi,[ebx+0x8]

00149302 897e04 mov [esi+0x4],edi

00149305 897308 mov [ebx+0x8],esi

00149308 56 push esi

00149309 e83cd70879 call mscorwks!PreStubWorker (791d6a4a)

0014930e 897b08 mov [ebx+0x8],edi

00149311 894604 mov [esi+0x4],eax

00149314 5a pop edx

00149315 59 pop ecx

00149316 5f pop edi

00149317 5e pop esi

00149318 5b pop ebx

00149319 5d pop ebp

0014931a 83c404 add esp,0x4

0014931d 8f0424 pop [esp]

00149320 c3 ret

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

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

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

Jason Zander在其 BLog 的一篇文章,SOS Debugging with the CLR (Part 1),里面也详细介绍了使用 WinDbg 和 SOS 调试 CLR 程序的部分方法,值得一看。

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有