2024年2月

一、简介
这是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 usingSystem.Diagnostics;2 
3 namespaceExampleCore_2_1_14 {5     internal classProgram6 {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 usingSystem.Diagnostics;2 
3 namespaceExampleCore_2_1_24 {5     internal classProgram6 {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 classTypeSample20 {21         public TypeSample(int x, int y, intz)22 {23             coordinates.x =x;24             coordinates.y =y;25             coordinates.z =z;26 }27 
28         /// <summary>
29         ///值类型30         /// </summary>
31         private structCoordinates32 {33             public intx;34             public inty;35             public intz;36 }37 
38         privateCoordinates coordinates;39 
40         public voidAddCoordinates()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>lm2 start             end                 module name3 00007ff6`895a0000 00007ff6`895d8000   notepad    (pdb symbols)          C:\ProgramData\Dbg\sym\notepad.pdb\FF9C9991EA5CB351AF10D24FCBA2CE391\notepad.pdb4 00007ffe`f9b90000 00007ffe`f9c6c000   efswrt     (deferred)5 00007fff`19aa0000 00007fff`19b06000   oleacc     (deferred)6 00007fff`1b030000 00007fff`1b2ca000   COMCTL32   (deferred)7 00007fff`2440000000007fff`244fc000   textinputframework   (deferred)8 00007fff`24650000 00007fff`24744000MrmCoreR   (deferred)9 00007fff`263c0000 00007fff`2646e000   TextShaping   (deferred)10 00007fff`278b0000 00007fff`27ab2000   twinapi_appcore   (deferred)11 00007fff`2924000000007fff`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`3123000000007fff`3133a000   gdi32full   (deferred)23 00007fff`3150000000007fff`3157f000   bcryptPrimitives   (deferred)24 00007fff`31650000 00007fff`31750000ucrtbase   (deferred)25 00007fff`31750000 00007fff`31763000kernel_appcore   (deferred)26 00007fff`3178000000007fff`3181e000   msvcrt     (deferred)27 00007fff`31880000 00007fff`31928000clbcatq    (deferred)28 00007fff`3193000000007fff`31c84000   combase    (deferred)29 00007fff`31c90000 00007fff`31d2b000   sechost    (deferred)30 00007fff`31ef0000 00007fff`31f5b000   WS2_32     (deferred)31 00007fff`31f60000 00007fff`32100000USER32     (deferred)32 00007fff`32230000 00007fff`32305000OLEAUT32   (deferred)33 00007fff`3288000000007fff`3292a000   ADVAPI32   (deferred)34 00007fff`3293000000007fff`329ed000   KERNEL32   (pdb symbols)          C:\ProgramData\Dbg\sym\kernel32.pdb\85A257DB4B7B82F2E19AD96AB7BB116A1\kernel32.pdb35 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`33871000SHELL32    (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+00023BE02 notepad!wWinMainCRTStartup:3 00007ff6`895c3be0 4883ec28        sub     rsp,28h4 00007ff6`895c3be4 e86b090000      call    notepad!_security_init_cookie (00007ff6`895c4554)5 00007ff6`895c3be9 4883c428        add     rsp,28h6 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 654D  ........_CorExeM4           00402780: 61 69 6E 00 6D 73 63 6F 72 65 65 2E 64 6C 6C 00ain.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.dll4                         402000Import Address Table5 40276A Import Name Table6                              0time date stamp7                              0Index of first forwarder reference8 
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>.chain2 Extension DLL search Path:3     C:\Program Files\WindowsApps\Microsoft.WinDbg_1.2306.14001.0_x64__8wekyb3d8bbwe\amd64\WINXP;..\.dotnet\tools4 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> !dumpdomain2 --------------------------------------
3 System Domain:      00007ffa16d190404 LowFrequencyHeap:   00007FFA16D195185 HighFrequencyHeap:  00007FFA16D195A86 StubHeap:           00007FFA16D196387 Stage:              OPEN8 Name:               None9 --------------------------------------
10 Domain 1:           00000252bb90865011 LowFrequencyHeap:   00007FFA16D1951812 HighFrequencyHeap:  00007FFA16D195A813 StubHeap:           00007FFA16D1963814 Stage:              OPEN15 Name:               clrhost16 Assembly:           00000252bb8ce240 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.0\System.Private.CoreLib.dll]17 ClassLoader:        00000252BB8CE2D018 Module19   00007ff9b6d24000    C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.0\System.Private.CoreLib.dll20 
21 ..................(省略无用的信息)

System Domain
就是系统级应用程序域,Domain 1就是我们默认的应用程序域。如果大家想查看 .Net Framework 版本的应用程序,就能看到3个,自己可以试试,我就不写了。

输出的内容,我们必须了解一下。
(1)、指向应用程序域的指针。这个参数可以作为【!dumpdomain】命令的输入参数,这样只输出指定应用程序域的信息。
我们获取【系统应用程序域】的信息。

1 0:000> !dumpdomain 00007ffa16d190402 --------------------------------------
3 System Domain:      00007ffa16d190404 LowFrequencyHeap:   00007FFA16D195185 HighFrequencyHeap:  00007FFA16D195A86 StubHeap:           00007FFA16D196387 Stage:              OPEN8 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 00000252bb9086502 --------------------------------------
3 Domain 1:           00000252bb9086504 LowFrequencyHeap:   00007FFA16D195185 HighFrequencyHeap:  00007FFA16D195A86 StubHeap:           00007FFA16D196387 Stage:              OPEN8 Name:               clrhost9 Assembly:           00000252bb8ce240 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.0\System.Private.CoreLib.dll]10 ClassLoader:        00000252BB8CE2D011 Module12   00007ff9b6d24000    C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.0\System.Private.CoreLib.dll

红色标注的就是程序集的数据,我们就可以使用【!dumpAssembly】命令,查看程序集的详情。

1 0:000> !dumpassembly 00000252bb8ce2402 Parent Domain:      00000252bb9086503 Name:               C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.0\System.Private.CoreLib.dll4 ClassLoader:        00000252BB8CE2D05 Module6   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
下载地址:
https://www.mzrst.com/
调试项目: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> ~0s2 ntdll!NtReadFile+0x14:3 00007ffc`8c7aae54 c3              ret

我们使用【!clrstack -a】命令,查看当前托管程序的调用栈所有的参数和局部变量,-a 指包括参数和局部变量,-l 指只有局部变量。

1 0:000> !clrstack -a2 OS Thread Id: 0x3e2c (0)3 Child SP               IP Call Site4 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.TypeSample3 MethodTable: 00007ffb47809460(这里是方法表)4 EEClass:     00007ffb47811f905 Tracked Type: false
6 Size:        32(0x20) bytes7 File:        E:\Visual Studio 2022\Source\...\ExampleCore_2_1_2\bin\Debug\net8.0\ExampleCore_2_1_2.dll8 Fields:9 MT    Field   Offset                 Type VT     Attr            Value Name10 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 0000028dd4c096482 Name:        ExampleCore_2_1_2.TypeSample+Coordinates3 MethodTable: 00007ffb4eae94084 EEClass:     00007ffb4eaf20085 Size:        32(0x20) bytes6 File:        E:\Visual Studio 2022\Source\...\ExampleCore_2_1_2\bin\Debug\net8.0\ExampleCore_2_1_2.dll7 Fields:8 MT    Field   Offset                 Type VT     Attr            Value Name9 00007ffb4e9a1188  4000002        0         System.Int32  1 instance               10x10 00007ffb4e9a1188  4000003        4         System.Int32  1 instance                5y11 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 -a2 OS Thread Id: 0x3088 (0)3 Child SP               IP Call Site4 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  000000004e2f9460 00007ffb 0000000a3 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 -a2 OS Thread Id: 0x3088 (0)3 Child SP               IP Call Site4 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  080000014e2f9460 00007ffb 0000000a3 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

这次我们看到了结果,红色加粗标注的 0x
08000001,08表示这个同步块索引里包含了哈希值,因为我们调用了对象的 GetHashCode() 方法。红色尾部部分包含 1,说明对象包含一个同步块索引。
如果想看同步块索引表中
08000001
处内容,可以使用【!syncblk】命令,输出所有同步快表中所有的元素。

1 0:000> !syncblk2 Index         SyncBlock MonitorHeld Recursion Owning Thread Info          SyncBlock Owner3     1 00000213FA113E08            1         1 00000213FA0F6B50 3088   000000213fe809640 ExampleCore_2_1_2.TypeSample4 -----------------------------
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 Owner3     1 00000213FA113E08            1         1 00000213FA0F6B50 3088   000000213fe809640 ExampleCore_2_1_2.TypeSample4 -----------------------------
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 -a2 OS Thread Id: 0x24c0 (0)3 Child SP               IP Call Site4 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

红色标注的就是我们的局部变量,
0x
00000129b7809640

这个地址就是 TypeSample 类型的实例变量 sample。我们继续使用【dp
0x
00000129b7809640

】命令转储出数据结构。
说明一下:如果是查找【同步块索引】,我们需要减去 0x4,如果想找到【类型句柄】,是不需要做多余的操作的,输出的结果值的第一个域值就是【类型句柄】的指针。

1 0:000> dp 0x00000129b7809640
2 00000129`b7809640  00007ffa`1a049460 00000005`0000000a3 00000129`b7809650  00000000`0000000a 00000000`00000000
4 00000129`b7809660  00007ffa`19f01188 00000000`0000006e5 00000129`b7809670  00000000`0000000000007ffa`19f011886 00000129`b7809680  00000000`00000037 00000000`00000000
7 00000129`b7809690  00007ffa`19f01188 00000000`0000006e8 00000129`b78096a0  00000000`0000000000007ffa`19ec5fa89 00000129`b78096b0  00000000`00000000 00000000`00000000

我加粗标红的值就是一个指针,它就是【类型句柄】,我们如果想查看【类型句柄】指向的方法表的具体内容,可以继续使用【dp】命令。

1 0:000>dp 00007ffa`1a0494602 00007ffa`1a049460  00000020`00000000 00000004`00030080
3 00007ffa`1a049470  00007ffa`19ec5fa8 00007ffa`1a01e0a04 00007ffa`1a049480  00007ffa`1a0494a8 00007ffa`1a051f905 00007ffa`1a049490  00000000`00000000 00000000`00000000
6 00007ffa`1a0494a0  00007ffa`19ec5ff0 00000000`00000080
7 00007ffa`1a0494b0  00000000`0000000000007ffa`1a0496708 00007ffa`1a0494c0  90001560`31001ddb 00007ffa`1a03b9609 00007ffa`1a0494d0  00007ffa`1a049670 00000000`00000000

里面的内容还是很多的,要想搞清楚每个项目的内容,还需要下点功夫。我先到此为止。
其实,我们可以使用【!dumpMT】这个命令查看方法表。

1 0:000> dp 0x00000129b7809640
2 00000129`b7809640  00007ffa`1a049460 00000005`0000000a3 00000129`b7809650  00000000`0000000a 00000000`00000000
4 00000129`b7809660  00007ffa`19f01188 00000000`0000006e5 00000129`b7809670  00000000`0000000000007ffa`19f011886 00000129`b7809680  00000000`00000037 00000000`00000000
7 00000129`b7809690  00007ffa`19f01188 00000000`0000006e8 00000129`b78096a0  00000000`0000000000007ffa`19ec5fa89 00000129`b78096b0  00000000`00000000 00000000`00000000
10 
11 
12 
13 0:000> !dumpmt 00007ffa`1a04946014 EEClass:             00007ffa1a051f9015 Module:              00007ffa1a01e0a016 Name:                ExampleCore_2_1_2.TypeSample17 mdToken:             0000000002000003
18 File:                E:\Visual Studio 2022\...\ExampleCore_2_1_2\bin\Debug\net8.0\ExampleCore_2_1_2.dll19 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.TypeSample3 MethodTable: 00007ffa1a049460(这个就是方法表的地址,也就是 DumpMT 命令的输入参数)4 EEClass:     00007ffa1a051f905 Tracked Type: false
6 Size:        32(0x20) bytes7 File:        E:\Visual Studio 2022\Source\Projects\..\ExampleCore_2_1_2\bin\Debug\net8.0\ExampleCore_2_1_2.dll8 Fields:9 MT    Field   Offset                 Type VT     Attr            Value Name10 00007ffa1a049408  4000001        8 ...ample+Coordinates  1instance 00000129b7809648 coordinates11 
12 
13 0:000> !DumpMT /d 00007ffa1a04946014 EEClass:             00007ffa1a051f9015 Module:              00007ffa1a01e0a016 Name:                ExampleCore_2_1_2.TypeSample17 mdToken:             0000000002000003
18 File:                E:\Visual Studio 2022\Source\..\ExampleCore_2_1_2\bin\Debug\net8.0\ExampleCore_2_1_2.dll19 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 -l2 OS Thread Id: 0x24c0 (0)3 Child SP               IP Call Site4 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
0x
00000129b7809640

】命令查看对象的数据结构,从而找到该类型的【方法表】的地址。当然,使用【dp
0x
00000129b7809640

】命令也是可以的。

1 0:000> dp 0x00000129b7809640
2 00000129`b7809640  00007ffa`1a049460 00000005`0000000a3 00000129`b7809650  00000000`0000000a 00000000`00000000
4 00000129`b7809660  00007ffa`19f01188 00000000`0000006e5 00000129`b7809670  00000000`0000000000007ffa`19f011886 00000129`b7809680  00000000`00000037 00000000`00000000
7 00000129`b7809690  00007ffa`19f01188 00000000`0000006e8 00000129`b78096a0  00000000`0000000000007ffa`19ec5fa89 00000129`b78096b0  00000000`00000000 00000000`00000000
10 
11 
12 0:000> !DumpObj /d 0x00000129b7809640
13 Name:        ExampleCore_2_1_2.TypeSample14 MethodTable: 00007ffa1a04946015 EEClass:     00007ffa1a051f9016 Tracked Type: false
17 Size:        32(0x20) bytes18 File:        E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_2_1_2\bin\Debug\net8.0\ExampleCore_2_1_2.dll19 Fields:20 MT    Field   Offset                 Type VT     Attr            Value Name21 00007ffa1a049408  4000001        8 ...ample+Coordinates  1 instance 00000129b7809648 coordinates

两个命令执行的结果,红色标注的都是 TypeSample 类型的方法表,有了方法表的地址,我们就可以执行【!DumpMT -md
00007ffa1a049460
】命令了。

1 0:000> !DumpMT -md 00007ffa1a0494602 EEClass:             00007ffa1a051f903 Module:              00007ffa1a01e0a04 Name:                ExampleCore_2_1_2.TypeSample5 mdToken:             0000000002000003
6 File:                E:\Visual Studio 2022\.\ExampleCore_2_1_2\bin\Debug\net8.0\ExampleCore_2_1_2.dll7 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 Table16 Entry       MethodDesc    JIT Name17 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 00007ffa1a0493a82 Method Name:          ExampleCore_2_1_2.TypeSample..ctor(Int32, Int32, Int32)3 Class:                00007ffa1a051f904 MethodTable:          00007ffa1a0494605 mdToken:              0000000006000003
6 Module:               00007ffa1a01e0a07 IsJitted:             yes8 Current CodeAddr:     00007ffa19f919d09 Version History:10   ILCodeVersion:      0000000000000000
11   ReJIT ID:           0
12 IL Addr:            00000129b4ce208513 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 -l2 OS Thread Id: 0xa5c (0)3 Child SP               IP Call Site4 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
0x
0000022d02409640

】,就会输出局部变量的数据信息。

1 0:000> !dumpobj /d 0x0000022d02409640
2 Name:        ExampleCore_2_1_2.TypeSample3 MethodTable: 00007ff962a094604 EEClass:     00007ff962a11f905 Tracked Type: false
6 Size:        32(0x20) bytes7 File:        E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_2_1_2\bin\Debug\net8.0\ExampleCore_2_1_2.dll8 Fields:9 MT    Field   Offset                 Type VT     Attr            Value Name10 00007ff962a09408  4000001        8 ...ample+Coordinates  1 instance 0000022d02409648 coordinates

红色标注的就是 TypeSample 类型的方法表的地址,我们可以继续使用【!dumpmt /d
00007ff962a09460
】命令输出详情。

1 0:000> !dumpmt 00007ff962a094602 EEClass:             00007ff962a11f903 Module:              00007ff9629de0a04 Name:                ExampleCore_2_1_2.TypeSample5 mdToken:             0000000002000003
6 File:                E:\Visual Studio 2022\.\ExampleCore_2_1_2\bin\Debug\net8.0\ExampleCore_2_1_2.dll7 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 00007ff9629de0a02 Name: E:\Visual Studio 2022\Source\...\ExampleCore_2_1_2\bin\Debug\net8.0\ExampleCore_2_1_2.dll3 Attributes:              PEFile4 TransientFlags:          00209011 
5 Assembly:                0000022cff24eba06 BaseAddress:             0000022CFF6E00007 PEAssembly:              0000022CFF24EA608 ModuleId:                00007FF9629DE4589 ModuleIndex:             0000000000000001
10 LoaderHeap:              00007FF9C280950811 TypeDefToMethodTableMap: 00007FF9629E432012 TypeRefToMethodTableMap: 00007FF9629E434813 MethodDefToDescMap:      00007FF9629E448814 FieldDefToDescMap:       00007FF9629E44B015 MemberRefToDescMap:      00007FF9629E43E816 FileReferencesMap:       0000000000000000
17 AssemblyReferencesMap:   00007FF9629E44E018 MetaData start address:  0000022CFF6E2168 (1788 bytes)

除了名称、属性、所属的程序集地址和加载器堆以外,还有一组映射(Map),这些映射只是将这些标记映射到底层的 CLR 数据结构。其实从命名上也可以看出端倪,不如:
TypeDefToMethodTableMap:
表示定义的类型与方法表中的映射,
MethodDefToDescMap
:定义的方法和描述符之间的映射。举个例子:如果要将一个方法定义标记映射到一个方法描述符,我们可以输出【
MethodDefToDescMap
】域的信息。我们使用【
dp 00007FF9629E4488
】查看。

1 0:000>dp 00007FF9629E44882 00007ff9`629e4488  00000000`0000000000007ff9`62a000c03 00007ff9`629e4498  00007ff9`62a000d8 00007ff9`62a093a84 00007ff9`629e44a8  00007ff9`62a093c0 00000000`00000000
5 00007ff9`629e44b8  00007ff9`62a09378 00007ff9`62a093d86 00007ff9`629e44c8  00007ff9`62a093e8 00007ff9`62a093f87 00007ff9`629e44d8  00000000`00000000 00000000`00000000
8 00007ff9`629e44e8  00007ff9`629dfbc8 00007ff9`62a097609 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`62a000c02 Method Name:          ExampleCore_2_1_2.Program.Main(System.String[])3 Class:                00007ff9629efbc04 MethodTable:          00007ff962a000e85 mdToken:              0000000006000001
6 Module:               00007ff9629de0a07 IsJitted:             yes8 Current CodeAddr:     00007ff9629519309 Version History:10   ILCodeVersion:      0000000000000000
11   ReJIT ID:           0
12 IL Addr:            0000022cff6e205013 CodeAddr:           00007ff962951930  (MinOptJitted)14      NativeCodeVersion:  0000000000000000
15 
16 0:000> !dumpmd /d 00007ff9`62a000d817 Method Name:          ExampleCore_2_1_2.Program..ctor()18 Class:                00007ff9629efbc019 MethodTable:          00007ff962a000e820 mdToken:              0000000006000002
21 Module:               00007ff9629de0a022 IsJitted:             no23 Current CodeAddr:     ffffffffffffffff24 Version History:25   ILCodeVersion:      0000000000000000
26   ReJIT ID:           0
27 IL Addr:            0000022cff6e207c28      CodeAddr:           0000000000000000(MinOptJitted)29      NativeCodeVersion:  0000000000000000
30 
31 0:000> !dumpmd /d 00007ff9`62a093a832 Method Name:          ExampleCore_2_1_2.TypeSample..ctor(Int32, Int32, Int32)33 Class:                00007ff962a11f9034 MethodTable:          00007ff962a0946035 mdToken:              0000000006000003
36 Module:               00007ff9629de0a037 IsJitted:             yes38 Current CodeAddr:     00007ff9629519d039 Version History:40   ILCodeVersion:      0000000000000000
41   ReJIT ID:           0
42 IL Addr:            0000022cff6e208543 CodeAddr:           00007ff9629519d0  (MinOptJitted)44      NativeCodeVersion:  0000000000000000
45 
46 0:000> !dumpmd /d 00007ff9`62a093c047 Method Name:          ExampleCore_2_1_2.TypeSample.AddCoordinates()48 Class:                00007ff962a11f9049 MethodTable:          00007ff962a0946050 mdToken:              0000000006000004
51 Module:               00007ff9629de0a052 IsJitted:             no53 Current CodeAddr:     ffffffffffffffff54 Version History:55   ILCodeVersion:      0000000000000000
56   ReJIT ID:           0
57 IL Addr:            0000022cff6e20b458      CodeAddr:           0000000000000000(MinOptJitted)59      NativeCodeVersion:  0000000000000000

【dumpmodule】命令不仅可以输出模块的特定信息,还可以输出在模块中定义和使用的所有类型。只要加上 -mt 命令开关。执行命令【
!dumpmodule -mt 00007ff9629de0a0
】查看模块中所有和使用的类型。

1 0:000> !dumpmodule -mt 00007ff9629de0a02 Name: E:\Visual Studio 2022\.\ExampleCore_2_1_2\bin\Debug\net8.0\ExampleCore_2_1_2.dll3 Attributes:              PEFile4 TransientFlags:          00209011 
5 Assembly:                0000022cff24eba06 BaseAddress:             0000022CFF6E00007 PEAssembly:              0000022CFF24EA608 ModuleId:                00007FF9629DE4589 ModuleIndex:             0000000000000001
10 LoaderHeap:              00007FF9C280950811 TypeDefToMethodTableMap: 00007FF9629E432012 TypeRefToMethodTableMap: 00007FF9629E434813 MethodDefToDescMap:      00007FF9629E448814 FieldDefToDescMap:       00007FF9629E44B015 MemberRefToDescMap:      00007FF9629E43E816 FileReferencesMap:       0000000000000000
17 AssemblyReferencesMap:   00007FF9629E44E018 MetaData start address:  0000022CFF6E2168 (1788bytes)19 
20 Types defined in thismodule21 
22 MT          TypeDef Name23 ------------------------------------------------------------------------------
24 00007ff962a000e8 0x02000002ExampleCore_2_1_2.Program25 00007ff962a09460 0x02000003ExampleCore_2_1_2.TypeSample26 00007ff962a09408 0x02000004 ExampleCore_2_1_2.TypeSample+Coordinates27 
28 Types referenced in thismodule29 
30 MT            TypeRef Name31 ------------------------------------------------------------------------------
32 00007ff962885fa8 0x0200000dSystem.Object33 00007ff9628860f0 0x0200000fSystem.ValueType34 00007ff962a09670 0x02000010System.Diagnostics.Debugger35 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 -l2 OS Thread Id: 0xa5c (0)3 Child SP               IP Call Site4 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 0000022d024096402 Name:        ExampleCore_2_1_2.TypeSample3 MethodTable: 00007ff962a094604 EEClass:     00007ff962a11f905 Tracked Type: false
6 Size:        32(0x20) bytes7 File:        E:\Visual Studio 2022\.\bin\Debug\net8.0\ExampleCore_2_1_2.dll8 Fields:9 MT    Field   Offset                 Type VT     Attr            Value Name10 00007ff962a09408  4000001        8 ...ample+Coordinates  1 instance 0000022d02409648 coordinates

我们知道了方法表,知道模块也就很容易了,执行命令【
!DumpMT /d 00007ff962a09460
】。

1 0:000> !DumpMT /d 00007ff962a094602 EEClass:             00007ff962a11f903 Module:              00007ff9629de0a04 Name:                ExampleCore_2_1_2.TypeSample5 mdToken:             0000000002000003
6 File:                E:\Visual Studio 2022\.\ExampleCore_2_1_2\bin\Debug\net8.0\ExampleCore_2_1_2.dll7 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 00007ff9629de0a02 Name: E:\Visual Studio 2022\.\ExampleCore_2_1_2\bin\Debug\net8.0\ExampleCore_2_1_2.dll3 Attributes:              PEFile4 TransientFlags:          00209011 
5 Assembly:                0000022cff24eba06 BaseAddress:             0000022CFF6E00007 PEAssembly:              0000022CFF24EA608 ModuleId:                00007FF9629DE4589 ModuleIndex:             0000000000000001
10 LoaderHeap:              00007FF9C280950811 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 00007FF9629E43202 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`62a000e82 EEClass:             00007ff9629efbc03 Module:              00007ff9629de0a04 Name:                ExampleCore_2_1_2.Program5 mdToken:             0000000002000002
6 File:                E:\Visual Studio 2022\Source\.\ExampleCore_2_1_2\bin\Debug\net8.0\ExampleCore_2_1_2.dll7 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`62a0946015 EEClass:             00007ff962a11f9016 Module:              00007ff9629de0a017 Name:                ExampleCore_2_1_2.TypeSample18 mdToken:             0000000002000003
19 File:                E:\Visual Studio 2022\Source\.ExampleCore_2_1_2\bin\Debug\net8.0\ExampleCore_2_1_2.dll20 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`62a0940828 EEClass:             00007ff962a1200829 Module:              00007ff9629de0a030 Name:                ExampleCore_2_1_2.TypeSample+Coordinates31 mdToken:             0000000002000004
32 File:                E:\Visual Studio 2022\Source\.\ExampleCore_2_1_2\bin\Debug\net8.0\ExampleCore_2_1_2.dll33 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 -l2 OS Thread Id: 0xa5c (0)3 Child SP               IP Call Site4 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 0000022d024096402 Name:        ExampleCore_2_1_2.TypeSample3 MethodTable: 00007ff962a094604 EEClass:     00007ff962a11f905 Tracked Type: false
6 Size:        32(0x20) bytes7 File:        E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_2_1_2\bin\Debug\net8.0\ExampleCore_2_1_2.dll8 Fields:9 MT    Field   Offset                 Type VT     Attr            Value Name10 00007ff962a09408  4000001        8 ...ample+Coordinates  1 instance 0000022d02409648 coordinates

以上红色标注的就是 EEClass 的地址,我们使用【!dumpclass /d
00007ff962a11f90
】命令显示其数据。

1 0:000> !dumpclass /d 00007ff962a11f902 Class Name:      ExampleCore_2_1_2.TypeSample3 mdToken:         0000000002000003 (这是类型定义,索引为值:3)
4 File:            E:\Visual Studio 2022\Source\P.\bin\Debug\net8.0\ExampleCore_2_1_2.dll5 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 Name14 00007ff962a09408  4000001        8 ...ample+Coordinates  1 instance           coordinates


四、总结
站在高人的肩膀之上,自己轻松了很多,但是,自己还是一个小学生,Net 高级调试这条路,也刚刚起步,还有很多要学的地方。皇天不负有心人,努力,不辜负自己,我相信付出就有回报,再者说,学习的过程,有时候,虽然很痛苦,但是,学有所成,学有所懂,这个开心的感觉还是不可言喻的。不忘初心,继续努力。做自己喜欢做的,开心就好。

技术背景

在python中定义一个列表时,我们一定要注意其中的可变对象的原理。虽然python的语法中没有指针,但是实际上定义一个列表变量时,是把变量名指到了一个可变对象上。如果此时我们定义另外一个变量也指到同一个可变对象的话,就会造成一个“联动”的现象。也就是改变其中的一个值时,另一个值也会随之而改变。本文使用的Python版本为Python 3.7.13

测试案例

这里我们先定义一个列表a,然后把这个空的列表a直接赋值给变量b,此时a和b都是一个空的列表:

In [1]: a = []

In [2]: b = a

In [3]: print (a,b)
[] []

那么如果此时我们修改a的值,那么此前被a赋值的变量b是否也会随之改变呢?

In [4]: a.append(1)

In [5]: print (a,b)
[1] [1]

In [6]: a.append(2)

In [7]: print (a,b)
[1, 2] [1, 2]

In [8]: a = [3]

In [9]: print (a,b)
[3] [1, 2]

In [10]: a.append(4)

In [11]: print (a,b)
[3, 4] [1, 2]

从运行结果来看,我们可以发现,当对a先后扩展一个元素1和2时,变量b的值也随之改变,跟a是同步变化的。但是如果把a这个变量名指向一个新的列表上,此时b的值不会发生变化。这就相当于,给变量a赋新的值的时候,变量b指向了a原来的值,而a这个变量名指向了新的数值,此后两者之间的关联就消失了。之所以没有指针定义的python编程语言,会出现这样的情况,就是因为列表类型属于可变参量,所以如果把两个变量指向同一个列表,两个变量的值是会同步的,即使初始的列表不是一个空的列表,结果也是一样的:

In [23]: a = [1]

In [24]: b = a

In [25]: a += [2]

In [26]: print (a,b)
[1, 2] [1, 2]

而且这个同步还是双向的,也就是说,修改a会同步到b,修改b也会同步到a:

In [11]: a = []

In [12]: b = a

In [13]: b.append(5)

In [14]: print (a,b)
[5] [5]

那么除了列表这个数据结构之外,其他类型的数据结构是否存在类似的现象呢?首先用字典类型来测试一下:

In [10]: a = {}

In [11]: b = a

In [12]: print (a,b)
{} {}

In [13]: a[1]=1

In [14]: print (a,b)
{1: 1} {1: 1}

经过测试我们发现,字典也是属于可变参量的类型。除了列表和字典外,其他的就是普通的数值类型和元组Tuple类型,还有一些第三方定义的数据类型,也可以分别测试一下:

In [15]: a = 1

In [16]: b = a

In [17]: a += 1

In [18]: print (a,b)
2 1

In [19]: a = (1,)

In [20]: b = a

In [21]: a += (2,)

In [22]: print (a,b)
(1, 2) (1,)

In [23]: a = '1'

In [24]: b = a

In [25]: a += '2'

In [26]: print (a,b)
12 1

测试结果表明,数值类型和元组类型在“链式”赋值之后,是直接把值给了其他变量的,而不是传递一个指针。但是另一个需要引起重视的是,第三方numpy所定义的array,也是一个可变参量:

In [19]: import numpy as np

In [20]: a = np.array([1], np.float32)

In [21]: b = a

In [22]: print (a,b)
[1.] [1.]

In [23]: a[0] = 2

In [24]: print (a,b)
[2.] [2.]

可以发现,a和b两者的结果也是同步变化的。因为没研究过Python的底层实现,也许
区分可变参量和非可变参量的方法,就是看其能不能被哈希

In [15]: hash(1)
Out[15]: 1

In [16]: hash([1])
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-16-0579e98ca3ee> in <module>
----> 1 hash([1])

TypeError: unhashable type: 'list'

In [17]: hash({'1':1})
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-17-b18acecf6a20> in <module>
----> 1 hash({'1':1})

TypeError: unhashable type: 'dict'

In [18]: hash((1,))
Out[18]: 3430019387558

In [29]: hash(np.array([1.]))
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-29-b9e8d96de6be> in <module>
----> 1 hash(np.array([1.]))

TypeError: unhashable type: 'numpy.ndarray'

In [30]: hash(np.array([1.]).tobytes())
Out[30]: 1211024724661850177

从结果中我们发现,那些可以被哈希的类型都是非可变参量,也就是在“链式赋值”的过程中不会发生“联动”的类型。

总结概要

假如你在Python中初始化了一个变量a的值,然后用a来初始化另一个变量b,此时你希望得到的b的数值是跟a同步变化的,还是独立变化的呢?Python这个编程语言虽然没有指针类型,但是Python中的可变参量也可以像指针一样,改变一个数值之后,所有指向该数值的可变参量都会随之而改变。就比如说改变a的值,会同步的去改变b的值。那么我们应该对这种类型的赋值有所了解,才能够避免在实际的编程中犯错。

版权声明

本文首发链接为:
https://www.cnblogs.com/dechinphy/p/syc-note.html

作者ID:DechinPhy

更多原著文章:
https://www.cnblogs.com/dechinphy/

请博主喝咖啡:
https://www.cnblogs.com/dechinphy/gallery/image/379634.html

中间件

Gin 框架允许开发者在处理请求的过程中,加入用户自己的钩子(Hook)函数。这个钩子函数就叫中间件,中间件适合处理一些公共的业务逻辑,比如登录认证、权限校验、数据分页、记录日志、耗时统计等。

通俗的讲
:中间件就是匹配路由前和匹配路由完成后执行的一系列操作

路由中间件

Gin 中的中间件必须是一个 gin.HandlerFunc 类型,配置路由的时候可以传递多个 func 回调函数,最后一个 func 回调函数前面触发的方法都可以称为中间件

// 请求方式的源码参数,... 可以传入多个func(context *gin.Context)
// 可以在func(context *gin.Context)之前传入自定义的一些处理方法
(relativePath string, handlers ...HandlerFunc)
定义和使用路由中间件
写法一
// 定义一个中间件要执行的方法
func MiddlewareFunc() {
	fmt.Println("中间件方法")

}

func ApiRoutersInit(router *gin.Engine) {

	apiRouter := router.Group("api")
	{
		apiRouter.GET("list",
			func(context *gin.Context) {
				// 请求前执行中间件方法
				MiddlewareFunc()
			},
			// 执行请求
			func(context *gin.Context) {
				context.String(http.StatusOK, "ok")

			})

	}
}

写法二
// 定义一个中间件要执行的方法
func MiddlewareFunc(context *gin.Context) {
	fmt.Println("中间件方法")

}

func ApiRoutersInit(router *gin.Engine) {

	apiRouter := router.Group("api")
	{	
		// 写法二
		apiRouter.GET("list", MiddlewareFunc,func(context *gin.Context) {
				context.String(http.StatusOK, "ok")

			})

	}
}

ctx.Next()

中间件里面加上 ctx.Next()可以让我们在路由匹配完成后执行一些操作

func MiddlewareFunc(context *gin.Context) {
	
	fmt.Println("请求执行前")
  // 调用该请求的剩余处理程序
	context.Next() 
  // 执行后面的func(context *gin.Context)方法
  // 每调用一次	context.Next() ,向后执行一个func(context *gin.Context)
  
  // 执行完之后再执行打印
	fmt.Println("请求执行完成")


}
ctx.Abort

Abort 是终止的意思, ctx.Abort() 表示终止调用该请求的剩余处理程序

func MiddlewareFunc(context *gin.Context) {
	fmt.Println("aaa")
	// 终止该请求的剩余处理程序 
	context.Abort()
	fmt.Println("这里继续打印")
  
  


}

全局中间件

func main() {
	router := gin.Default()

	// 在匹配路由之前配置全局中间件

	// 使用Use配置全局中间件,参数就是中间件方法,可以传入多个,
	router.Use(MiddlewareFunc1,MiddlewareFunc2)

	router.GET("/", func(context *gin.Context) {
		context.String(http.StatusOK, "ok")

	})

	// 将默认引擎传给其他文件定义的接收引擎的方法
	api.ApiRoutersInit(router)
	router.Run()

}

路由分组中间件

方法一
func ApiRoutersInit(router *gin.Engine) {
	// 在路由分组的Group后配置中间件
	apiRouter := router.Group("api",MiddlewareFunc)
	{
		apiRouter.GET("list",
			// 执行请求
			func(context *gin.Context) {
				context.String(http.StatusOK, "ok")

			})

	}
}

方法二
func ApiRoutersInit(router *gin.Engine) {
	apiRouter := router.Group("api")
	// 调用group对象 配置中间件
	apiRouter.Use(MiddlewareFunc)
	{
		apiRouter.GET("list",
			// 执行请求
			func(context *gin.Context) {
				context.String(http.StatusOK, "ok")

			})

	}
}

中间件和对应控制器共享数据

// 中间件

func MiddlewareFunc(context *gin.Context) {
	// 通过Set设置一个数据 k,v
	context.Set("name", "li")

}
// 控制器
func (a ApiController) ApiSetInfo(context *gin.Context) {
  // 通过.Get(key) 获取值,获取到的是一个any类型的值和是否异常的bool
	username, _ := context.Get("name")
	// 通过类型断言获取string类型的name
	name, _ := username.(string)
	context.String(http.StatusOK, name)
}

中间件注意事项

gin默认中间件

gin.Default()默认使用了 Logger 和 Recovery 中间件,其中:

• Logger 中间件将日志写入 gin.DefaultWriter,即使配置了 GIN_MODE=release。

• Recovery 中间件会 recover 任何 panic。如果有 panic 的话,会写入 500 响应码。

如果不想使用上面两个默认的中间件,可以使用 gin.New()新建一个没有任何默认中间件的路由

中间件中使用协程

当在中间件或 handler 中启动新的 goroutine 时,
不能使用
原始的上下文(c *gin.Context),必须使用其只读副本(c.Copy())

func MiddlewareFunc(context *gin.Context) {
	c := context.Copy()

	go func() {
		fmt.Println("url是", c.Request.URL)
	}()

	go func() {
		fmt.Println("body是", c.Request.Body)
	}()

}
	// 不需要wait等待协程完成,因为主程序main.go会一直执行

一、项目中使用 Jetpack Compose

从此节开始,为方便起见,如无特殊说明,Compose 均指代 Jetpack Compose。
开发工具: Android Studio

1.1 创建支持 Compose 新应用

新版 Android Studio 默认创建新项目即为 Compose 项目。

注意:在 Language 下拉菜单中,Kotlin 是唯一可用的选项,因为 Jetpack Compose 仅适用于使用 Kotlin 编写的类。
在 Minimum API level dropdown 菜单中,选择 API 级别 21 或更高级别。

1.2 为现有应用设置 Compose

如果要在现有项目中使用 Compose,只需要将一下定义添加到应用的
build.gradle
文件中:

android {
    buildFeatures {
        compose = true
    }

    composeOptions {
        kotlinCompilerExtensionVersion = "1.5.9"
    }
}
  • 在 Android
    BuildFeatures
    代码块内将
    compose
    标志设置为
    true
    会启用 Compose 功能。
  • ComposeOptions
    代码块中定义的 Kotlin 编译器扩展版本控制与 Kotlin 版本控制相关联。请参阅
    兼容性对应图
    ,并选择与项目的 Kotlin 版本匹配的库版本。

1.3 添加依赖

dependencies {

    val composeBom = platform("androidx.compose:compose-bom:2024.02.01")
    implementation(composeBom)
    androidTestImplementation(composeBom)

    // Choose one of the following:
    // Material Design 3
    implementation("androidx.compose.material3:material3")
    // or Material Design 2
    implementation("androidx.compose.material:material")
    // or skip Material Design and build directly on top of foundational components
    implementation("androidx.compose.foundation:foundation")
    // or only import the main APIs for the underlying toolkit systems,
    // such as input and measurement/layout
    implementation("androidx.compose.ui:ui")

    // Android Studio Preview support
    implementation("androidx.compose.ui:ui-tooling-preview")
    debugImplementation("androidx.compose.ui:ui-tooling")

    // UI Tests
    androidTestImplementation("androidx.compose.ui:ui-test-junit4")
    debugImplementation("androidx.compose.ui:ui-test-manifest")

    // Optional - Included automatically by material, only add when you need
    // the icons but not the material library (e.g. when using Material3 or a
    // custom design system based on Foundation)
    implementation("androidx.compose.material:material-icons-core")
    // Optional - Add full set of material icons
    implementation("androidx.compose.material:material-icons-extended")
    // Optional - Add window size utils
    implementation("androidx.compose.material3:material3-window-size-class")

    // Optional - Integration with activities
    implementation("androidx.activity:activity-compose:1.8.2")
    // Optional - Integration with ViewModels
    implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")
    // Optional - Integration with LiveData
    implementation("androidx.compose.runtime:runtime-livedata")
    // Optional - Integration with RxJava
    implementation("androidx.compose.runtime:runtime-rxjava2")
}

我们看一下新建的项目中,自动生成的 Activity 的代码如下:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            FirstComposeDemoTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    Greeting("Android")
                }
            }
        }
    }
}

@Composable
fun Greeting(name: String) {
    Text(
        text = "Hello $name!"
    )
}

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    FirstComposeDemoTheme {
        Greeting("Android")
    }
}

二、 Comppose API 设计原则

2.1 一切皆为函数

Compose 声明式 UI 的基础是 Composable 函数,使用 Compose, 需要通过定义一组接收数据而渲染界面元素的可组合函数来构建界面。
看上面的最简单的示例:
Greeting
widget, 它接收一个
String
并渲染出一个显示问候消息的
Text
widget。

@Composable
fun Greeting(name: String) {
    Text("Hello, $name")
}

运行效果如下:

对于需要渲染成界面的函数,称之为可组合函数,有一下特点:

  • 此函数带有
    @Composable
    注释,表明它是一个可组合函数,所有可组合函数都必须带有此注释。
  • 可组合函数需要在其它可组合函数的作用域内被调用。
  • 为了与普通函数区分,约定可组合函数首字母大写。

代码中还有一个,带有 @Preview 注解的 Composable 函数,顾名思义,该函数用来实时预览效果的。点击 design 选项,可看到预览的样式。Compose 强大的预览功能,大家可以自行探索。

我们自定义
Greeting
组件,里面实际上包含了一个
Text
组件, 点击跳转到 Text:

@Composable
fun Text(
    text: String,
    modifier: Modifier = Modifier,
    color: Color = Color.Unspecified,
    fontSize: TextUnit = TextUnit.Unspecified,
    fontStyle: FontStyle? = null,
    fontWeight: FontWeight? = null,
    fontFamily: FontFamily? = null,
    letterSpacing: TextUnit = TextUnit.Unspecified,
    textDecoration: TextDecoration? = null,
    textAlign: TextAlign? = null,
    lineHeight: TextUnit = TextUnit.Unspecified,
    overflow: TextOverflow = TextOverflow.Clip,
    softWrap: Boolean = true,
    maxLines: Int = Int.MAX_VALUE,
    onTextLayout: (TextLayoutResult) -> Unit = {},
    style: TextStyle = LocalTextStyle.current
) {
    // ...
}

可见框架提供的 Text 组件也是一个 Composable 函数。

Composable 函数通过多级嵌套形成结构化的函数调用链,函数调用链经过运行后生成 UI 一棵视图树。视图树一旦生成便不可随意改变,视图树的刷新依靠 Composable 函数的反复执行来实现,当需要显示的数据发生变化时,Composable 基于新的参数再次执行,更新底层的视图树。最终完成视图的刷新。
这个通过反复执更新视图树的过程称之为重组。后面的文章再详细介绍重组。

在 Compose 中,一切组件都是顶层函数,没有类的概念,自然也不会有任何的继承结构。

2.2 组合优于继承

看一个常用控件,按钮。

...
Surface(
    modifier = Modifier.fillMaxSize(),
    color = MaterialTheme.colorScheme.background
) {
    Column {
        Greeting("Android")
        Button(onClick = { /*TODO*/ }) {

        }
    }
}
...

为了方便演示,我这里增加了一个 Column 组件,相当于传统 View 视图中的垂直方向的线性布局。然后在里面增加了一个 Button 组件。

效果如上图,界面上多了一个按钮,我们没有设置 button 的颜色,它却默认与当前系统主题颜色适应了。点击,还能看到水波纹效果。这是因为我们使用的 Button 组件来自 Google material3 包里面,自动适配了这些。由于我们没有给按钮设置文本,所以按钮上并没有文字显示。那如 何给按钮添加文本呢?

@Composable
fun Button(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    shape: Shape = ButtonDefaults.shape,
    colors: ButtonColors = ButtonDefaults.buttonColors(),
    elevation: ButtonElevation? = ButtonDefaults.buttonElevation(),
    border: BorderStroke? = null,
    contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    content: @Composable RowScope.() -> Unit
) {
    // ...
}

我们跳转到 Button 的源码,却并没有发现类似 Text 组件一样的 text 参数。也就是说,我们并不能通过设置参数的方式,给 Button 组件设置文本,那要怎么做?
从源码我们看到,有两个参数是没有默认值的,需要我们调用时传入。一个是
onClick
, 这里我们传入了一个空的 lambda 表达式,另一个没有默认值的参数
content
,类型是
@Composable RowScope.() -> Unit
,其实是要求传入一个 Composable ,并且,它提供的作用域是 RowScope。看代码就明白了:

...
Column {
    Greeting("Android")
    Button(onClick = { /*TODO*/ }) {

    }
}
...

我们成功给 Button 组件添加上了文字,不是通过参数的形式设置的,二是将一个 Button 组件和一个 Text 组件组合起来,形成了一个带有文本的按钮。仔细想一下,这样的设计是否更合理,Button 本身的作用就是提供点击时间,Text 提供文本作用的。从设计模式的角度来讲,各个组件职责更单一。也变面出现了上文中提到的 “带有剪贴板功能的按钮” 这种问题。

这也是为什么说组合优于继承。

2.3 单一数据源

单一数据源是包括 Compose 在内的所有声明式 UI 框架的一个重要原则。
回想传统 View 视图中的 EditText 控件。它的文本变化可能来自用用户的输入,也可能来自代码某处的
setText
。这种多数据源在状态变化的情况下不容易跟踪,且状态源过度分散,会增加状态同步的工作量,比如 EditText 内部持有一个 mText 状态,其它组件需要监听它的状态变化,同时,它还有可能需要监听其它组件的状态变化。
我们再看看在 Compose 中,是如何实现 EditText 的效果的。

Compose 提供了
TextField
作为常用的文本输入框。它也遵循 Meterial Design 设计准则。看看它最简单的使用方式:

Column {
    Greeting("Android")
    Button(onClick = { /*TODO*/ }) {
        Text("I’m a button")
    }

    var text by remember { mutableStateOf("文本框初始值") }
    TextField(value = text, onValueChange = {
        text = it
    })
}

效果如下:

这里出现了关于 State 的使用,关于状态,将在下一篇文章中讲解,这里只需要知道,TextField 的参数
value
是唯一能决定其显示文本的数据源。我们定义了一个状态变量
text
, 并设置给了 value 参数。如果给 value 传入一个固定的字符串,则无论在键盘上输入什么,TextField 的显示都不会改变。
onValueChange
参数这个回到中,可以获取到当前来自软键盘的最新输入。我们利用这个信息来更新可变状态 text, 驱动界面刷新来显示最新的输入文本。

三、Compose 与 View 互操作

Compose 生成的 UI 树节点是 LayoutNode, View 生成的 UI 树节点是 View 和 ViewGroup, 两者之间可以共存与一棵树中,就像 DOM 节点可以依靠 Webview 挂载到 View 树一样, Compose 与 View 之间也存在这样的桥梁,使得两者可以共同存在。

3.1 Compose 中使用 View

什么时候会在 Compose 中使用 View 呢?

  • 极少数 View 暂时还没有 Compose 版本,比如 MapView, WebView
  • 有一块之前写好的 UI, (暂时或者永远)不想动,想直接拿过来用
  • 初学者用 Compose 实现不了想要的效果,先用 View

3.1.1 Compose 中使用 AndroidView

看例子:

@Composable
fun MyTextView(text: String) {
    AndroidView(
        fatory = { context ->
            TextView(context).apply {
                setText(text)
            }
        },
        update = { view -> 
            view.setText(text)
        }
    )
}

这个桥梁是
AndroidView
, 它是一个 Composable 函数。

@Composable
fun <T: View> AndroidView(
    fatory: (context) -> T,
    modifier: Modifier = Modifier,
    update: (T) -> Unit = NoOpUpdate
)

fatory 接收一个 Context 参数,用来构建一个 View, update 方法是一个 callback, inflate 之后会执行,读取的状态 state 值变化后,也会被执行。

3.1.2 Compose 中使用 xml 布局

上面使用 AndroidView 适用于少量的 UI, 如果需要复用一个已经存在的 xml 布局,怎么办?

  • 首先开启 viewBinding
android {
    buildFeatures {
        compose = true
        viewbinding = true
    }
}
  • 添加 Compose viewbinding 依赖
implementation("androidx.compose.ui:ui-viewbinding:1.5.4")

使用过 ViewBinding 的同学应该清楚,build 之后,会根据 xml 文件生成对应的 Binding 类,例如
TestLayoutBinding

@Composable
fun TestComposableLayout() {
    AndroidViewBinding(TestLayoutBinding::inflate) {
        testButton.setOnClickListener {
            //...
        }
    }
}

其实 AndroidViewBinding 内部还是调用了 AndroidView 这个 Composable 函数。

3.2 View 中使用 Compose

使用
ComposeView
作为桥梁。
普通 xml 文件中加入 ComposeView

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com.apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <TextView id="@+id/tv_test"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="test" />

    <androidx.compose.ui.platform.ComposeView
        android:id="@+id/compose_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>

代码中,先根据 id 查找出来,再 setContent 即可:

findViewById<ComposeView>(R.id.compose_view).setContent {
    Text("I'm Composable")
}

动态添加也可:

addView(ComposeView(this@MainActivity).apply {
    setContent {
        Text("I'm Composable")
    }
})

这里起到桥梁作用的 ComposeView, 本质上是一个 ViewGroup, 它的 setContent() 方法开启了 Compose 世界的大门,在这里可以传入 Composable 函数。

小结:

  • Compose 中调用 View, 借助 AndroidView
  • View 中调用 Compose,借助 ComposeView
    Compose 和 View 的互操作性也保证了项目可以逐步迁移。

DevToys

  • 项目简介:DevToys是一个专门为开发者设计的Windows工具箱,完全支持离线运行,无需使用许多不真实的网站来处理你的数据,常用功能有:格式化(支持 JSON、SQL、XML)、JWT解码、URL编码/解码、UUID生成、图片压缩、文本比较、正则表达式测试、Markdown预览等28+种实用工具。
  • 项目源码地址:
    https://github.com/veler/DevToys
  • 公众号详细介绍:
    https://mp.weixin.qq.com/s/Dg7mGLXYKKIwfHAv2GEkVQ

Microsoft PowerToys

1Remote

ScreenToGif

  • 项目简介:ScreenToGif 是一款免费的开源屏幕录制和GIF 制作工具。它可以帮助用户捕捉计算机屏幕上的实时动画,并将其保存为高质量的 GIF 图像格式。该工具不仅适用于技术支持、软件演示和教程制作,还可以用于创建有趣的 GIF 图片和动画表情。。
  • 项目源码地址:
    https://github.com/NickeManarin/ScreenToGif
  • 公众号详细介绍:
    https://mp.weixin.qq.com/s/dj_EMNDCIo4s5nljzrNvww

GeekDesk

QuickLook

Optimizer

  • 项目简介:Optimizer是一款功能强大的Windows系统优化工具,可帮助用户提高计算机性能、加强隐私和安全保护。该工具支持22种语言,同时提供了许多实用的功能,如关闭不必要的Windows服务、停止Windows自动更新、卸载UWP应用、清理系统垃圾文件和浏览器配置文件、修复常见的注册表问题等。此外,Optimizer还提供了硬件检测工具、IP连通性和延迟测试工具、快速更改DNS服务器、编辑HOSTS文件、识别和终止文件锁定句柄等实用工具。
  • 项目源码地址:
    https://github.com/hellzerg/optimizer
  • 公众号详细介绍:
    https://mp.weixin.qq.com/s/-7r0p75xV4Q_t3Ny5cvcvw

ToastFish

WinMemoryCleaner

Files

优秀项目和框架精选


以上Windows软件都已收录到C#/.NET/.NET Core优秀项目和框架精选中,关注优秀项目和框架精选能让你及时了解C#、.NET和.NET Core领域的最新动态和最佳实践,提高开发工作效率和质量。坑已挖,欢迎大家踊跃提交PR推荐或自荐(让优秀的项目和框架不被埋没