松風水月 发表于 2023-10-30 14:03:31

Net 高级调试之三:类型元数据介绍(同步块表、类型句柄、方法描述符等)

一、简介
    今天是《Net 高级调试》的第三篇文章,压力还是不小的。上一篇文章,我们浅浅的谈了谈 CLR 和 Windows 加载器是如何加载 Net 程序集的,如何找到程序的入口点的,有了前面的基础,我们今天看一点更详细的东西。既然 Windows 操作系统已经加载了 CLR,初始化了应用程序域,加载了我们的 Net 程序,那我们就看看Net 类型在内存中的具体样子。这一篇文章还是有一点难度的,我看第一遍视频的时候,也不知道说了个啥,后来又看了《Net 高级调试》,似懂非懂。一遍不行,那就再来一遍,还不行,那就再来一遍,俗话说的好,书读千遍,其意自现。

    如果在没有说明的情况下,所有代码的测试环境都是 Net Framewok 4.8,但是,有时候为了查看源码,可能需要使用 Net Core 的项目,我会在项目章节里进行说明。好了,废话不多说,开始我们今天的调试工作。
    调试环境我需要进行说明,以防大家不清楚,具体情况我已经罗列出来。
          操作系统:Windows Professional 10
          调试工具:Windbg Preview(可以去Microsoft Store 去下载)
          开发工具:Visual Studio 2022
          Net 版本:Net Framework 4.8
          CoreCLR源码:源码下载

二、相关知识
    我们知道了 CLR,了解了 JIT,晓得了 Net 的编译过程,也真正做到了眼见为实,所有的知识点都有根了,这次好好的研究一下类型的东西,当然,这写东西平时时很难遇到的,就是不懂,也可以写出东西。但是,如果要想做到,知其一也要知其二的话,这些只是还是有必要了解的,对我们写出高效的代码还是很有帮助的,一以下就是相关的知识点,我一一罗列出来。

    栈stack(先进后出)是编译期间就分配好的内存空间,因此你的代码中必须就栈的大小有明确的定义;

    堆heap(队列优先,先进先出)是程序运行期间动态分配的内存空间,你可以根据程序的运行情况确定要分配的堆内存的大小。

    1、简介
        类型是 Net 程序中基本编程单元,类型又可以细分为:值类型,引用类型。
        a)、值类型
          枚举【enum】,结构【Struct】和其他简单类型,比如:int,float,double,char,bool等。这些类型占据的空间小,一般存放在线程栈上,当然也可以保存在寄存器中、托管堆中或者是私有堆中。
        b)、引用类型
          接口、数组、类和我们自定义的 Class,都是引用类型,这样的类型,一般占据的空间比较大,它们存在托管堆中,由 GC 负责分配内存和回收内存来管理这些引用类型的实例。

    2、值类型布局
        一般而言,方法的参数、在方法内部声明的局部变量都是存放在当前的线程栈上,也就是说在线程栈上直接存储值类型的值。

        https://img2023.cnblogs.com/blog/1048776/202310/1048776-20231026134438610-850574000.png

    3、引用类型布局
        class 类型是一种引用类型,实例对象在托管堆中分配空间,并将对象的首地址存在栈地址上。
        https://img2023.cnblogs.com/blog/1048776/202310/1048776-20231026134613583-1804275356.png 

    4、同步块表
        这个名称叫的不太准确,叫 ObjectHeader 更好点,因为源码中就是叫这个名称。托管堆上的每个对象的前面都有一个同步块索引,它指向 CLR 中私有堆上的同步块表,同步块表中可以包含很多信息,比如:对象散列码、锁信息、应用程序域索引。
        https://img2023.cnblogs.com/blog/1048776/202310/1048776-20231030111316904-2105317722.png
        
    5、类型句柄(方法表)
        类型句柄是针对类型的描述信息,比如:这个类中有多少个方法,方法的结构,方法的字段信息等。
        https://img2023.cnblogs.com/blog/1048776/202310/1048776-20231030111316904-2105317722.png
    6、方法描述符
        用来描述C# 方法在 CLR 层面的特征,使用 MethodDesc 类结构来承载,记录了方法的字节码,所属类,Token 等信息。

    7、模块
        模块是包含在程序集中,程序集是一个 Net 程序的部署单元,可以用 !dumpAssembly 和 !dumpmodule 显示各自的信息。

    8、元数据标记
        因为程序集是自描述的,类型信息都有响应的 Metadata 来表示,可以使用 ILSpy 来查看。可以使用 !token2ee 来检索对应的方法。
        
    9、EEClass
        EEClass 和 MethodTable 是同级别的,用来描述 C# 的一个类,可以使用 !dumpclass 来显示类型的 EECLass 信息。

三、调试过程
    废话不多说,这一节是具体的调试操作的过程,有可以说是眼见为实的过程,在开始之前,我还是要啰嗦两句,这一节分为两个部分,第一部分是测试的源码部分,没有代码,当然就谈不上测试了,调试必须有载体。第二部分就是根据具体的代码来证实我们学到的知识,是具体的眼见为实。
    1、测试源码
        1.1、Example_3_1_1
https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gifhttps://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif 1 namespace Example_3_1_1 2 { 3   internal class Program 4   { 5         static void Main(string[] args) 6         { 7             int a = 10; 8             long b = 11; 9             short c = 12;10             Console.ReadLine();11         }12   }13 }View Code
        1.2、Example_3_1_2
https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gifhttps://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif 1 namespace Example_3_1_2 2 { 3   internal class Program 4   { 5         static void Main(string[] args) 6         { 7             var person = new Person() 8             { 9               Name = "jack",10               Age = 2011             };12             Console.ReadLine();13         }14   }15 16   public class Person17   {18         public string Name { get; set; }19 20         public int Age { get; set; }21   }22 }View Code
        1.3、Example_3_1_3            https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gifhttps://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif 1 namespace Example_3_1_3 2 { 3   internal class Program 4   { 5         static void Main(string[] args) 6         { 7             var person = new Person() { Name = "jack", Age = 20 }; 8             var hashcode = person.GetHashCode().ToString("x"); 9             Console.WriteLine($"hashcode={hashcode}");10             Debugger.Break();11             Console.ReadLine();12         }13   }14 15   public class Person16   {17         public string Name { get; set; }18 19         public int Age { get; set; }20   }21 }View Code
        1.4、Example_3_1_4            https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gifhttps://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif 1 namespace Example_3_1_4 2 { 3   internal class Program 4   { 5         public static Person person=new Person(); 67         static void Main(string[] args) 8         { 9             Task.Run(() =>10             {11               lock (person)12               {13                     Console.WriteLine($"tid={Environment.CurrentManagedThreadId}进入锁了");14                     Console.ReadLine();15               }16             });17             Task.Run(() => {18               lock (person)19               {20                     Console.WriteLine($"tid={Environment.CurrentManagedThreadId}进入锁了");21                     Console.ReadLine();22               }23             });24 25             Console.ReadLine();26         }27   }28 29   public class Person30   {31         public string Name { get; set; }32 33         public int Age { get; set; }34   }35 }View Code
        1.5、Example_3_1_5     
https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gifhttps://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif 1 namespace Example_3_1_5 2 { 3   internal class Program 4   { 5         static void Main(string[] args) 6         { 7             var person = new Person() 8             { 9               Name = "jack",10               Age = 2011             };12             Console.WriteLine("Hello World!");13             Console.ReadLine();14         }15   }16   public class Person17   {18         public string Name { get; set; }19 20         public int Age { get; set; }21   }22 }View Code 
        1.6、Example_3_1_5_1(这个项目是 Net 7.0版本的)
https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gifhttps://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif 1 namespace Example_3_1_5_1 2 { 3   internal class Program 4   { 5         static void Main(string[] args) 6         { 7             var person = new Person() 8             { 9               Name = "jack",10               Age = 2011             };12             Console.WriteLine("Hello World!");13             Console.ReadLine();14         }15   }16   public class Person17   {18         public string Name { get; set; }19 20         public int Age { get; set; }21   }22 }View Code
  
    2、眼见为实
        2.1、值类型的布局
            代码样例:Example_3_1_1
            我们使用 Windbg Preview 调试器,通过【launch executable】菜单加载【Example_3_1_1.exe】项目,通过【g】命令,运行程序,调试器运行代【Console.ReadLine()】次会暂停执行,然后我们点击【break】按钮,进入调试状态。我们还需要通过【~0s】命令,切换到主线程,当然,我们可以使用【cls】命令清理一下调试器显示的过多信息,自己来决定,我是会清理的。
             !clrstack -l 这个命令是显示当前的线程调用栈局部变量,l 表示 local,局部变量,代码关键部分
1 0:000> !clrstack -l 2 OS Thread Id: 0x317c (0) 3 Child SP       IP Call Site 4 00aff1c4 778e10fc 5 00aff1c0 6fee9b71 ...(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte*, Int32, Int32 ByRef, IntPtr) 67 ...... 89 00aff2c0 00d3089e Example_3_1_1.Program.Main(System.String[]) 10   LOCALS:【表示局部变量】11         0x00aff2d0 = 0x0000000a【0x00aff2d0是栈地址,0x0000000a 是栈上的值,这是十六进制的】12         0x00aff2c8 = 0x0000000b【0x00aff2c8是栈地址,0x0000000b 是栈上的值,这是十六进制的】13         0x00aff2c4 = 0x0000000c【0x00aff2d0是栈地址,0x0000000c 是栈上的值,这是十六进制的】14 15 00aff448 70f1f036             以上显示的红色部分是最重要的,LOCALS 表示局部变量,11,12,13 三行是具体的局部变量,等号前面是 线程栈上的变量地址,后面是具体的值,我们可以使用【?】命令查看具体的值。
https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gifhttps://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif1 0:000> ? 0x0000000a2 Evaluate expression: 10 = 0000000a3 0:000> ? 0x0000000b4 Evaluate expression: 11 = 0000000b5 0:000> ? 0x0000000c6 Evaluate expression: 12 = 0000000cView Code            对应 C# 代码中的赋值操作。
            https://img2023.cnblogs.com/blog/1048776/202310/1048776-20231026140805064-2043092061.png
            由于栈的特点,先进后出,后进先出,所以说【a】是最先入栈的,在栈底,依次是【b】,最上面的是【c】,所以我们从【c】的地址打印,可以显示【c、b、a】的值。由此,我们执行【dp】命令,效果如下。
1 0:000> dp 0x00aff2c4 l42 00aff2c40000000c 0000000b 00000000 0000000a            我们可以继续验证,由于栈的地址是由高到低的分配,所以,【c】的地址加上 0x4,为什么加4呢,虽然【c】占用2个字节,但是会按4个字节算的,就是【b】变量的值,如下:1 0:000> dp 00aff2c4+0x4 l12 00aff2c80000000b            继续验证,【b】的地址加上 0x8,就是【a】变量的值,为什么是加8呢,因为【b】占用8个字节,如下:
1 0:000> dp 00aff2c8+0x8 l12 00aff2d00000000a            当然,我们可以以【c】变量的地址为基准,算出【b】和【a】的值,如下:
1 0:000> dp 0x00aff2c4+0x4 l1(以c 的地址为基准,找到b的地址,加4)2 00aff2c80000000b3 0:000> dp 0x00aff2c4+0xc l1(以c 的地址为基准,找到a的地址,加12,十六进制就是0xc)4 00aff2d00000000a
        2.2、引用类型的布局
            代码样例:Example_3_1_2
            我们使用 Windbg Preview 调试器,通过【launch executable】菜单加载【Example_3_1_2.exe】项目,通过【g】命令,运行程序,调试器运行代【Console.ReadLine()】次会暂停执行,然后我们点击【break】按钮,进入调试状态。我们还需要通过【~0s】命令,切换到主线程,当然,我们可以使用【cls】命令清理一下调试器显示的过多信息,自己来决定,我是会清理的。
            我们先使用【!clrstack -a】命令,查看线程栈的局部变量。 1 0:000> !clrstack -a 2 OS Thread Id: 0x3930 (0) 3 Child SP       IP Call Site 4 0133ee8c 778e10fc 5 0133ee88 6fee9b71 6 ...... 7 0133ef88 018c08b1 Example_3_1_2.Program.Main(System.String[]) 8   PARAMETERS: 9         args (0x0133ef94) = 0x033b24bc10     LOCALS:11         0x0133ef90 = 0x033b24e0(0x0133ef90 是栈地址,0x033b24e0 person变量的引用地址)12 13 0133f108 70f1f036             我们可以通过【dp】命令查看栈地址,值是 033b24e0,这个值就是 person变量引用的地址。
1 0:000> dp 0x0133ef90 l12 0133ef90033b24e0(这个地址就是 person变量的地址)            我们可以使用【!do|!DumpObj】命令,查看对象的详情。
1 0:000> !DumpObj /d 033b24e0 2 Name:      Example_3_1_2.Person 3 MethodTable: 01874e1c 4 EEClass:   01871314 5 Size:      16(0x10) bytes 6 File:      E:\Visual Studio 2022\Source\Projects\......\Example_3_1_2\bin\Debug\Example_3_1_2.exe 7 Fields: 8       MT    Field   Offset               Type VT   Attr    Value Name 9 6fa424e44000001      4      System.String0 instance 033b24c8 k__BackingField10 6fa442a84000002      8         System.Int321 instance       20 k__BackingField            033b24c8 k__BackingField,这个是 string 类型的字段,033b24c8又是一个引用地址,我们继续【!do】,查看详情。
1 0:000> !DumpObj /d 033b24c8 2 Name:      System.String 3 MethodTable: 6fa424e4 4 EEClass:   6fb47690 5 Size:      22(0x16) bytes 6 File:      C:\Windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll 7 String:      jack(这个就是我们赋值的) 8 Fields: 9       MT    Field   Offset               Type VT   Attr    Value Name10 6fa442a84000283      4         System.Int321 instance      4 m_stringLength11 6fa42c9c4000284      8          System.Char1 instance       6a m_firstChar12 6fa424e44000288       70      System.String0   shared   static Empty13   >> Domain:Value0151ca70:NotInit   !do 033b24c8 2 Name:      System.String 3 MethodTable: 6fa424e4 4 EEClass:   6fb47690 5 Size:      22(0x16) bytes 6 File:      C:\Windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll 7 String:      jack 8 Fields: 9       MT    Field   Offset               Type VT   Attr    Value Name10 6fa442a84000283      4         System.Int321 instance      4 m_stringLength11 6fa42c9c4000284      8          System.Char1 instance       6a m_firstChar12 6fa424e44000288       70      System.String0   shared   static Empty13   >> Domain:Value0151ca70:NotInit   !dumpmt 01874e1c 2 EEClass:         01871314 3 Module:          01874044 4 Name:            Example_3_1_2.Person 5 mdToken:         02000003 6 File:            E:\Visual Studio 2022\Source\Projects\......\Example_3_1_2\bin\Debug\Example_3_1_2.exe 7 BaseSize:      0x10 8 ComponentSize:   0x0 9 Slots in VTable: 910 Number of IFaces in IFaceMap: 0
        2.3、同步块包含对象散列码
            代码样例:Example_3_1_3
            我们使用 Windbg Preview 调试器,通过【launch executable】菜单加载【Example_3_1_3.exe】项目,通过【g】命令,运行程序,调试器运行代【Debugger.Break()】次会暂停执行,我们程序的输出结果是:hashcode=2bf8098。
            接下来,我们看看对象头中是否散列码,就可以检验了。我们先使用【!clrstack -l】命令,看看线程栈。
1 0:000> !clrstack -l 2 OS Thread Id: 0x2600 (0) 3 Child SP       IP Call Site 4 00dcef18 7696f262 System.Diagnostics.Debugger.BreakInternal() 5 00dcef94 705bf195 System.Diagnostics.Debugger.Break() 67 00dcefbc 02f40905 Example_3_1_3.Program.Main(System.String[]) 8   LOCALS: 9         0x00dcefd0 = 0x030b251010         0x00dcefcc = 0x030b39ac11         0x00dcefd8 = 0x02bf809812 13 00dcf154 70f1f036             0x00dcefd0 = 0x030b2510,这个地址就是我们声明的 person 变量。既然由了对象的地址,只要用对象的地址,减去 0x4,就是同步块的地址,然后使用【dp】命令就可以查看了。   
1 0:000> dp 0x030b2510-0x4 l42 030b250c0ebf8098 01414e1c 030b24c8 00000014            第二行的第二列以前是0,表示没有任何数据,现在有值了。现在我们用这个值,减去我们得到的散列码,看看是什么。
1 0:000> ? 0ebf8098-2bf80982 Evaluate expression: 201326592 = 0c000000            0c000000它就是一个掩码,告诉CLR 这个字段中包含的是散列码的值,起到标识的作用,因为还可以存放其他东西。

        2.4、同步块包含对象锁信息
            代码样例:Example_3_1_4
            我们使用 Windbg Preview 调试器,通过【launch executable】菜单加载【Example_3_1_4.exe】项目,通过【g】命令,运行程序,调试器运行代【Console.ReadLine()】次会暂停执行,然后我们点击【break】按钮,进入调试状态,此时,我们程序的输出是:tid=3进入锁了,说明 Person 被锁住了。
            接下来,我们就要查看对象的对象头包含什么东西,意图很明显。
            我们首先找到 Person 对象,可以使用【!dumpheap -type Person】命令获取对象。1 0:001> !dumpheap -type Person2Address       MT   Size3 033824c8 014d4e60       16   4 5 Statistics:6       MT    Count    TotalSize Class Name7 014d4e60      1         16 Example_3_1_4.Person8 Total 1 objects            红色标记的就是Person 对象的地址,然后我们使用这个地址减去 0x4,就可以获取同步块索引了。    
1 0:001> dp 033824c8-0x4 l42 033824c408000007 014d4e60 00000000 00000000            08000007 就是同步块索引的值,08是一个掩码,表示是同步块索引,07就是线程 Id。我们可以使用【!syncblk】命令来验证。
1 0:001> !syncblk 2 Index         SyncBlock MonitorHeld Recursion Owning Thread Info          SyncBlock Owner 3   6 015670f0            3         1 01512ba8 3d4c   0   03388210 System.IO.TextReader+SyncTextReader 4   7 01567124            3         1 0157c340 f8   9   033824c8 Example_3_1_4.Person(被锁的对象是 person)              3:(一个线程持有锁,一个等待锁)
5 ----------------------------- 6 Total         7 7 CCW             1 8 RCW             2 9 ComClassFactory 010 Free            0            这里是9,为什么我们的程序输出是3,3是托管线程的编号。9是windbg 标识的号码。
1 0:001> !t 2 ThreadCount:      4 3 UnstartedThread:0 4 BackgroundThread: 3 5 PendingThread:    0 6 DeadThread:       0 7 Hosted Runtime:   no 8   (托管线程ID)                                                         Lock   9      ID OSID ThreadOBJ    State GC Mode   GC Alloc ContextDomain   Count Apt Exception10    0    1 3d4c 01512ba8   2a020 Preemptive03388254:00000000 0150ca30 1   MTA 11    5    2 324c 0154f738   2b220 Preemptive00000000:00000000 0150ca30 0   MTA (Finalizer) 12    9    3   f8 0157c340   3029220 Preemptive03387214:00000000 0150ca30 1   MTA (Threadpool Worker) 13   11    4264 0157cd28   3029220 Preemptive0338A21C:00000000 0150ca30 0   MTA (Threadpool Worker)              我们可以切换到9好线程,看看他的线程栈。
1 0:001> ~~s 2 eax=00000000 ebx=00000001 ecx=00000000 edx=00000000 esi=00000001 edi=00000001 3 eip=778e166c esp=05e2f0e8 ebp=05e2f278 iopl=0         nv up ei pl nz na pe nc 4 cs=0023ss=002bds=002bes=002bfs=0053gs=002b             efl=00000206 5 ntdll!NtWaitForMultipleObjects+0xc: 6 778e166c c21400          ret   14h 789 0:009> !clrstack10 OS Thread Id: 0xf8 (9)11 Child SP       IP Call Site12 05e2f444 778e166c 13 05e2f524 778e166c System.Threading.Monitor.Enter(System.Object)14 05e2f59c 7076377b System.IO.TextReader+SyncTextReader.ReadLine() 15 05e2f5ac 705c1845 System.Console.ReadLine() 16 05e2f5b4 016f0ae8 Example_3_1_4.Program+c.b__1_0() 17 05e2f600 6fe8d4bb System.Threading.Tasks.Task.InnerInvoke() 18 05e2f60c 6fe8b731 System.Threading.Tasks.Task.Execute() 19 05e2f630 6fe8b6fc System.Threading.Tasks.Task.ExecutionContextCallback(System.Object) 20 05e2f634 6fe28604 System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean) 21 05e2f6a0 6fe28537 System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean) 22 05e2f6b4 6fe8b4b2 System.Threading.Tasks.Task.ExecuteWithThreadLocal(System.Threading.Tasks.Task ByRef) 23 05e2f718 6fe8b357 System.Threading.Tasks.Task.ExecuteEntry(Boolean) 24 05e2f728 6fe8b29d System.Threading.Tasks.Task.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem() 25 05e2f72c 6fdfeb7d System.Threading.ThreadPoolWorkQueue.Dispatch() 26 05e2f77c 6fdfe9db System.Threading._ThreadPoolWaitCallback.PerformWaitCallback() 27 05e2f99c 70f1f036
        2.5、查看类型句柄
            代码样例:Example_3_1_5
            我们使用 Windbg Preview 调试器,通过【launch executable】菜单加载【Example_3_1_5.exe】项目,通过【g】命令,运行程序,调试器运行代【Console.ReadLine()】次会暂停执行,然后我们点击【break】按钮,进入调试状态。我们还需要通过【~0s】命令,切换到主线程,当然,我们可以使用【cls】命令清理一下调试器显示的过多信息,自己来决定,我是会清理的。
            我们先去托管堆中查找一下 Person 对象,使用【!dumpheap -type Person】。红色标注的就是 Person 的引用地址。
1 0:000> !dumpheap -type Person2Address       MT   Size3 02d72508 01004e1c       16   4 5 Statistics:6       MT    Count    TotalSize Class Name7 01004e1c      1         16 Example_3_1_5.Person8 Total 1 objects            我们由了 Person 对象的指针地址,就可以通过这个地址查看它的方法表的信息了。
1 0:000> !DumpObj /d 02d72508 2 Name:      Example_3_1_5.Person 3 MethodTable: 01004e1c 4 EEClass:   01001318 5 Size:      16(0x10) bytes 6 File:      E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\Example_3_1_5\bin\Debug\Example_3_1_5.exe 7 Fields: 8       MT    Field   Offset               Type VT   Attr    Value Name 9 6fa424e44000001      4      System.String0 instance 02d724c8 k__BackingField10 6fa442a84000002      8         System.Int321 instance       20 k__BackingField            当然,我们通过【dp】命令也能证明类型句柄的信息。标红的 01004e1c 就是方法表的地址。
1 0:000> dp 02d72508 l42 02d7250801004e1c 02d724c8 00000014 00000000            我们可以【!dumpmt -md】列出所有的方法描述信息。
https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gifhttps://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif 1 0:000> !dumpmt -md 01004e1c 2 EEClass:         01001318 3 Module:          01004044 4 Name:            Example_3_1_5.Person 5 mdToken:         02000003 6 File:            E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\Example_3_1_5\bin\Debug\Example_3_1_5.exe 7 BaseSize:      0x10 8 ComponentSize:   0x0 9 Slots in VTable: 910 Number of IFaces in IFaceMap: 011 --------------------------------------12 MethodDesc Table13    Entry MethodDe    JIT Name14 6fe397b8 6fa3c838 PreJIT System.Object.ToString()15 6fe396a0 6fb78978 PreJIT System.Object.Equals(System.Object)16 6fe421f0 6fb78998 PreJIT System.Object.GetHashCode()17 6fdf4f2c 6fb789a0 PreJIT System.Object.Finalize()18 02c008d8 01004e08    JIT Example_3_1_5.Person..ctor()19 02c0044d 01004dd8   NONE Example_3_1_5.Person.get_Name()20 02c00910 01004de4    JIT Example_3_1_5.Person.set_Name(System.String)21 02c00455 01004df0   NONE Example_3_1_5.Person.get_Age()22 02c00950 01004dfc    JIT Example_3_1_5.Person.set_Age(Int32)View Code            PreJIT表示已经预编译了,JIT表示已经被 JIT 编译过了,NONE表示还没有被 JIT 编译过。

        2.6、查看 MethodTable 详情(Net Framework是闭源的,看不到,NetCore是可以的)
            代码样例:Example_3_1_5_1
            我们使用 Windbg Preview 调试器,通过【launch executable】菜单加载【Example_3_1_5.exe】项目,通过【g】命令,运行程序,调试器运行代【Console.ReadLine()】次会暂停执行,然后我们点击【break】按钮,进入调试状态。当然,我们可以使用【cls】命令清理一下调试器显示的过多信息,自己来决定,我是会清理的。
            进入调试状态后,我们先找到我们需要的 Person 对象,命令就是【!dumpheap -type Person】
1 0:006> !dumpheap -type Person2          Address               MT         Size3   026828409f60   7ffb8a239c50             32 4 5 Statistics:6         MT Count TotalSize Class Name7 7ffb8a239c50   1      32 Example_3_1_5_1.Person8 Total 1 objects, 32 bytes            我们知道了对象的地址,可以执行【!do】命令,查看 Person 对象的详情。红色标注的就是方法表,我们可以使用【dt】命令查看结构。
1 0:006> !do 026828409f60 2 Name:      Example_3_1_5_1.Person 3 MethodTable: 00007ffb8a239c50 4 EEClass:   00007ffb8a222578 5 Tracked Type: false 6 Size:      32(0x20) bytes 7 File:      E:\Visual Studio 2022\Source\Projects\.....\Example_3_1_5_1\bin\Debug\net7.0\Example_3_1_5_1.dll 8 Fields: 9               MT    Field   Offset               Type VT   Attr            Value Name10 00007ffb8a12fd104000004      8      System.String0 instance 0000026828409f10 k__BackingField11 00007ffb8a0ae8d04000005       10         System.Int321 instance               20 k__BackingField            执行命令 【dt coreclr!MethodTable 00007ffb8a239c50】,00007ffb8a239c50就是方法表的地址。
https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gifhttps://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif 1 0:006> dt coreclr!MethodTable 00007ffb8a239c50 2    =00007ffb`e9f688a8 s_pMethodDataCache : 0x00000268`2424c440 MethodDataCache 3    =00007ffb`e9f688b0 s_fUseParentMethodData : 0n1 4    =00007ffb`e9f688a0 s_fUseMethodDataCache : 0n1 5    +0x000 m_dwFlags      : 0x1000200 6    +0x004 m_BaseSize       : 0x20 7    +0x008 m_wFlags2      : 0x4088 8    +0x00a m_wToken         : 7 9    +0x00c m_wNumVirtuals   : 410    +0x00e m_wNumInterfaces : 011    +0x010 m_pParentMethodTable : 0x00007ffb`89f893b8 MethodTable12    +0x018 m_pLoaderModule: 0x00007ffb`8a20cf48 Module13    +0x020 m_pWriteableData : 0x00007ffb`8a239cb8 MethodTableWriteableData14    +0x028 m_pEEClass       : 0x00007ffb`8a222578 EEClass15    +0x028 m_pCanonMT       : 0x00007ffb`8a22257816    +0x030 m_pPerInstInfo   : 0x00007ffb`8a24a2d0-> 0x8b4c0000`0ffa25ff Dictionary17    +0x030 m_ElementTypeHnd : 0x00007ffb`8a24a2d018    +0x030 m_pMultipurposeSlot1 : 0x00007ffb`8a24a2d019    +0x038 m_pInterfaceMap: (null) 20    +0x038 m_pMultipurposeSlot2 : 021    =00007ffb`e9ea9fb8 c_DispatchMapSlotOffsets : "080@"22    =00007ffb`e9ea9fb0 c_NonVirtualSlotsOffsets : "080@8@@H080@"23    =00007ffb`e9ea9fa0 c_ModuleOverrideOffsets : "080@8@@H8@@H@HHP080@8@@H080@"24    =00007ffb`e9ebb648 c_OptionalMembersStartOffsets : "@@@@@@@H@@@H@HHP@@@H@HHP@HHPHPPX"View Code            以上就是 MethodTable 在 CLR 级别的结构。
        2.7、查看方法描述符 MethodDesc。
            代码样例:Example_3_1_5_1
            我们使用 Windbg Preview 调试器,通过【launch executable】菜单加载【Example_3_1_5.exe】项目,通过【g】命令,运行程序,调试器运行代【Console.ReadLine()】次会暂停执行,然后我们点击【break】按钮,进入调试状态。当然,我们可以使用【cls】命令清理一下调试器显示的过多信息,自己来决定,我是会清理的。
            进入调试状态后,我们先找到我们需要的 Person 对象,命令就是【!dumpheap -type Person】
            1 0:006> !dumpheap -type Person2          Address               MT         Size3   026828409f60   7ffb8a239c50             32 4 5 Statistics:6         MT Count TotalSize Class Name7 7ffb8a239c50   1      32 Example_3_1_5_1.Person8 Total 1 objects, 32 bytes             我们得到了红色标记的 Person 对象的地址,然后执行【!do】命令查看 Person 对象的详情。
1 0:006> !do 026828409f60 2 Name:      Example_3_1_5_1.Person 3 MethodTable: 00007ffb8a239c50 4 EEClass:   00007ffb8a222578 5 Tracked Type: false 6 Size:      32(0x20) bytes 7 File:      E:\Visual Studio 2022\Source\Projects\......\Example_3_1_5_1\bin\Debug\net7.0\Example_3_1_5_1.dll 8 Fields: 9               MT    Field   Offset               Type VT   Attr            Value Name10 00007ffb8a12fd104000004      8      System.String0 instance 0000026828409f10 k__BackingField11 00007ffb8a0ae8d04000005       10         System.Int321 instance               20 k__BackingField              执行以上命令,我们得到了 Person 对象的方法表,然后我们使用【!dumpmt】查看方法表详情。
1 0:006> !dumpmt -md 00007ffb8a239c50 2 EEClass:             00007ffb8a222578 3 Module:            00007ffb8a20cf48 4 Name:                Example_3_1_5_1.Person 5 mdToken:             0000000002000007 6 File:                E:\Visual Studio 2022\Source\Projects\.....\Example_3_1_5_1\bin\Debug\net7.0\Example_3_1_5_1.dll 7 AssemblyLoadContext: Default ALC - The managed instance of this context doesn't exist yet. 8 BaseSize:            0x20 9 ComponentSize:       0x010 DynamicStatics:      false11 ContainsPointers:    true12 Slots in VTable:   913 Number of IFaces in IFaceMap: 014 --------------------------------------15 MethodDesc Table16            Entry       MethodDesc    JIT Name17 00007FFB8A0B0048 00007ffb89f89348   NONE System.Object.Finalize()18 00007FFB8A0B0060 00007ffb89f89358   NONE System.Object.ToString()19 00007FFB8A0B0078 00007ffb89f89368   NONE System.Object.Equals(System.Object)20 00007FFB8A0B00C0 00007ffb89f893a8   NONE System.Object.GetHashCode()21 00007FFB8A24A2D0 00007ffb8a239c28    JIT Example_3_1_5_1.Person..ctor()22 00007FFB8A24A270 00007ffb8a239bc8   NONE Example_3_1_5_1.Person.get_Name()23 00007FFB8A24A288 00007ffb8a239be0    JIT Example_3_1_5_1.Person.set_Name(System.String)24 00007FFB8A24A2A0 00007ffb8a239bf8   NONE Example_3_1_5_1.Person.get_Age()25 00007FFB8A24A2B8 00007ffb8a239c10    JIT Example_3_1_5_1.Person.set_Age(Int32)            执行命令后,红色标记的就是方法描述符,我们可以点击去查看 MethodDesc 详情。我们执行【!dumpmd】命令,查看 MethodDesc。
1 0:006> !DumpMD /d 00007ffb8a239be0 2 Method Name:          Example_3_1_5_1.Person.set_Name(System.String) 3 Class:                00007ffb8a222578 4 MethodTable:          00007ffb8a239c50 5 mdToken:            0000000006000009 6 Module:               00007ffb8a20cf48 7 IsJitted:             yes 8 Current CodeAddr:   00007ffb8a1407c0 9 Version History:10   ILCodeVersion:      000000000000000011   ReJIT ID:         012   IL Addr:            00000268240a20ef13    CodeAddr:         00007ffb8a1407c0(MinOptJitted)14      NativeCodeVersion:0000000000000000              由于代码已经编译了,所以是有地址的,我们可以执行【!u】命令查看set_Name()方法的汇编代码。
1 E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\Example_3_1_5_1\Program.cs @ 18: 2 >>> 00007ffb`8a1407c0 55            push    rbp 3 00007ffb`8a1407c1 57            push    rdi 4 00007ffb`8a1407c2 4883ec28      sub   rsp,28h 5 00007ffb`8a1407c6 488d6c2430      lea   rbp, 6 00007ffb`8a1407cb 48894d10      mov   qword ptr ,rcx 7 00007ffb`8a1407cf 48895518      mov   qword ptr ,rdx 8 00007ffb`8a1407d3 833d16ca0c0000cmp   dword ptr ,0 9 00007ffb`8a1407da 7405            je      00007ffb`8a1407e110 00007ffb`8a1407dc e8ef6bc15f      call    coreclr!JIT_DbgIsJustMyCode (00007ffb`e9d573d0)11 00007ffb`8a1407e1 488b5510      mov   rdx,qword ptr 12 00007ffb`8a1407e5 488d4a08      lea   rcx,13 00007ffb`8a1407e9 488b5518      mov   rdx,qword ptr 14 00007ffb`8a1407ed e81ef8e2ff      call    00007ffb`89f70010 (JitHelp: CORINFO_HELP_ASSIGN_REF)15 00007ffb`8a1407f2 90            nop16 00007ffb`8a1407f3 4883c428      add   rsp,28h17 00007ffb`8a1407f7 5f            pop   rdi18 00007ffb`8a1407f8 5d            pop   rbp19 00007ffb`8a1407f9 c3            ret
        2.8、我们可以通过【!token2ee】命令根据指定的 token 查找 MethodDesc。
            代码样例:Example_3_1_5_1
            我们使用 Windbg Preview 调试器,通过【launch executable】菜单加载【Example_3_1_5_1.exe】项目,通过【g】命令,运行程序,调试器运行代【Console.ReadLine()】次会暂停执行,然后我们点击【break】按钮,进入调试状态。当然,我们可以使用【cls】命令清理一下调试器显示的过多信息,自己来决定,我是会清理的。
            1 0:006> !token2ee Example_3_1_5 060000012 Module:      00e640443 Assembly:    Example_3_1_5.exe4 Token:       060000015 MethodDesc:00e64d586 Name:      Example_3_1_5.Program.Main(System.String[])7 JITTED Code Address: 02990848
        2.9、查看 EECLass 的结构。
            代码样例:Example_3_1_5
            我们使用 Windbg Preview 调试器,通过【launch executable】菜单加载【Example_3_1_5.exe】项目,通过【g】命令,运行程序,调试器运行代【Console.ReadLine()】次会暂停执行,然后我们点击【break】按钮,进入调试状态。当然,我们可以使用【cls】命令清理一下调试器显示的过多信息,自己来决定,我是会清理的。
https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gifhttps://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif 1 0:006> !dumpheap -type Person 2Address       MT   Size 3 029d2508 00e64e1c       16      45 Statistics: 6       MT    Count    TotalSize Class Name 7 00e64e1c      1         16 Example_3_1_5.Person 8 Total 1 objects 9 10 11 0:006> !do 029d250812 Name:      Example_3_1_5.Person13 MethodTable: 00e64e1c14 EEClass:   00e6131815 Size:      16(0x10) bytes16 File:      E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\Example_3_1_5\bin\Debug\Example_3_1_5.exe17 Fields:18       MT    Field   Offset               Type VT   Attr    Value Name19 6fa424e44000001      4      System.String0 instance 029d24c8 k__BackingField20 6fa442a84000002      8         System.Int321 instance       20 k__BackingField21 22 23 0:006> !DumpClass /d 00e6131824 Class Name:      Example_3_1_5.Person25 mdToken:         0200000326 File:            E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\Example_3_1_5\bin\Debug\Example_3_1_5.exe27 Parent Class:    6fa315c828 Module:          00e6404429 Method Table:    00e64e1c30 Vtable Slots:    431 Total Method Slots:532 Class Attributes:    10000133 Transparency:      Critical34 NumInstanceFields:   235 NumStaticFields:   036       MT    Field   Offset               Type VT   Attr    Value Name37 6fa424e44000001      4      System.String0 instance         k__BackingField38 6fa442a84000002      8         System.Int321 instance         k__BackingFieldView Code            
四、总结
    终于完成了,这篇文章写了好几天,看底层的东西,需要耐性和坚持。写完了,感觉还是收获不小的,对 Net 底层的细节了解更多了。学习是艰苦的过程,还挺费时费力的。就写到这里了,不忘初心,继续努力。
来源:https://www.cnblogs.com/PatrickLiu/archive/2023/10/30/17788818.html
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!
页: [1]
查看完整版本: Net 高级调试之三:类型元数据介绍(同步块表、类型句柄、方法描述符等)