一、简介
这是2024新年后我的第一篇文章,也是我的《Advanced .Net Debugging 》这个系列的第二篇文章。这篇文章告诉我们为了进行有效的程序调试,我们需要掌握哪些知识。言归正传,无论采取什么形式来分析问题,对被调试系统的底层了解的越多,就越有可能成功的找出问题的根源。在 Net 领域,同样适用,即我们需要理解【运行时 Runtime】本身的各种功能和行为。了解了垃圾收集器的工作原理将使你在调试内存泄漏问题是更加高效。了解了互用性的工作原理将使你在调试COM问题时更加高效,而了解了同步机制的工作原理将使你在调试挂起问题时更加高效。今天我们就来了解一些 Net 底层的东西,让我们做到知其然知其所以然。
如果在没有说明的情况下,所有代码的测试环境都是 Net 8.0,如果有变动,我会在项目章节里进行说明。好了,废话不多说,开始我们今天的调试工作。
调试环境我需要进行说明,以防大家不清楚,具体情况我已经罗列出来。
操作系统:Windows Professional 10
** 调试工具:Windbg Preview(Debugger Client:1.2306.1401.0,Debugger engine:10.0.25877.1004)** 下载地址:可以去Microsoft Store 去下载 ** 开发工具:Microsoft Visual Studio Community 2022 (64 位) - Current版本 17.8.3**
** Net 版本:.Net 8.0**
** CoreCLR源码:源码下载**
二、调试源码
废话不多说,本节是调试的源码部分,没有代码,当然就谈不上测试了,调试必须有载体。
2.1、ExampleCore_2_1_1
1 using System.Diagnostics;
2
3 namespace ExampleCore_2_1_1
4 {
5 internal class Program
6 {
7 static void Main(string[] args)
8 {
9 Console.WriteLine("Welcome to Advanced .Net Debugging!");
10 Debugger.Break();
11 }
12 }
13 }
View Code
2.2、ExampleCore_2_1_2
1 using System.Diagnostics;
2
3 namespace ExampleCore_2_1_2
4 {
5 internal class Program
6 {
7 static void Main(string[] args)
8 {
9 TypeSample sample = new TypeSample(10, 5, 10);
10 Debugger.Break();
11 sample.AddCoordinates();
12 Console.ReadLine();
13 }
14 }
15
16 /// <summary>
17 /// 引用类型
18 /// </summary>
19 public class TypeSample
20 {
21 public TypeSample(int x, int y, int z)
22 {
23 coordinates.x = x;
24 coordinates.y = y;
25 coordinates.z = z;
26 }
27
28 /// <summary>
29 /// 值类型
30 /// </summary>
31 private struct Coordinates
32 {
33 public int x;
34 public int y;
35 public int z;
36 }
37
38 private Coordinates coordinates;
39
40 public void AddCoordinates()
41 {
42 int hashCode = GetHashCode();
43 lock (this)
44 {
45 Debugger.Break();
46 Coordinates tempCoord;
47 tempCoord.x = coordinates.x + 100;
48 tempCoord.y = coordinates.y + 50;
49 tempCoord.z = coordinates.z + 100;
50
51 Console.WriteLine("x={0},y={1},z={2}", tempCoord.x, tempCoord.y, tempCoord.z);
52 }
53 }
54 }
55 }
View Code
三、基础知识和眼见为实
1、高层预览
从高层面上看来,Net 是一个虚拟的运行时环境,它包含了一个虚拟执行引擎---通用语言运行时(CLR)及其一组相关的框架库。我们通过一幅图来仔细查看和理解一下 Net 由那些组件构成的,效果如图:
我们从这张图上可以看到 Net 分成四块,由里到外:ECMA、CLR、NET Framework、Net Application。
ECMA: Net 的核心就是 ECMA 标准,表示 NET 运行时的所有实现都必须遵从这个标准,说白了,它就是一个规范,如果有人想实现自己的运行时,就按这个标准来肯定没问题。描述这个标准的文档叫通用语言基础架构(CLI)。CLI不仅为运行时本身定义了一组规则,还包括一组非常关键的通用类库,这组类库被称为【基础类型库(Base Class Libraries,BCL)】。
CLR: 它的中文名称叫【通用语言运行时】,它就是 Microsoft 对 CLI 的实现。
Net 框架: 该框架包含了开发人员在编写 Net 应用程序时需要使用的所有库。比如:WCF、WPF、Web MVC、Web API、WinForm 等。
Net应用程序: 我们自己编写的各种应用程序了,比如:MES、ERP、CMS等。
我们知道了Net 的组成,也要知道Net的执行模型。效果如图:
Net 程序源代码就是码农门写的东西,这些源代码经过编译器生成一种中间语言MSIL。非托管应用程序的源代码在被编译和链接之后将直接转换为特定于 CPU 的指令,而MSIL 则不同,它是一种与平台无关的更高级的语言。编译的输出结果就是程序集。当 Net 程序集运行时,CLR将被自动加载并开始执行MSIL,MSIL代码将被 JIT(即时编译器)转换为机器指令,来完成其功能。
2、CLR 和 Windows 加载器
托管程序和非托管程序是不一样的,非托管程序可以直接执行,但是 Net 的托管程序有两次编译,第一次编译获取的程序集不是机器码,不能直接执行的。那为什么 Windows 加载器可以将托管程序和非托管程序采取一样的启动方式呢?答案就是依赖 Windows 上的一种文件格式:可移植的可执行的文件格式(Portable Executable,PE)。PE映像文件一般结构如图:
为了支持 PE 映像的执行,在PE的头部包含了叫做【AddressOfEntryPoint】的域,这个域表示PE文件的入口点位置。我们可以使用 PPEE 工具查看PE文件信息。PPEE 是用来查看 PE 文件格式的工具,使用很简单,菜单也很少。官网下载地址:https://www.mzrst.com/ 眼见为实:了解 PE 文件
调试源码:ExampleCore_2_1_1
这个项目不需要实际的代码,大家可以根据自己喜欢建立,使用 PPEE 文件打开我们编译而成的 EXE 文件,在左侧有很多节点,我们可以依次点击【NT Header】--->【Optional Header】,在右侧就可以看到有一个栏目:AddressOfEntryPoint,因为我的程序是 Net 程序,Comment 里面显示的.text,在 Net 程序集中,这个值指向 .text 段中的一小段存根(stub)代码。 如图:
在PE头文件还有一个域,是【DIRECTORY_ENTRY_COM_DESCRIPTOR】,这个域是专门为支持 Net 应用程序增加的,它的图标也是【Net】,在这个域中包含了许多的信息,比如:托管代码应用程序的入口点(EntryPointToken),目标 CLR 的主版本号(MajorRuntimeVersion)和从版本号(MinorRuntimeVersion),以及程序集的强名称签名(StrongNameSignature)等。如图:
在【DIRECTORY_ENTRY_COM_DESCRIPTOR】这个域下还有一个节点是【MetaData】,这个节点包含的是Net 程序包含的元数据结构信息,【DIRECTORY_ENTRY_COM_DESCRIPTOR】本身的【MetaData】字段包含了一个【.text】内容,在【.text】段中包含了程序集的元数据表,MSIL以及非托管启动存根代码。非托管启动存根代码包含了有 Windows 加载器执行以启动 PE 文件执行的代码。如图:
有了这些信息,Windows 可以知道要加载哪个版本的 CLR以及关于程序集本身的一些最基本信息。
2.1、加载非托管映像
我们看看 Windows 加载器是如何加载非托管的 PE 映像的。我们以 notepad.exe 为例(它的路径:C:\Windows\notepad.exe)。我们需要查看 notepad 的 PE 文件,如果想查看它的 PE文件,我们必须提前准备 PPEE.exe 工具,它是专门用于查看 PE 文件的工具。
【应用场合】查看 Windows 应用程序的 PE 文件。
【下载地址】https://www.mzrst.com/
【软件版本】1.12
在开始操作之前,我们先来了解2个概念:文件偏移(file offset)和相对虚地址(Relative Virtual Address)。
文件偏移(file offset) :指 PE 文件中任意位置的偏移量。
相对虚地址(Relative Virtual Address) :仅当 PE 映像已经被加载后才需要使用这个值,它是在进程虚拟地址空间中的相对地址。
我们使用 PPEE.exe 文件打开 notepad.exe 文件,当然,也可以直接将 notepad.exe 文件拖到 PPEE 工具的空白区,就会打开 notepad.exe 软件的 PE 文件,效果如图:
我们在 PPEE 工作左侧,依次点击【NT Header】--->【Optional Header】,在右侧我们就能看到【AddressOfEntryPoint】域,它的值是:00023BE0,这个值就是 RVA 的值,我们可以使用 Windbg 加载 notepad.exe,使用【lm】命令查看加载的所有模块。
1 0:007> lm
2 start end module name
3 00007ff6`895a0000 00007ff6`895d8000 notepad (pdb symbols) C:\ProgramData\Dbg\sym\notepad.pdb\FF9C9991EA5CB351AF10D24FCBA2CE391\notepad.pdb
4 00007ffe`f9b90000 00007ffe`f9c6c000 efswrt (deferred)
5 00007fff`19aa0000 00007fff`19b06000 oleacc (deferred)
6 00007fff`1b030000 00007fff`1b2ca000 COMCTL32 (deferred)
7 00007fff`24400000 00007fff`244fc000 textinputframework (deferred)
8 00007fff`24650000 00007fff`24744000 MrmCoreR (deferred)
9 00007fff`263c0000 00007fff`2646e000 TextShaping (deferred)
10 00007fff`278b0000 00007fff`27ab2000 twinapi_appcore (deferred)
11 00007fff`29240000 00007fff`2925d000 MPR (deferred)
12 00007fff`2c0f0000 00007fff`2c246000 wintypes (deferred)
13 00007fff`2c850000 00007fff`2cbaa000 CoreUIComponents (deferred)
14 00007fff`2cbb0000 00007fff`2cca2000 CoreMessaging (deferred)
15 00007fff`2cfc0000 00007fff`2d05f000 uxtheme (deferred)
16 00007fff`2e120000 00007fff`2e8aa000 windows_storage (deferred)
17 00007fff`2f9f0000 00007fff`2fa23000 ntmarta (deferred)
18 00007fff`306b0000 00007fff`306db000 Wldp (deferred)
19 00007fff`30ca0000 00007fff`30f67000 KERNELBASE (deferred)
20 00007fff`30f70000 00007fff`30f92000 win32u (deferred)
21 00007fff`30fa0000 00007fff`3103d000 msvcp_win (deferred)
22 00007fff`31230000 00007fff`3133a000 gdi32full (deferred)
23 00007fff`31500000 00007fff`3157f000 bcryptPrimitives (deferred)
24 00007fff`31650000 00007fff`31750000 ucrtbase (deferred)
25 00007fff`31750000 00007fff`31763000 kernel_appcore (deferred)
26 00007fff`31780000 00007fff`3181e000 msvcrt (deferred)
27 00007fff`31880000 00007fff`31928000 clbcatq (deferred)
28 00007fff`31930000 00007fff`31c84000 combase (deferred)
29 00007fff`31c90000 00007fff`31d2b000 sechost (deferred)
30 00007fff`31ef0000 00007fff`31f5b000 WS2_32 (deferred)
31 00007fff`31f60000 00007fff`32100000 USER32 (deferred)
32 00007fff`32230000 00007fff`32305000 OLEAUT32 (deferred)
33 00007fff`32880000 00007fff`3292a000 ADVAPI32 (deferred)
34 00007fff`32930000 00007fff`329ed000 KERNEL32 (pdb symbols) C:\ProgramData\Dbg\sym\kernel32.pdb\85A257DB4B7B82F2E19AD96AB7BB116A1\kernel32.pdb
35 00007fff`32a10000 00007fff`32a65000 shlwapi (deferred)
36 00007fff`32a70000 00007fff`32a9a000 GDI32 (deferred)
37 00007fff`32aa0000 00007fff`32bb5000 MSCTF (deferred)
38 00007fff`32bf0000 00007fff`32c9e000 shcore (deferred)
39 00007fff`32e30000 00007fff`32e60000 IMM32 (deferred)
40 00007fff`32e60000 00007fff`32f83000 RPCRT4 (deferred)
41 00007fff`33140000 00007fff`33871000 SHELL32 (deferred)
42 00007fff`338d0000 00007fff`33ac4000 ntdll (pdb symbols) C:\ProgramData\Dbg\sym\ntdll.pdb\63E12347526A46144B98F8CF61CDED791\ntdll.pdb
View Code
【00007ff6`895a0000-00007ff6`895d8000 notepad 】这行信息就是加载 notepad.exe 的起始和结束地址空间,也就是说 notepad.exe 实例被加载在地址 00007ff6`895a0000 处。我们又知道了 notepad.exe 的【AddressOfEntryPoint】的 RVA 值(00023BE0),用开始地址(也叫基地址:00007ff6`895a0000)加上 RVA 值就是 notepad.exe 的【wWinMainCRTStartup】的地址,这个函数也就是应用程序的入口点。我们可以使用 【u 00007ff6`895a0000+00023BE0】命令证明一下。
1 0:007> u 00007ff6`895a0000+00023BE0
2 notepad!wWinMainCRTStartup:
3 00007ff6`895c3be0 4883ec28 sub rsp,28h
4 00007ff6`895c3be4 e86b090000 call notepad!_security_init_cookie (00007ff6`895c4554)
5 00007ff6`895c3be9 4883c428 add rsp,28h
6 00007ff6`895c3bed e96efeffff jmp notepad!__scrt_common_main_seh (00007ff6`895c3a60)
7 00007ff6`895c3bf2 cc int 3
8 00007ff6`895c3bf3 cc int 3
9 00007ff6`895c3bf4 cc int 3
10 00007ff6`895c3bf5 cc int 3
【wWinMainCRTStartup】是一个外层包装函数,它在调用 Notepad.exe 的 WinMain 函数之前执行一些 CRT 初始化工作。这就是 Windows 加载器在加载映像的过程中用到的信息,当然,PE 文件中还包含大量其他信息,这个过程也说明了 Windows 加载器是如何找到并执行 PE 映像文件的入口点。
2.2、加载 Net 程序集。
如果想观察 Windows 加载器是如何加载 Net 程序集的,最简单的办法就是观察一个简单的 NET 命令行程序。由于 NET 应用程序在执行时要预先加载 CLR,那 Windows 是如何加载并初始化 CLR的。这就要提到之前说过的 PE 文件了,微软对 PE 文件进行了扩展。前面提到过,PE 格式是 Windows 可执行程序的文件格式,用来管理 PE 文件中代码的执行。为了支持 NET 程序,在 PE 文件格式中增加了对程序集的支持。
如图:
我在这里再次强调一次,当我们使用 PPEE 工具查看 PE 文件的时候,必须使用 .DLL,不要使用 .EXE,只有在 .DLL 的 PE 文件里才有针对 NET 扩展的数据结构。
测试项目 :ExampleCore_2_1_1
为了更好的说明这些概念,我使用了一个工具【dumpbin.exe】,它能够解析 PE 文件格式并且以简洁易读的形式转储 PE 文件的内容。官网下载地址:https://github.com/Delphier/dumpbin。对于我们而言,一般不需要独立下载该工具,【dumpbin.exe】这个工具是包含在 MSVC 工具集中的,如果我们在安装了 Visual Studio 的时候,并选择【使用 C++ 桌面开发】工作负载,Visual Studio 安装完成,【dumpbin.exe】工具也已经安装好了。
如图:
如果没有安装或者不会使用,我们也可以在微软的网站上找到解决办法:https://learn.microsoft.com/zh-cn/cpp/build/building-on-the-command-line?view=msvc-170,我们安装好 Visual Studio 2022 后,在开始菜单里就可以找到 Visual Studio 的安装文件夹,里面包含很多【命令行工具】,这些工具就可以使用【dumpbin.exe】工具。
安装目录,如图:
运行效果,如图:
我们打开【开发者命令提示(C)】,执行【dumpbin /all ExampleCore_2_1_1.dll】命令,效果如图:
我们在【OPTIONAL HEADER VALUES】这个节,可以看到【2796 entry point (00402796)】这个值,这就是入口点。如图:
当然,我们也可以使用 PPEE 工具查看,它看的更清楚一点。我们将【ExampleCore_2_1_1.dll】文件拖到 PPEE.exe 应用中,我们在【HT Hheader】-->【Optional Header】节中看到【AddressOfEntryPoint】域的值是:00002796,效果如图:
上面的 Entry point 域对应于 PE 文件中的 【AddressOfEntryPoint】,值为:0x00402796。要找出位置 0x00402796 所对应的代码,需要查看 PE 文件映像中的 .text 段,具体来说就是如下面截图中的【RAW DATA】段:
1 00402750: 00 00 00 20 00 00 00 00 00 00 00 00 00 00 00 00 ... ............
2 00402760: 00 00 00 00 00 00 00 00 00 00 76 27 00 00 00 00 ..........v'....
3 00402770: 00 00 00 00 00 00 00 00 5F 43 6F 72 45 78 65 4D ........_CorExeM
4 00402780: 61 69 6E 00 6D 73 63 6F 72 65 65 2E 64 6C 6C 00 ain.mscoree.dll.
5 00402790: 00 00 00 00 00 00 FF 25 00 20 40 00 ......?%. @.
上面信息中的【FF 25 00 20 40 00】字节对应于【AddressOfEntryPoint】域,这些字节对应的机器指令为:JMP 402000。
0x402000 是什么意思?事实上,0x402000 指向的是 PE 映像文件中的 import 段。在这个段中列出的是 PE 文件依赖的所有模块。效果如图:
在加载时,系统将修正导入函数的实际地址,并执行正确的调用。要找到【0x402000】指向的内容,我们可以查看 PE 文件的导入段,可以发现一下内容(dumpbin.exe):
1 Section contains the following imports:
2
3 mscoree.dll
4 402000 Import Address Table
5 40276A Import Name Table
6 0 time date stamp
7 0 Index of first forwarder reference
8
9 0 _CorExeMain
可以看到,0x402000 指向的是 mscoree.dll(Microsoft 对象运行时执行引擎,Microsoft Object Runtime Execution Engine),这个库中包含了一个导出函数_CorExeMain。然后,前面的 JMP 指令可以转换为一下伪码:JMP _CorExeMain。
我们已经看到了,_CorExeMain 是 mscoree.dll 的一部分,这个函数也是在加载 NET 程序集时第一个被调用的函数。mscoree.dll(和_CorExeMain)的主要作用就是启动 CLR。mscoree.dll 在启动 CLR 时将执行一些列工作:
(1)、查看 PE 文件中的元数据,找出 NET 程序集是基于哪个版本的 CLR 创建的。
** (2)、找到操作系统中正确版本 CLR 的路径。**
** (3)、开始加载并初始化 CLR。**
当 CLR 被成功加载并初始化后,在PE 映像的CLR 头中就可以找到程序集的入口点(Main()),然后,JIT 开始编译并执行入口点。我们可以使用 PPEE.exe 工具查看一下【ExampleCore_2_1_1.dll】的 PE映像文件中有关 Net 元数据的扩展。效果如图:
以上我们说的 CLR都是概念的东西,接下来,我就展示一下它是一个真实存在的东西。在任何一台机器上都有可能存在多个版本的 clr.dll,以下就是32位 4.0版本的 clr.dll,是64位 4.0版本的 clr.dll,其他版本大家自己去找吧,很容易的。
C:\Windows\Microsoft.NET\Framework\v4.0.30319\clr.dll
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\clr.dll
mscoree.dll 的作用就是通过查看 PE 映像文件的 CLR 头来找出程序集需要使用哪个版本的 CLR。具体来说,就是它主要查看两个域,分别是:MajorRuntimeVersion 和 MinorRuntimeVersion,就可以加载正确版本的 CLR。
我们总结 Net 程序集加载算法如下:
** (1)、用户执行一个 NET 应用程序。**
** (2)、Windows 加载器查看【Optional Header】中的【AddressOfEntryPoint】域,并找到 PE 映像文件的 .text 段。**
** (3)、位于【AddressOfEntryPoint】域上 .text 的内容就是 JMP 字节指令,这个指令会加载【mscoree.dll】中的一个导入函数,函数名是:_CorExeMain。**
** (4)、将执行控制权转移到 【mscoree.dll】中的【_CorExeMain】中,这个函数将启动并加载CLR,并且找到程序集的入口点,最后把执行的控制权转移到程序集的入口点,开始执行我的程序。**
3、应用程序域
我们知道 Windows 系统为了提升系统的稳定性和可靠性,实现了进程级别的隔离。同理,.Net 应用程序也是同样被限制在进程内执行。但是不同的是,.Net 引入了另一种逻辑隔离层,那就是我们通常所说的【引用程序域】。我们通过一张图片,看看进行和应用程序域的关系。效果如图:
在图中,我已说明,在.Net 跨平台版本里是没有【共享应用程序域】了,大家需要注意。
在任何启动了【CLR】的 Windows 进程中都会定义一个或者多个应用程序域。通常来说,应用程序域对于应用程序是透明的,大多数应用程序都不会显示的创建应用程序域。为了使运行的应用程序不会对系统的其他部分造成破坏,这些代码将会加载到自己的应用程序域中。对于没有显示创建应用程序域的应用程序来说,CLR 在加载的时候将创建2类应用程序(在.Net Framework 版本里创建3类应用程序域:System Domian、Shared Domian、Default Domain),换句话说,启动了 CLR 的进程在运行时至少拥有两类应用程序域(这种情况是 .Net 跨平台版本,.Net Framework 版本是三类应用程序域,因为我这个系列是使用的跨平台的版本,以后就不说明了)。
眼见为实:查看引用程序域
调试源码:ExampleCore_2_1_1
我们打开 Windbg Preview,通过【File】-->【Launch executable】,加载我们的编译好的 ExampleCore_2_1_1.exe 文件。由于我们使用的最新的调试工具,它会自动加载 SOS.dll,当我们成功加载了 ExampleCore_2_1_1.exe 文件,这个时候 Windbg Preview,并没有加载 SOS.DLL,你可以通过【.chain】命令验证。我们通过【g】命令继续调试器,我们的应用程序输出:Welcome to Advanced .Net Debugging!,效果如图:
程序在【Debugger.Break();】暂停,我们点击【Break】按钮,进入调试模式,可以使用【.chain】命令,就可以看到 SOS.DLL 已经加载了。
1 0:000> .chain
2 Extension DLL search Path:
3 C:\Program Files\WindowsApps\Microsoft.WinDbg_1.2306.14001.0_x64__8wekyb3d8bbwe\amd64\WINXP;..\.dotnet\tools
4 Extension DLL chain:
5 sos: image 7.0.430602, API 2.0.0, built Wed Jun 7 08:01:54 2023
6 [path: C:\Program Files\WindowsApps\Microsoft.WinDbg_1.2306.14001.0_x64__8wekyb3d8bbwe\amd64\winext\sos\sos.dll]
7 CLRComposition: image 10.0.25877.1004, API 0.0.0,
8 [path: C:\Program Files\WindowsApps\Microsoft.WinDbg_1.2306.14001.0_x64__8wekyb3d8bbwe\amd64\winext\CLRComposition.dll]
9 JsProvider: image 10.0.25877.1004, API 0.0.0,
10 [path: C:\Program Files\WindowsApps\Microsoft.WinDbg_1.2306.14001.0_x64__8wekyb3d8bbwe\amd64\winext\JsProvider.dll]
11 DbgModelApiXtn: image 10.0.25877.1004, API 0.0.0,
12 [path: C:\Program Files\WindowsApps\Microsoft.WinDbg_1.2306.14001.0_x64__8wekyb3d8bbwe\amd64\winext\DbgModelApiXtn.dll]
13 dbghelp: image 10.0.25877.1004, API 10.0.6,
14 [path: C:\Program Files\WindowsApps\Microsoft.WinDbg_1.2306.14001.0_x64__8wekyb3d8bbwe\amd64\dbghelp.dll]
15 exts: image 10.0.25877.1004, API 1.0.0,
16 [path: C:\Program Files\WindowsApps\Microsoft.WinDbg_1.2306.14001.0_x64__8wekyb3d8bbwe\amd64\WINXP\exts.dll]
17 uext: image 10.0.25877.1004, API 1.0.0,
18 [path: C:\Program Files\WindowsApps\Microsoft.WinDbg_1.2306.14001.0_x64__8wekyb3d8bbwe\amd64\winext\uext.dll]
19 ntsdexts: image 10.0.25877.1004, API 1.0.0,
20 [path: C:\Program Files\WindowsApps\Microsoft.WinDbg_1.2306.14001.0_x64__8wekyb3d8bbwe\amd64\WINXP\ntsdexts.dll]
当然,我们也可以使用【!sos.help】命令,查看所有的 SOS 的命令。为了查看应用程序域,我们可以使用【!dumpdomain】命令。
1 0:000> !dumpdomain
2 --------------------------------------
3 System Domain: 00007ffa16d19040
4 LowFrequencyHeap: 00007FFA16D19518
5 HighFrequencyHeap: 00007FFA16D195A8
6 StubHeap: 00007FFA16D19638
7 Stage: OPEN
8 Name: None
9 --------------------------------------
10 Domain 1: 00000252bb908650
11 LowFrequencyHeap: 00007FFA16D19518
12 HighFrequencyHeap: 00007FFA16D195A8
13 StubHeap: 00007FFA16D19638
14 Stage: OPEN
15 Name: clrhost
16 Assembly: 00000252bb8ce240 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.0\System.Private.CoreLib.dll]
17 ClassLoader: 00000252BB8CE2D0
18 Module
19 00007ff9b6d24000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.0\System.Private.CoreLib.dll
20
21 ..................(省略无用的信息)
System Domain 就是系统级应用程序域,Domain 1就是我们默认的应用程序域。如果大家想查看 .Net Framework 版本的应用程序,就能看到3个,自己可以试试,我就不写了。
输出的内容,我们必须了解一下。
(1)、指向应用程序域的指针。这个参数可以作为【!dumpdomain】命令的输入参数,这样只输出指定应用程序域的信息。
我们获取【系统应用程序域】的信息。
1 0:000> !dumpdomain 00007ffa16d19040
2 --------------------------------------
3 System Domain: 00007ffa16d19040
4 LowFrequencyHeap: 00007FFA16D19518
5 HighFrequencyHeap: 00007FFA16D195A8
6 StubHeap: 00007FFA16D19638
7 Stage: OPEN
8 Name: None
(2)、LowFrequencyHeap、HighFrequencyHeap 和 StubHeap,通常,每个应用程序域都有相关 MSIL 代码,在 JIT 编译 MSIL 代码的过程中,需要保存与编译过程相关的数据,比如:编译生成的机器代码和方发表等。因此,每个应用程序域都需要创建一定数量的堆来存储这些数据。LowFrequencyHeap 在这个堆中包含的是一些较少被更新或被访问的数据,而 HighFrequencyHeap 堆中包含的是经常被频繁访问的数据。StubHeap 堆包含的是 CLR 执行互用性调用时需要的辅助数据。
(3)、在应用程序域中加载的所有程序集。
3.1、系统应用程序域
1)、可以创建其他的应用程序域(共享应用程序域(Net Framework 版本)和默认应用程序域)。
2)、将 mscorlib.dll 加载到共享应用程序域中。
3)、记录进程中所有其他的应用程序域,包括提供加载和卸载应用程序的功能。
4)、记录字符串池中字符串常量,因为允许任意字符串在每一个进程中都存在一个副本。
5)、初始化特性类型的异常,例如:内存耗尽异常、栈溢出异常以及执行引擎异常等。
3.2、共享应用程序域
共享应用程序域这个域在书中有,我就保留了,但是,在 .Net 版本里已经没有这个应用程序域了。在 .net framework 版本里是存在的,在这个域中包含的是一些通用的代码,mscorlib.dll 被加载到这个应用程序域中,此外还包括在 System 命名空间下的一些基本类型(enum、String、ValueType、Array等),在大多数情况下,非用户代码将被加载到这个域中。
3.3、默认应用程序域
这个域中就是我们的应用程序生存的地方,位于默认应用程序域中的所有代码都只有在这个域中才有效。
4、程序集简介
程序集是 .Net 程序的主要构件和部署单元,.Net 的程序集是自包含的,也可以说是自描述的。程序集的自包含性对于消除 DLL Hell起到了积极作用。
共有两类程序集:
1)、共享程序集:指可以在不同应用程序中使用的程序集。由于共享程序集可以跨越不同的应用程序,所以必须是【强命名】的。通常,共享程序集被安装到全局程序集缓存中(GAC:Global Assembly Cache)。
2)、私有程序集:指属于特性应用程序或者组件的程序集。当加载私有程序集时,它通常只会局限于某个应用程序域中。
当加载私有程序集时,要么被加载到默认应用程序域中,要么是被加载到显示创建的应用程序域中。我们可以使用【!dumpdomain】命令,查看系统的所有应用程序域,也会列出每个应用程序域加载的程序集,有了程序集的,我们就可以使用【!dumpAssembly】命令,查看程序集的详情。
我们先使用【!dumpdomain 00000252bb908650】命令查看指定应用程序域的信息,这个调试也是使用的【ExampleCore_2_1_1】项目。
1 0:000> !dumpdomain 00000252bb908650
2 --------------------------------------
3 Domain 1: 00000252bb908650
4 LowFrequencyHeap: 00007FFA16D19518
5 HighFrequencyHeap: 00007FFA16D195A8
6 StubHeap: 00007FFA16D19638
7 Stage: OPEN
8 Name: clrhost
9 Assembly: 00000252bb8ce240 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.0\System.Private.CoreLib.dll]
10 ClassLoader: 00000252BB8CE2D0
11 Module
12 00007ff9b6d24000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.0\System.Private.CoreLib.dll
红色标注的就是程序集的数据,我们就可以使用【!dumpAssembly】命令,查看程序集的详情。
1 0:000> !dumpassembly 00000252bb8ce240
2 Parent Domain: 00000252bb908650
3 Name: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.0\System.Private.CoreLib.dll
4 ClassLoader: 00000252BB8CE2D0
5 Module
6 00007ff9b6d24000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.0\System.Private.CoreLib.dll
除了显示程序集的名称,还显示了程序集的安全描述符和包含该程序集的父应用程序域的地址。
5、程序集清单
既然程序集是.Net 应用程序的基础构件并且是完全自描述的,这些子描述信息存储在程序集的元数据段,这些信息被称为程序集的清单。通常,程序集的清单位于 PE 文件中,但并非一定要在这个位置。比如:如果程序集包含了多个模块,程序集清单就会保存到一个独立的文件中(也就是程序集的 PE 文件只包含了清单),在这个文件中包含的是在加载和使用各个模块的时候所引用的数据。我们来一个图展示一下单文件程序集和多文件程序集有关程序集清单的区别,如图:
接下来,我们看看程序集清单包含了什么数据:
1)、需要依赖的非托管代码模块列表。
2)、要依赖的程序集列表。
3)、程序集的版本。
4)、程序集的公钥标记。
5)、程序集的资源。
6)、程序集的标志,比如:栈的预留空间、子系统等信息。
眼见为实之查看程序集清单:
查看工具:PPEE,ILDasm
调试项目:ExampleCore_2_1_1
1)、使用 ILDASM 查看程序集清单。
这本书中使用的是 ILDasm 工具,大家应该熟悉这个工具,想要查看反编译代码、元数据和程序集清单都会用到这个工具。使用起来也很简单,打开【Developer Command Prompt for vs 2022】命令行窗口,直接执行命令:ildasm assembliyName(程序集的名称,注意程序集的目录),就可以打开反编译窗口。这里需要说明一下,由于我使用的是 .Net 8.0 版本,使用的命令是【ildasm ExampleCore_2_1_1.dll】,这个名称是以 .dll 为后缀的,不是 .exe 的文件,如果是 .Net Framework 框架,则直接使用【ildasm ildasm ExampleCore_2_1_1.exe】命令。
执行命令,如图:
打开【IL DASM】窗口,如图:
双击【MANIFEST】打开程序集清单窗口,如图:
和
2)、我们在使用一下 PPEE 查看一下程序集的清单
使用 PPEE 查看程序集清单,很容易,而且看起来更清晰,更有条例。操作很简单,我们直接将我们的 .dll 文件拖到 PPEE 文件的主界面上,就可以打开我们程序集了。
效果如图:
当然里面还有很多其他项,大家可以自己点击进去看看,不是很难,我就不多说了。
6、类型的元数据
类型是 .Net 程序的基本编程单元,我们都知道的它又分为值类型和引用类型,微软为什么对数据类型进行这样的区分,主要的考虑是效率,毕竟在托管堆上分配数据、处理数据和销毁数据都是一个比较消耗资源的操作。我们上一张图,来看一下值类型和引用类型在内存上分配的区别。
眼见为实:查看值类型和引用类型
源码项目:ExampleCore_2_1_2
正确编译我们的项目,然后打开【Windbg Preview】,依次点击【File】-->【Launch executable】,选择我们的 EXE 文件,选择【打开】,加载我们的应用程序,并进入调试器页面。此时,我们的应用程序并没有执行。使用【g】命令,运行调试器,等我们的控制台程序输出:x=110,y=55,z=110,然后点击【Break】按钮,进入调试状态,就可以调试我们的应用程序了。如果调试没有在主线程,我们可以执行【~0s】,切换到主线程。
1 0:001> ~0s
2 ntdll!NtReadFile+0x14:
3 00007ffc`8c7aae54 c3 ret
我们使用【!clrstack -a】命令,查看当前托管程序的调用栈所有的参数和局部变量,-a 指包括参数和局部变量,-l 指只有局部变量。
1 0:000> !clrstack -a
2 OS Thread Id: 0x3e2c (0)
3 Child SP IP Call Site
4 0000000E9377E790 00007ffc8c7aae54 [InlinedCallFrame: 0000000e9377e790]
5 0000000E9377E790 00007ffbcff076eb [InlinedCallFrame: 0000000e9377e790]
......75
76 0000000E9377EAE0 00007ffb477519a5 ExampleCore_2_1_2.Program.Main(System.String[]) [E:\Visual Studio 2022\ExampleCore_2_1_2\Program.cs @ 9]
77 PARAMETERS:
78 args (0x0000000E9377EB30) = 0x000001a410808ea0
79 LOCALS:
80 0x0000000E9377EB18 = 0x000001a410809640
如图:
我们可以使用【!do 000001a410809640】命令验证是不是 sample 变量。
1 0:000> !do 0x000001a410809640
2 Name: ExampleCore_2_1_2.TypeSample
3 MethodTable: 00007ffb47809460(这里是方法表)
4 EEClass: 00007ffb47811f90
5 Tracked Type: false
6 Size: 32(0x20) bytes
7 File: E:\Visual Studio 2022\Source\...\ExampleCore_2_1_2\bin\Debug\net8.0\ExampleCore_2_1_2.dll
8 Fields:
9 MT Field Offset Type VT Attr Value Name
10 00007ffb47809408 4000001 8 ...ample+Coordinates 1 instance 000001a410809648 coordinates
【!dumpObj】命令的参数是引用类型实例的地址,它能显示这个实例对象所有信息。在前面的输出中我们可以看到这个类型包含一个域,偏移(Offset)是8个字节,类型是(Type)Coordinates,并且 VT (ValueType)列的值是1(0就是引用类型),说明是一个值类型。在 Value 列给出来这个域所在的地址。如果要显示引用类型对象中的各个域,可以再次使用【dumpObj】命令,如果是值类型,可以直接使用【dumpvc】命令。
解释如图:
我们既然知道了局部变量的地址,又知道它是值类型,我们直接使用【!dumpVC】命令查看它的详情。
1 0:000> !DumpVC /d 00007ffb4eae9408 0000028dd4c09648
2 Name: ExampleCore_2_1_2.TypeSample+Coordinates
3 MethodTable: 00007ffb4eae9408
4 EEClass: 00007ffb4eaf2008
5 Size: 32(0x20) bytes
6 File: E:\Visual Studio 2022\Source\...\ExampleCore_2_1_2\bin\Debug\net8.0\ExampleCore_2_1_2.dll
7 Fields:
8 MT Field Offset Type VT Attr Value Name
9 00007ffb4e9a1188 4000002 0 System.Int32 1 instance 10 x
10 00007ffb4e9a1188 4000003 4 System.Int32 1 instance 5 y
11 00007ffb4e9a1188 4000004 8 System.Int32 1 instance 10 z
该命令的格式:!DumpVC /d 00007ffb4eae9408(方法表的地址) 0000028dd4c09648(值类型的地址)。
我们知道了程序集是通过程序集清单来描述自己的,数据的类型是通过类型元数据描述的。在深入探讨类型元数据之前,还有一个问题需要解决,类型的实例在内存中是如何布局的。上图表示,图简单命令。
在托管堆上的每个实例对象都包含以下信息:
1)、同步块(sync block):同步块可以是一个位掩码,也可以是由 CLR维持的一个同步块表中的索引。它包含的是对象的辅助信息。
2)、类型句柄:它是 CLR 类型系统的基础,它可以对托管堆上的类型进行完整的描述。
3)、对象实例:在同步块和类型句柄之后就是实际的对象数据了。
6.1、同步块表
a)、基础知识:
在托管堆上的每个实例的前面都包含一个同步块索引,它指向 CLR 私有堆中的同步块表。在同步块表中包含的是执行各个同步块的指针,同步块可以包含很多信息,比如:对象的锁、互用性数据、引用程序域的索引、对象的散列码等。当然,也有可能在对象中不包含任何同步块数据,此时的同步块索引值是0。
b)、眼见为实:
一个对象在获取锁和没有获取锁的的同步块是什么样子?
源码项目:ExampleCore_2_1_2
首先在 Main() 方法中设置了一个中断,在 AddCoordinates() 方法中设置了一个中断,之所以要设置两个中断,是为了说明一个对象在没有获取任何锁或者获取了一个锁这两种情况下的不同。
我们首先编译我们的项目,打开【Windbg Preview】,依次点击【文件】-->【Launch executable】加载我们的可以执行程序。进入到调试器界面后,【g】继续运行,程序会在Main()方法第10行【Debugger.Break();】这行代码处暂停。效果如图:
我们现在可以查看线程的调用栈,使用【!clrstack -a】命令。
1 0:000> !clrstack -a
2 OS Thread Id: 0x3088 (0)
3 Child SP IP Call Site
4 000000A920B7E678 00007ffc89ba9202 [HelperMethodFrame: 000000a920b7e678] System.Diagnostics.Debugger.BreakInternal()
5 000000A920B7E780 00007ffba4e360aa System.Diagnostics.Debugger.Break() [/_/src/coreclr/...src/System/Diagnostics/Debugger.cs @ 18]
6
7 000000A920B7E7B0 00007ffb4e241998 ExampleCore_2_1_2.Program.Main(System.String[]) [E:\Visual Studio 2022\.\ExampleCore_2_1_2\Program.cs @ 10]
8 PARAMETERS:
9 args (0x000000A920B7E800) = 0x00000213fe808ea0
10 LOCALS:
11 0x000000A920B7E7E8 = 0x00000213fe809640
0x00000213fe809640红色标记的地址就是 TypeSample 引用类型的局部变量 sample 的地址。 这个地址指针指向的对象实例的起始位置,想要查看它的同步块索引,需要减去4个字节(DWORD)。继续使用【dd 0x00000213fe809640-0x4】命令查看数据。
1 0:000> dd 0x00000213fe809640-0x4
2 00000213`fe80963c 00000000 4e2f9460 00007ffb 0000000a
3 00000213`fe80964c 00000005 0000000a 00000000 00000000
4 00000213`fe80965c 00000000 00000000 00000000 00000000
5 00000213`fe80966c 00000000 00000000 00000000 00000000
6 00000213`fe80967c 00000000 00000000 00000000 00000000
7 00000213`fe80968c 00000000 00000000 00000000 00000000
8 00000213`fe80969c 00000000 00000000 00000000 00000000
9 00000213`fe8096ac 00000000 00000000 00000000 00000000
在对位置【0x00000213fe809640-0x4】进行转出输出时结果是0x0,也就是红色标记的值,这表示该对象并不包含相关的同步块索引。
接下来,我们继续使用【g】命令,运行调试器,会在 AddCoordinates() 方法内的第 45 行【Debugger.Break();】代码处暂停。效果如图:
我们再次执行【!clrstack -a】命令,查看线程托管代码的调用栈。
1 0:000> !clrstack -a
2 OS Thread Id: 0x3088 (0)
3 Child SP IP Call Site
4 000000A920B7E5F8 00007ffc89ba9202 [HelperMethodFrame: 000000a920b7e5f8] System.Diagnostics.Debugger.BreakInternal()
5 000000A920B7E700 00007ffba4e360aa System.Diagnostics.Debugger.Break() [/_/src/coreclr/System.Private.CoreLib/./Debugger.cs @ 18]
6
7 000000A920B7E730 00007ffb4e241acd ExampleCore_2_1_2.TypeSample.AddCoordinates() [E:\Visual Studio 2022\.\ExampleCore_2_1_2\Program.cs @ 45]
8 PARAMETERS:
9 this (0x000000A920B7E7B0) = 0x00000213fe809640
10 LOCALS:
11 0x000000A920B7E79C = 0x000000000378734a
12 0x000000A920B7E790 = 0x00000213fe809640
13 0x000000A920B7E788 = 0x0000000000000001
14 0x000000A920B7E778 = 0x0000000000000000
15
16 000000A920B7E7B0 00007ffb4e2419a5 ExampleCore_2_1_2.Program.Main(System.String[]) [E:\Visual Studio 2022\.\ExampleCore_2_1_2\Program.cs @ 11]
17 PARAMETERS:
18 args (0x000000A920B7E800) = 0x00000213fe808ea0
19 LOCALS:
20 0x000000A920B7E7E8 = 0x00000213fe809640
我们之所以再次使用使用【clrstack】命令,是因为在垃圾收集器的空闲期间,托管堆上的对象可能会被移动到其他位置。在输出结果中,地址没有变化,说明对象没有被移动。我们继续使用【dd 0x00000213fe809640-0x4】命令。
1 0:000> dd 0x00000213fe809640-0x4
2 00000213`fe80963c 08000001 4e2f9460 00007ffb 0000000a
3 00000213`fe80964c 00000005 0000000a 00000000 00000000
4 00000213`fe80965c 00000000 00000000 00000000 00000000
5 00000213`fe80966c 00000000 00000000 00000000 00000000
6 00000213`fe80967c 00000000 00000000 00000000 00000000
7 00000213`fe80968c 00000000 00000000 00000000 00000000
8 00000213`fe80969c 00000000 00000000 00000000 00000000
9 00000213`fe8096ac 00000000 00000000 00000000 00000000
这次我们看到了结果,红色加粗标注的 0x08000001,08表示这个同步块索引里包含了哈希值,因为我们调用了对象的 GetHashCode() 方法。红色尾部部分包含 1,说明对象包含一个同步块索引。
如果想看同步块索引表中08000001处内容,可以使用【!syncblk】命令,输出所有同步快表中所有的元素。
1 0:000> !syncblk
2 Index SyncBlock MonitorHeld Recursion Owning Thread Info SyncBlock Owner
3 1 00000213FA113E08 1 1 00000213FA0F6B50 3088 0 00000213fe809640 ExampleCore_2_1_2.TypeSample
4 -----------------------------
5 Total 1
6 CCW 0
7 RCW 0
8 ComClassFactory 0
9 Free 0
也可以使用【!syncblk 1】,只输出同步快表中索引值为1的信息,这里两个命令输出是一样的。
1 0:000> !syncblk 1
2 Index SyncBlock MonitorHeld Recursion Owning Thread Info SyncBlock Owner
3 1 00000213FA113E08 1 1 00000213FA0F6B50 3088 0 00000213fe809640 ExampleCore_2_1_2.TypeSample
4 -----------------------------
5 Total 1
6 CCW 0
7 RCW 0
8 ComClassFactory 0
9 Free 0
从上面的输出可以看到,索引为1的同步块指向的是一个已锁定的监视器,由线程【00000213FA0F6B50】持有,也可以说持有锁的线程【00000213FA0F6B50】锁住了【ExampleCore_2_1_2.TypeSample】类型的实例 。说明一下,我们在AddCoordinates()方法中调用了GetHashCode()方法,之所以要这样做,是为了强制创建一个同步块入口,当调用【lock】语句时,它将判断是否存在一个同步块与对象相关,如果存在,则把同步块作为同步数据。如果不存在同步块,CLR 将初始化一个廋锁(thin lock),而瘦锁保存的位置与同步块不同。
6.2、类型句柄
a)、基础知识
引用类型的实例都被存储在托管堆上,这些实例都包含一个【类型句柄】。【类型句柄】是CLR 类型系统中的粘合剂,它把对象实例和其相关的所有类型数据关联起来。对象的【类型句柄】存储在托管堆上,它是一个指针,指向类型的方法表。我们先看看托管堆中的对象和方法表的结构,效果如图:
当然,还有一部分图像没有显示出来,大家不必太在意,书上有原图。【类型句柄】指向的方法表包含了和类型相关的各种元数据,他们完整的描述了这个类型。在【类型句柄】指向的第一类数据中包含了关于类型本身的一些信息,我们列出一些,如图:
b)、眼见为实(了解类型句柄)
源码项目:ExampleCore_2_1_2
编译好我们的项目,打开【Windbg Preview】,依次点击【File】-->【Launch executable】,选择加载我们的项目文件:ExampleCore_2_1_2.exe,进入到调试器界面。我们使用【g】命令,继续运行调试器,开始执行我们的程序,我们的程序会在 Main() 方法的【Debugger.Break()】这行代码处暂停。
接下来,我们使用【!clrstack -a】命令查看一下托管代码的线程调用栈。
1 0:000> !clrstack -a
2 OS Thread Id: 0x24c0 (0)
3 Child SP IP Call Site
4 0000000E5577EA08 00007ffb3eee9202 [HelperMethodFrame: 0000000e5577ea08] System.Diagnostics.Debugger.BreakInternal()
5 0000000E5577EB10 00007ffa78ae60aa System.Diagnostics.Debugger.Break() [/_/src/coreclr/./System/Diagnostics/Debugger.cs @ 18]
6
7 0000000E5577EB40 00007ffa19f91998 ExampleCore_2_1_2.Program.Main(System.String[]) [E:\Visual Studio\.\ExampleCore_2_1_2\Program.cs @ 10]
8 PARAMETERS:
9 args (0x0000000E5577EB90) = 0x00000129b7808ea0
10 LOCALS:
11 0x0000000E5577EB78 = 0x00000129b7809640
红色标注的就是我们的局部变量,0x00000129b7809640 这个地址就是 TypeSample 类型的实例变量 sample。我们继续使用【dp 0x00000129b7809640】命令转储出数据结构。说明一下:如果是查找【同步块索引】,我们需要减去 0x4,如果想找到【类型句柄】,是不需要做多余的操作的,输出的结果值的第一个域值就是【类型句柄】的指针。
1 0:000> dp 0x00000129b7809640
2 00000129`b7809640 00007ffa\`1a049460 00000005`0000000a
3 00000129`b7809650 00000000`0000000a 00000000`00000000
4 00000129`b7809660 00007ffa`19f01188 00000000`0000006e
5 00000129`b7809670 00000000`00000000 00007ffa`19f01188
6 00000129`b7809680 00000000`00000037 00000000`00000000
7 00000129`b7809690 00007ffa`19f01188 00000000`0000006e
8 00000129`b78096a0 00000000`00000000 00007ffa`19ec5fa8
9 00000129`b78096b0 00000000`00000000 00000000`00000000
我加粗标红的值就是一个指针,它就是【类型句柄】,我们如果想查看【类型句柄】指向的方法表的具体内容,可以继续使用【dp】命令。
1 0:000> dp 00007ffa`1a049460
2 00007ffa`1a049460 00000020`00000000 00000004`00030080
3 00007ffa`1a049470 00007ffa`19ec5fa8 00007ffa`1a01e0a0
4 00007ffa`1a049480 00007ffa`1a0494a8 00007ffa`1a051f90
5 00007ffa`1a049490 00000000`00000000 00000000`00000000
6 00007ffa`1a0494a0 00007ffa`19ec5ff0 00000000`00000080
7 00007ffa`1a0494b0 00000000`00000000 00007ffa`1a049670
8 00007ffa`1a0494c0 90001560`31001ddb 00007ffa`1a03b960
9 00007ffa`1a0494d0 00007ffa`1a049670 00000000`00000000
里面的内容还是很多的,要想搞清楚每个项目的内容,还需要下点功夫。我先到此为止。
其实,我们可以使用【!dumpMT】这个命令查看方法表。
1 0:000> dp 0x00000129b7809640
2 00000129`b7809640 00007ffa\`1a049460 00000005`0000000a
3 00000129`b7809650 00000000`0000000a 00000000`00000000
4 00000129`b7809660 00007ffa`19f01188 00000000`0000006e
5 00000129`b7809670 00000000`00000000 00007ffa`19f01188
6 00000129`b7809680 00000000`00000037 00000000`00000000
7 00000129`b7809690 00007ffa`19f01188 00000000`0000006e
8 00000129`b78096a0 00000000`00000000 00007ffa`19ec5fa8
9 00000129`b78096b0 00000000`00000000 00000000`00000000
10
11
12
13 0:000> !dumpmt 00007ffa`1a049460
14 EEClass: 00007ffa1a051f90
15 Module: 00007ffa1a01e0a0
16 Name: ExampleCore_2_1_2.TypeSample
17 mdToken: 0000000002000003
18 File: E:\Visual Studio 2022\...\ExampleCore_2_1_2\bin\Debug\net8.0\ExampleCore_2_1_2.dll
19 AssemblyLoadContext: Default ALC - The managed instance of this context doesn't exist yet.
20 BaseSize: 0x20
21 ComponentSize: 0x0
22 DynamicStatics: false
23 ContainsPointers: false
24 Slots in VTable: 6
25 Number of IFaces in IFaceMap: 0
我们还有其他方法,可以先使用【!DumpObj】命令,然后再使用【!DumpMT】命令,也是可以的。
1 0:000> !DumpObj /d 00000129b7809640(这个是TypeSmaple 类型局部变量的地址) 2 Name: ExampleCore_2_1_2.TypeSample
3 MethodTable: 00007ffa1a049460(这个就是方法表的地址,也就是 DumpMT 命令的输入参数) 4 EEClass: 00007ffa1a051f90
5 Tracked Type: false
6 Size: 32(0x20) bytes
7 File: E:\Visual Studio 2022\Source\Projects\..\ExampleCore_2_1_2\bin\Debug\net8.0\ExampleCore_2_1_2.dll
8 Fields:
9 MT Field Offset Type VT Attr Value Name
10 00007ffa1a049408 4000001 8 ...ample+Coordinates 1 instance 00000129b7809648 coordinates
11
12
13 0:000> !DumpMT /d 00007ffa1a049460
14 EEClass: 00007ffa1a051f90
15 Module: 00007ffa1a01e0a0
16 Name: ExampleCore_2_1_2.TypeSample
17 mdToken: 0000000002000003
18 File: E:\Visual Studio 2022\Source\..\ExampleCore_2_1_2\bin\Debug\net8.0\ExampleCore_2_1_2.dll
19 AssemblyLoadContext: Default ALC - The managed instance of this context doesn't exist yet.
20 BaseSize: 0x20
21 ComponentSize: 0x0
22 DynamicStatics: false
23 ContainsPointers: false
24 Slots in VTable: 6
25 Number of IFaces in IFaceMap: 0
无论使用什么命令执行获取方法表信息,都必须是先使用【!clrstack -a|-l】找到局部变量的地址,然后才可以继续。
6.3、方法描述符
a)、基础知识
我们知道了方法表是描述类型的,那类型的方法是如何自描述的呢?答案是通过【方法描述符】来实现的,在【方法描述符】中包含了方法的详细信息,包括:方法的文本表示、它所在的模块、标记以及实现方法的代码的地址。
要想找到指定方法的描述,可以使用【!dumpMT】,同时使用 -md 开关。
b)、眼见为实(观察方法描述符)
源码项目:ExampleCore_2_1_2
编译好我们的项目,打开【Windbg Preview】调试器,依次点击【文件】---->【Launch executable】,加载我们的项目文件:ExampleCore_2_1_2.exe,进入调试器界面。我们使用【g】命令继续运行调试器,调试器会在源码中 Main()方法的【Debugger.Break()】这行代码处暂停。在开始之前,先查看一下调试器是否在主线程,如果不是,我们必须切换到主线程,执行命令【~0s】,我们就可以开始我们的调试了。
1 0:000> !clrstack -l
2 OS Thread Id: 0x24c0 (0)
3 Child SP IP Call Site
4 0000000E5577E7F0 00007ffb412eae54 [InlinedCallFrame: 0000000e5577e7f0]
5 0000000E5577E7F0 00007ffa784d76eb [InlinedCallFrame: 0000000e5577e7f0]
6 ......
7
8 0000000E5577EB40 00007ffa19f919ac ExampleCore_2_1_2.Program.Main(System.String[]) [E:\Visual Studio\.\ExampleCore_2_1_2\Program.cs @ 12]
9 LOCALS:
10 0x0000000E5577EB78 = 0x00000129b7809640
红色标注的就是我们 TypeSample 类型的局部变量 sample 的地址,我们使用【!DumpObj /d 0x00000129b7809640】命令查看对象的数据结构,从而找到该类型的【方法表】的地址。当然,使用【dp 0x00000129b7809640】命令也是可以的。
1 0:000> dp 0x00000129b7809640
2 00000129`b7809640 00007ffa\`1a049460 00000005`0000000a
3 00000129`b7809650 00000000`0000000a 00000000`00000000
4 00000129`b7809660 00007ffa`19f01188 00000000`0000006e
5 00000129`b7809670 00000000`00000000 00007ffa`19f01188
6 00000129`b7809680 00000000`00000037 00000000`00000000
7 00000129`b7809690 00007ffa`19f01188 00000000`0000006e
8 00000129`b78096a0 00000000`00000000 00007ffa`19ec5fa8
9 00000129`b78096b0 00000000`00000000 00000000`00000000
10
11
12 0:000> !DumpObj /d 0x00000129b7809640
13 Name: ExampleCore_2_1_2.TypeSample
14 MethodTable: 00007ffa1a04946015 EEClass: 00007ffa1a051f90
16 Tracked Type: false
17 Size: 32(0x20) bytes
18 File: E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_2_1_2\bin\Debug\net8.0\ExampleCore_2_1_2.dll
19 Fields:
20 MT Field Offset Type VT Attr Value Name
21 00007ffa1a049408 4000001 8 ...ample+Coordinates 1 instance 00000129b7809648 coordinates
两个命令执行的结果,红色标注的都是 TypeSample 类型的方法表,有了方法表的地址,我们就可以执行【!DumpMT -md 00007ffa1a049460】命令了。
1 0:000> !DumpMT -md 00007ffa1a049460
2 EEClass: 00007ffa1a051f90
3 Module: 00007ffa1a01e0a0
4 Name: ExampleCore_2_1_2.TypeSample
5 mdToken: 0000000002000003
6 File: E:\Visual Studio 2022\.\ExampleCore_2_1_2\bin\Debug\net8.0\ExampleCore_2_1_2.dll
7 AssemblyLoadContext: Default ALC - The managed instance of this context doesn't exist yet.
8 BaseSize: 0x20
9 ComponentSize: 0x0
10 DynamicStatics: false
11 ContainsPointers: false
12 Slots in VTable: 6
13 Number of IFaces in IFaceMap: 0
14 --------------------------------------
15 MethodDesc Table
16 Entry MethodDesc JIT Name
17 00007FFA19ED0048 00007ffa19ec5f38 NONE System.Object.Finalize()
18 00007FFA19ED0060 00007ffa19ec5f48 NONE System.Object.ToString()
19 00007FFA19ED0078 00007ffa19ec5f58 NONE System.Object.Equals(System.Object)
20 00007FFA1A0B9548 00007ffa19ec5f98 PreJIT System.Object.GetHashCode()
21 00007FFA1A03B930 00007ffa1a0493a8 JIT ExampleCore_2_1_2.TypeSample..ctor(Int32, Int32, Int32)
22 00007FFA1A03B948 00007ffa1a0493c0 JIT ExampleCore_2_1_2.TypeSample.AddCoordinates()
MethodDesc Table 红色标注的列表输出了 TypeSample 类型所有方法描述符。
1)、PreJIT:表示位于 Entry 地址处的代码已经被 JIT 预编译过了。
2)、JIT:表示这段代码已经编译过了。
3)、NONE:表示这段代码还没有被 JIT 编译过。
如果我们想获取更详细的信息,可以将【MethodDesc】列的地址,传递给【!DumpMD】命令。
1 0:000> !DumpMD 00007ffa1a0493a8
2 Method Name: ExampleCore_2_1_2.TypeSample..ctor(Int32, Int32, Int32)
3 Class: 00007ffa1a051f90
4 MethodTable: 00007ffa1a049460
5 mdToken: 0000000006000003
6 Module: 00007ffa1a01e0a0
7 IsJitted: yes
8 Current CodeAddr: 00007ffa19f919d0
9 Version History:
10 ILCodeVersion: 0000000000000000
11 ReJIT ID: 0
12 IL Addr: 00000129b4ce2085
13 CodeAddr: 00007ffa19f919d0 (MinOptJitted)
14 NativeCodeVersion: 0000000000000000
在这个输出中有两个要注意的项目,分别是:IsJitted 和 CodeAddr。如果【IsJitted】的值是 Yes,说明方法已经被JIT编译了。如果方法未编译,则是 no 值,【Current CodeAddr】的值也是:ffffffffffffffff,效果如图:
6.4、模块
a)、基础知识
我们知道程序集是 .Net 应用程序的逻辑容器,它可以包含一个或者多个模块,这个模块就是真正包含代码和资源的组件。这一节,我们就主要关注模块,内容不是很多,主要就是演示。
b)、眼见为实
调试源码:ExampleCore_2_1_2
编译好我们的项目,打开【Windbg Preview】,依次点击【文件】---->【Launch executable】,加载我们的项目文件:ExampleCore_2_1_2.exe,选择【打开】按钮,进入调试器界面。通过【g】命令运行调试器。接下来,我们查看一下模块的信息。
我们先查找一下托管程序的线程调用栈,找到我们需要的局部变量 sample。
1 0:000> !clrstack -l
2 OS Thread Id: 0xa5c (0)
3 Child SP IP Call Site
4 000000D3F3B7EB48 00007ffa87319202 [HelperMethodFrame: 000000d3f3b7eb48] System.Diagnostics.Debugger.BreakInternal()
5 000000D3F3B7EC50 00007ff9c18660aa System.Diagnostics.Debugger.Break() [/_/src/coreclr/./src/System/Diagnostics/Debugger.cs @ 18]
6
7 000000D3F3B7EC80 00007ff962951998 ExampleCore_2_1_2.Program.Main(System.String[]) [E:\Visual Studio\.\ExampleCore_2_1_2\Program.cs @ 10]
8 LOCALS:
9 0x000000D3F3B7ECB8 = 0x0000022d02409640
红色标注的就是 TypeSmaple 类型的局部变量 sample 的地址。我们查看【!dumpobj 0x0000022d02409640】,就会输出局部变量的数据信息。
1 0:000> !dumpobj /d 0x0000022d02409640
2 Name: ExampleCore_2_1_2.TypeSample
3 MethodTable: 00007ff962a09460 4 EEClass: 00007ff962a11f90
5 Tracked Type: false
6 Size: 32(0x20) bytes
7 File: E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_2_1_2\bin\Debug\net8.0\ExampleCore_2_1_2.dll
8 Fields:
9 MT Field Offset Type VT Attr Value Name
10 00007ff962a09408 4000001 8 ...ample+Coordinates 1 instance 0000022d02409648 coordinates
红色标注的就是 TypeSample 类型的方法表的地址,我们可以继续使用【!dumpmt /d 00007ff962a09460】命令输出详情。
1 0:000> !dumpmt 00007ff962a09460
2 EEClass: 00007ff962a11f90
3 Module: 00007ff9629de0a0 4 Name: ExampleCore_2_1_2.TypeSample
5 mdToken: 0000000002000003
6 File: E:\Visual Studio 2022\.\ExampleCore_2_1_2\bin\Debug\net8.0\ExampleCore_2_1_2.dll
7 AssemblyLoadContext: Default ALC - The managed instance of this context doesn't exist yet.
8 BaseSize: 0x20
9 ComponentSize: 0x0
10 DynamicStatics: false
11 ContainsPointers: false
12 Slots in VTable: 6
13 Number of IFaces in IFaceMap: 0
红色标注的就是方法表这个数据结构所属与的模块地址。我们可以使用【!dumpmodule /d 00007ff9629de0a0】命令查看有关模块的相信信息了。
1 0:000> !dumpmodule /d 00007ff9629de0a0
2 Name: E:\Visual Studio 2022\Source\...\ExampleCore_2_1_2\bin\Debug\net8.0\ExampleCore_2_1_2.dll
3 Attributes: PEFile
4 TransientFlags: 00209011
5 Assembly: 0000022cff24eba0
6 BaseAddress: 0000022CFF6E0000
7 PEAssembly: 0000022CFF24EA60
8 ModuleId: 00007FF9629DE458
9 ModuleIndex: 0000000000000001
10 LoaderHeap: 00007FF9C2809508
11 TypeDefToMethodTableMap: 00007FF9629E4320
12 TypeRefToMethodTableMap: 00007FF9629E4348
13 MethodDefToDescMap: 00007FF9629E4488
14 FieldDefToDescMap: 00007FF9629E44B0
15 MemberRefToDescMap: 00007FF9629E43E8
16 FileReferencesMap: 0000000000000000
17 AssemblyReferencesMap: 00007FF9629E44E0
18 MetaData start address: 0000022CFF6E2168 (1788 bytes)
除了名称、属性、所属的程序集地址和加载器堆以外,还有一组映射(Map),这些映射只是将这些标记映射到底层的 CLR 数据结构。其实从命名上也可以看出端倪,不如:TypeDefToMethodTableMap:表示定义的类型与方法表中的映射,MethodDefToDescMap:定义的方法和描述符之间的映射。举个例子:如果要将一个方法定义标记映射到一个方法描述符,我们可以输出【MethodDefToDescMap】域的信息。我们使用【dp 00007FF9629E4488】查看。
1 0:000> dp 00007FF9629E4488
2 00007ff9`629e4488 00000000`0000000000007ff9\`62a000c03 00007ff9`629e4498 00007ff9\`62a000d8 00007ff9\`62a093a84 00007ff9`629e44a8 00007ff9`62a093c0 00000000`00000000
5 00007ff9`629e44b8 00007ff9`62a09378 00007ff9`62a093d8
6 00007ff9`629e44c8 00007ff9`62a093e8 00007ff9`62a093f8
7 00007ff9`629e44d8 00000000`00000000 00000000`00000000
8 00007ff9`629e44e8 00007ff9`629dfbc8 00007ff9`62a09760
9 00007ff9`629e44f8 00000000`00000000 00007ff9`629de0a0
00007ff9`62a000c0 就是 Main() 方法的方法描述符的地址。00007ff9`62a000d8 就是 Program..ctor() 方法的描述符地址,00007ff9`62a093a8 就是 TypeSample..ctor(Int32, Int32, Int32) 方法的描述符地址。00007ff9`62a093c0 就是TypeSample.AddCoordinates()方发的描述符地址。
1 0:000> !dumpmd /d 00007ff9`62a000c0
2 Method Name: ExampleCore_2_1_2.Program.Main(System.String[])
3 Class: 00007ff9629efbc0
4 MethodTable: 00007ff962a000e8
5 mdToken: 0000000006000001
6 Module: 00007ff9629de0a0
7 IsJitted: yes
8 Current CodeAddr: 00007ff962951930
9 Version History:
10 ILCodeVersion: 0000000000000000
11 ReJIT ID: 0
12 IL Addr: 0000022cff6e2050
13 CodeAddr: 00007ff962951930 (MinOptJitted)
14 NativeCodeVersion: 0000000000000000
15
16 0:000> !dumpmd /d 00007ff9`62a000d8
17 Method Name: ExampleCore_2_1_2.Program..ctor()
18 Class: 00007ff9629efbc0
19 MethodTable: 00007ff962a000e8
20 mdToken: 0000000006000002
21 Module: 00007ff9629de0a0
22 IsJitted: no
23 Current CodeAddr: ffffffffffffffff
24 Version History:
25 ILCodeVersion: 0000000000000000
26 ReJIT ID: 0
27 IL Addr: 0000022cff6e207c
28 CodeAddr: 0000000000000000 (MinOptJitted)
29 NativeCodeVersion: 0000000000000000
30
31 0:000> !dumpmd /d 00007ff9`62a093a8
32 Method Name: ExampleCore_2_1_2.TypeSample..ctor(Int32, Int32, Int32)
33 Class: 00007ff962a11f90
34 MethodTable: 00007ff962a09460
35 mdToken: 0000000006000003
36 Module: 00007ff9629de0a0
37 IsJitted: yes
38 Current CodeAddr: 00007ff9629519d0
39 Version History:
40 ILCodeVersion: 0000000000000000
41 ReJIT ID: 0
42 IL Addr: 0000022cff6e2085
43 CodeAddr: 00007ff9629519d0 (MinOptJitted)
44 NativeCodeVersion: 0000000000000000
45
46 0:000> !dumpmd /d 00007ff9`62a093c0
47 Method Name: ExampleCore_2_1_2.TypeSample.AddCoordinates()
48 Class: 00007ff962a11f90
49 MethodTable: 00007ff962a09460
50 mdToken: 0000000006000004
51 Module: 00007ff9629de0a0
52 IsJitted: no
53 Current CodeAddr: ffffffffffffffff
54 Version History:
55 ILCodeVersion: 0000000000000000
56 ReJIT ID: 0
57 IL Addr: 0000022cff6e20b4
58 CodeAddr: 0000000000000000 (MinOptJitted)
59 NativeCodeVersion: 0000000000000000
【dumpmodule】命令不仅可以输出模块的特定信息,还可以输出在模块中定义和使用的所有类型。只要加上 -mt 命令开关。执行命令【!dumpmodule -mt 00007ff9629de0a0】查看模块中所有和使用的类型。
1 0:000> !dumpmodule -mt 00007ff9629de0a0
2 Name: E:\Visual Studio 2022\.\ExampleCore_2_1_2\bin\Debug\net8.0\ExampleCore_2_1_2.dll
3 Attributes: PEFile
4 TransientFlags: 00209011
5 Assembly: 0000022cff24eba0
6 BaseAddress: 0000022CFF6E0000
7 PEAssembly: 0000022CFF24EA60
8 ModuleId: 00007FF9629DE458
9 ModuleIndex: 0000000000000001
10 LoaderHeap: 00007FF9C2809508
11 TypeDefToMethodTableMap: 00007FF9629E4320
12 TypeRefToMethodTableMap: 00007FF9629E4348
13 MethodDefToDescMap: 00007FF9629E4488
14 FieldDefToDescMap: 00007FF9629E44B0
15 MemberRefToDescMap: 00007FF9629E43E8
16 FileReferencesMap: 0000000000000000
17 AssemblyReferencesMap: 00007FF9629E44E0
18 MetaData start address: 0000022CFF6E2168 (1788 bytes)
19
20 Types defined in this module
21
22 MT TypeDef Name
23 ------------------------------------------------------------------------------
24 00007ff962a000e8 0x02000002 ExampleCore_2_1_2.Program
25 00007ff962a09460 0x02000003 ExampleCore_2_1_2.TypeSample
26 00007ff962a09408 0x02000004 ExampleCore_2_1_2.TypeSample+Coordinates
27
28 Types referenced in this module
29
30 MT TypeRef Name
31 ------------------------------------------------------------------------------
32 00007ff962885fa8 0x0200000d System.Object
33 00007ff9628860f0 0x0200000f System.ValueType
34 00007ff962a09670 0x02000010 System.Diagnostics.Debugger
35 00007ff962a0aa78 0x02000011 System.Console
6.5、元数据标记
a)、基础知识
我们到现在为止,已经看到了很多运行时的数据结构,比如:程序集,模块,方法表和方法描述符等。所有这些数据结构都是为了支持类型系统和自描述。这些数据结构就是元数据,它们以表格的形式存储在运行时的引擎中。元数据表有很多,这是必须要知道的,简单类说,元数据标记就是一个 4 字节的值。高位的 1 个字节表示该标记所引用的表。元数据表如图:
例如:06000001的元数据标记表示指向方法定义表的(高位字节为0x06)中的第1个索引。其实,元数据表,我们已经看过了,下面我们在看一次。
b)、眼见为实
调试源码:ExampleCore_2_1_2
编译好我们的项目,打开【Windbg Preview】,依次点击【文件】---->【Launch executable】,加载我们的项目文件:ExampleCore_2_1_2.exe,选择【打开】按钮,进入调试器界面。通过【g】命令运行调试器。接下来,我们查看一下模块的信息。
我们先来看看我们托管程序的线程调用栈,执行命令【!clrstack -l】。
1 0:000> !clrstack -l
2 OS Thread Id: 0xa5c (0)
3 Child SP IP Call Site
4 000000D3F3B7EB48 00007ffa87319202 [HelperMethodFrame: 000000d3f3b7eb48] System.Diagnostics.Debugger.BreakInternal()
5 000000D3F3B7EC50 00007ff9c18660aa System.Diagnostics.Debugger.Break() [/_/src/coreclr/./Debugger.cs @ 18]
6
7 000000D3F3B7EC80 00007ff962951998 ExampleCore_2_1_2.Program.Main(System.String[]) [E:\Visual Studio\.\ExampleCore_2_1_2\Program.cs @ 10]
8 LOCALS:
9 0x000000D3F3B7ECB8 = 0x0000022d02409640
找到我们本地的局部变量 sample,然后输出它的内容,执行【!DumpObj /d 0000022d02409640】命令。
1 0:000> !DumpObj /d 0000022d02409640
2 Name: ExampleCore_2_1_2.TypeSample
3 MethodTable: 00007ff962a09460 4 EEClass: 00007ff962a11f90
5 Tracked Type: false
6 Size: 32(0x20) bytes
7 File: E:\Visual Studio 2022\.\bin\Debug\net8.0\ExampleCore_2_1_2.dll
8 Fields:
9 MT Field Offset Type VT Attr Value Name
10 00007ff962a09408 4000001 8 ...ample+Coordinates 1 instance 0000022d02409648 coordinates
我们知道了方法表,知道模块也就很容易了,执行命令【!DumpMT /d 00007ff962a09460】。
1 0:000> !DumpMT /d 00007ff962a09460
2 EEClass: 00007ff962a11f90
3 Module: 00007ff9629de0a0 4 Name: ExampleCore_2_1_2.TypeSample
5 mdToken: 0000000002000003
6 File: E:\Visual Studio 2022\.\ExampleCore_2_1_2\bin\Debug\net8.0\ExampleCore_2_1_2.dll
7 AssemblyLoadContext: Default ALC - The managed instance of this context doesn't exist yet.
8 BaseSize: 0x20
9 ComponentSize: 0x0
10 DynamicStatics: false
11 ContainsPointers: false
12 Slots in VTable: 6
13 Number of IFaces in IFaceMap: 0
我们有了模块地址,就可以使用【!DumpModule /d 00007ff9629de0a0】命令,查看模块详情。
1 0:000> !DumpModule /d 00007ff9629de0a0
2 Name: E:\Visual Studio 2022\.\ExampleCore_2_1_2\bin\Debug\net8.0\ExampleCore_2_1_2.dll
3 Attributes: PEFile
4 TransientFlags: 00209011
5 Assembly: 0000022cff24eba0
6 BaseAddress: 0000022CFF6E0000
7 PEAssembly: 0000022CFF24EA60
8 ModuleId: 00007FF9629DE458
9 ModuleIndex: 0000000000000001
10 LoaderHeap: 00007FF9C2809508
11 TypeDefToMethodTableMap: 00007FF9629E432012 TypeRefToMethodTableMap: 00007FF9629E434813 MethodDefToDescMap: 00007FF9629E448814 FieldDefToDescMap: 00007FF9629E44B015 MemberRefToDescMap: 00007FF9629E43E816 FileReferencesMap: 0000000000000000
17 AssemblyReferencesMap: 00007FF9629E44E018 MetaData start address: 0000022CFF6E2168 (1788 bytes)
红色标注的就是【dumpmodule】命令输出中包含的一组常见的表映射。我们看看【TypeDefToMethodTableMap】这个域的值,它的地址:00007FF9629E4320,它将类型定义映射到相应的方法表。我们可以使用【dp】命令看一下具体数据。
1 0:000> dp 00007FF9629E4320
2 00007ff9`629e4320 00000000`00000000 00000000`00000000
3 00007ff9`629e4330 00007ff9\`62a000e8 00007ff9\`62a094604 00007ff9`629e4340 00007ff9\`62a09408 00000000`00000000
5 00007ff9`629e4350 00000000`00000000 00000000`00000000
6 00007ff9`629e4360 00000000`00000000 00000000`00000000
7 00007ff9`629e4370 00000000`00000000 00000000`00000000
8 00007ff9`629e4380 00000000`00000000 00000000`00000000
9 00007ff9`629e4390 00000000`00000000 00000000`00000000
红色标注的就是我们定义的类型,依次是:Program、TypeSample 和 TypeSample+Coordinates,数据显示是如下。
1 0:000> !dumpmt 00007ff9`62a000e8
2 EEClass: 00007ff9629efbc0
3 Module: 00007ff9629de0a0
4 Name: ExampleCore_2_1_2.Program
5 mdToken: 0000000002000002
6 File: E:\Visual Studio 2022\Source\.\ExampleCore_2_1_2\bin\Debug\net8.0\ExampleCore_2_1_2.dll
7 AssemblyLoadContext: Default ALC - The managed instance of this context doesn't exist yet.
8 BaseSize: 0x18
9 ComponentSize: 0x0
10 DynamicStatics: false
11 ContainsPointers: false
12 Slots in VTable: 6
13 Number of IFaces in IFaceMap: 0
14 0:000> !dumpmt 00007ff9`62a09460
15 EEClass: 00007ff962a11f90
16 Module: 00007ff9629de0a0
17 Name: ExampleCore_2_1_2.TypeSample
18 mdToken: 0000000002000003
19 File: E:\Visual Studio 2022\Source\.ExampleCore_2_1_2\bin\Debug\net8.0\ExampleCore_2_1_2.dll
20 AssemblyLoadContext: Default ALC - The managed instance of this context doesn't exist yet.
21 BaseSize: 0x20
22 ComponentSize: 0x0
23 DynamicStatics: false
24 ContainsPointers: false
25 Slots in VTable: 6
26 Number of IFaces in IFaceMap: 0
27 0:000> !dumpmt 00007ff9`62a09408
28 EEClass: 00007ff962a12008
29 Module: 00007ff9629de0a0
30 Name: ExampleCore_2_1_2.TypeSample+Coordinates
31 mdToken: 0000000002000004
32 File: E:\Visual Studio 2022\Source\.\ExampleCore_2_1_2\bin\Debug\net8.0\ExampleCore_2_1_2.dll
33 AssemblyLoadContext: Default ALC - The managed instance of this context doesn't exist yet.
34 BaseSize: 0x20
35 ComponentSize: 0x0
36 DynamicStatics: false
37 ContainsPointers: false
38 Slots in VTable: 4
39 Number of IFaces in IFaceMap: 0
我们就拿第一个【00007ff9`62a000e8】进行说明,它定义的类型是:ExampleCore_2_1_2.Program,也就是 name 属性的值:ExampleCore_2_1_2.Program,mdToken的值:0000000002000002,02000002为了两个部分,高位表示(0200)是一个类型,低位部分(0002)表示索引值为2。
6.6、EEClass
a)、基础知识
EEClass 和 MethodTable 是同级别的,用来描述 C# 的一个类,可以使用 !dumpclass 来显示类型的 EECLass 信息。从本质上来看,EEClass 和 MethodTable 他们又是两种截然不同的结构,不过从逻辑角度看,它们表示相同的概念。之所以有这种区分,主要是根据 CLR 针对类型域使用的频繁程度来决定的,频繁被使用的域存在方法表(Method Table)里,不太被频繁使用的域保存到 EEClass 数据结构中。
b)、眼见为实
调试源码:ExampleCore_2_1_2
编译好我们的项目,打开【Windbg Preview】,依次点击【文件】---->【Launch executable】,加载我们的项目文件:ExampleCore_2_1_2.exe,选择【打开】按钮,进入调试器界面。通过【g】命令运行调试器。接下来,我们查看一下模块的信息。
我们先来看看我们托管程序的线程调用栈,执行命令【!clrstack -l】。
1 0:000> !clrstack -l
2 OS Thread Id: 0xa5c (0)
3 Child SP IP Call Site
4 000000D3F3B7EB48 00007ffa87319202 [HelperMethodFrame: 000000d3f3b7eb48] System.Diagnostics.Debugger.BreakInternal()
5 000000D3F3B7EC50 00007ff9c18660aa System.Diagnostics.Debugger.Break() [/_/src/coreclr/./Debugger.cs @ 18]
6
7 000000D3F3B7EC80 00007ff962951998 ExampleCore_2_1_2.Program.Main(System.String[]) [E:\Visual Studio\.\ExampleCore_2_1_2\Program.cs @ 10]
8 LOCALS:
9 0x000000D3F3B7ECB8 = 0x0000022d02409640
0x0000022d02409640 这个就是我们的局部变量,可以使用【!dumpobj /d 0000022d02409640】显示类型的信息。
1 0:000> !DumpObj /d 0000022d02409640
2 Name: ExampleCore_2_1_2.TypeSample
3 MethodTable: 00007ff962a09460
4 EEClass: 00007ff962a11f90 5 Tracked Type: false
6 Size: 32(0x20) bytes
7 File: E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_2_1_2\bin\Debug\net8.0\ExampleCore_2_1_2.dll
8 Fields:
9 MT Field Offset Type VT Attr Value Name
10 00007ff962a09408 4000001 8 ...ample+Coordinates 1 instance 0000022d02409648 coordinates
以上红色标注的就是 EEClass 的地址,我们使用【!dumpclass /d 00007ff962a11f90】命令显示其数据。
1 0:000> !dumpclass /d 00007ff962a11f90
2 Class Name: ExampleCore_2_1_2.TypeSample
3 mdToken: 0000000002000003 (这是类型定义,索引为值:3)
4 File: E:\Visual Studio 2022\Source\P.\bin\Debug\net8.0\ExampleCore_2_1_2.dll
5 Parent Class: 00007ff96287f5b0 (这是父类的地址)
6 Module: 00007ff9629de0a0 (这是模块的地址)
7 Method Table: 00007ff962a09460 (这是方法表的地址)
8 Vtable Slots: 4 (TypeSample)
9 Total Method Slots: 4 (TypeSample 类型方法总的数量 4个)
10 Class Attributes: 100001
11 NumInstanceFields: 1(TypeSample类型实例字段有一个)
12 NumStaticFields: 0 (TypeSample 类型静态字段没有)
13 MT Field Offset Type VT Attr Value Name
14 00007ff962a09408 4000001 8 ...ample+Coordinates 1 instance coordinates
四、总结
站在高人的肩膀之上,自己轻松了很多,但是,自己还是一个小学生,Net 高级调试这条路,也刚刚起步,还有很多要学的地方。皇天不负有心人,努力,不辜负自己,我相信付出就有回报,再者说,学习的过程,有时候,虽然很痛苦,但是,学有所成,学有所懂,这个开心的感觉还是不可言喻的。不忘初心,继续努力。做自己喜欢做的,开心就好。