翼度科技»论坛 编程开发 .net 查看内容

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

8

主题

8

帖子

24

积分

新手上路

Rank: 1

积分
24
一、简介
    今天是《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、值类型布局
        一般而言,方法的参数、在方法内部声明的局部变量都是存放在当前的线程栈上,也就是说在线程栈上直接存储值类型的值。

        


    3、引用类型布局
        class 类型是一种引用类型,实例对象在托管堆中分配空间,并将对象的首地址存在栈地址上。
        
 

    4、同步块表
        这个名称叫的不太准确,叫 ObjectHeader 更好点,因为源码中就是叫这个名称。托管堆上的每个对象的前面都有一个同步块索引,它指向 CLR 中私有堆上的同步块表,同步块表中可以包含很多信息,比如:对象散列码、锁信息、应用程序域索引。
        

        
    5、类型句柄(方法表)
        类型句柄是针对类型的描述信息,比如:这个类中有多少个方法,方法的结构,方法的字段信息等。
        

    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
  1. 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
  1. 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            
  1. 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            
  1. 1 namespace Example_3_1_4 2 { 3     internal class Program 4     { 5         public static Person person=new Person(); 6  7         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     
  1. 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版本的)
  1. 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. 1 0:000> !clrstack -l 2 OS Thread Id: 0x317c (0) 3 Child SP       IP Call Site 4 00aff1c4 778e10fc [InlinedCallFrame: 00aff1c4]  5 00aff1c0 6fee9b71 ...(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte*, Int32, Int32 ByRef, IntPtr) 6  7 ...... 8  9 [b]00aff2c0 00d3089e Example_3_1_1.Program.Main(System.String[]) [E:\...\Example_3_1_1\Program.cs @ 12]10     LOCALS:【表示局部变量】11         0x00aff2d0 = 0x0000000a【[b]0x00aff2d0[/b]是栈地址,[b]0x0000000a [/b]是栈上的值,这是十六进制的】12         0x00aff2c8 = 0x0000000b[b]【[b]0x00aff2c8[/b]是栈地址,[b][b]0x0000000b[/b] [/b]是栈上的值,这是十六进制的】[/b]13         0x00aff2c4 = 0x0000000c[b]【[b]0x00aff2d0[/b]是栈地址,[b][b]0x0000000c[/b] [/b]是栈上的值,这是十六进制的】[/b]14 15 00aff448 70f1f036 [GCFrame: 00aff448] [/b]
复制代码
            以上显示的红色部分是最重要的,LOCALS 表示局部变量,11,12,13 三行是具体的局部变量,等号前面是 线程栈上的变量地址,后面是具体的值,我们可以使用【?】命令查看具体的值。
  1. 1 0:000> ? 0x0000000a2 Evaluate expression: 10 = 0000000a3 0:000> ? 0x0000000b4 Evaluate expression: 11 = 0000000b5 0:000> ? 0x0000000c6 Evaluate expression: 12 = 0000000c
复制代码
View Code            对应 C# 代码中的赋值操作。
            

            由于栈的特点,先进后出,后进先出,所以说【a】是最先入栈的,在栈底,依次是【b】,最上面的是【c】,所以我们从【c】的地址打印,可以显示【c、b、a】的值。由此,我们执行【dp】命令,效果如下。
  1. 1 0:000> dp 0x00aff2c4 l42 00aff2c4  0000000c 0000000b 00000000 0000000a
复制代码
            我们可以继续验证,由于栈的地址是由高到低的分配,所以,【c】的地址加上 0x4,为什么加4呢,虽然【c】占用2个字节,但是会按4个字节算的,就是【b】变量的值,如下:
  1. 1 0:000> dp 00aff2c4+0x4 l12 00aff2c8  0000000b
复制代码
            继续验证,【b】的地址加上 0x8,就是【a】变量的值,为什么是加8呢,因为【b】占用8个字节,如下:
  1. 1 0:000> dp 00aff2c8+0x8 l12 00aff2d0  0000000a
复制代码
            当然,我们可以以【c】变量的地址为基准,算出【b】和【a】的值,如下:
  1. 1 0:000> dp 0x00aff2c4+0x4 l1(以c 的地址为基准,找到b的地址,加4)2 00aff2c8  0000000b3 0:000> dp 0x00aff2c4+0xc l1(以c 的地址为基准,找到a的地址,加12,十六进制就是0xc)4 00aff2d0  0000000a
复制代码
        2.2、引用类型的布局
            代码样例:Example_3_1_2
            我们使用 Windbg Preview 调试器,通过【launch executable】菜单加载【Example_3_1_2.exe】项目,通过【g】命令,运行程序,调试器运行代【Console.ReadLine()】次会暂停执行,然后我们点击【break】按钮,进入调试状态。我们还需要通过【~0s】命令,切换到主线程,当然,我们可以使用【cls】命令清理一下调试器显示的过多信息,自己来决定,我是会清理的。
            我们先使用【!clrstack -a】命令,查看线程栈的局部变量。
  1. 1 0:000> !clrstack -a 2 OS Thread Id: 0x3930 (0) 3 Child SP       IP Call Site 4 0133ee8c 778e10fc [InlinedCallFrame: 0133ee8c]  5 0133ee88 6fee9b71 6 ...... 7 0133ef88 018c08b1 Example_3_1_2.Program.Main(System.String[]) [E:\Visual Studio 2022\Source\Projects\......\Example_3_1_2\Program.cs @ 14] 8     PARAMETERS: 9         args (0x0133ef94) = 0x033b24bc10 [b]    LOCALS:[/b]11         [b]0x0133ef90[/b] = [b]0x033b24e0([b]0x0133ef90 [/b]是栈地址,[b]0x033b24e0[/b] person变量的引用地址)[/b]12 13 0133f108 70f1f036 [GCFrame: 0133f108]
复制代码
            我们可以通过【dp】命令查看栈地址,值是 033b24e0,这个值就是 person变量引用的地址。
  1. 1 0:000> dp 0x0133ef90 l12 0133ef90  033b24e0(这个地址就是 person变量的地址)
复制代码
            我们可以使用【!do|!DumpObj】命令,查看对象的详情。
  1. 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 6fa424e4  4000001        4        System.String  0 instance [b]033b24c8 k__BackingField[/b]10 6fa442a8  4000002        8         System.Int32  1 instance       20 k__BackingField
复制代码
            033b24c8 k__BackingField,这个是 string 类型的字段,033b24c8又是一个引用地址,我们继续【!do】,查看详情。
  1. 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:      [b]jack(这个就是我们赋值的)[/b] 8 Fields: 9       MT    Field   Offset                 Type VT     Attr    Value Name10 6fa442a8  4000283        4         System.Int32  1 instance        4 m_stringLength11 6fa42c9c  4000284        8          System.Char  1 instance       6a m_firstChar12 6fa424e4  4000288       70        System.String  0   shared   static Empty13     >> Domain:Value  0151ca70: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 6fa442a8  4000283        4         System.Int32  1 instance        4 m_stringLength11 6fa42c9c  4000284        8          System.Char  1 instance       6a m_firstChar12 6fa424e4  4000288       70        System.String  0   shared   static Empty13     >> Domain:Value  0151ca70: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. 1 0:000> !clrstack -l 2 OS Thread Id: 0x2600 (0) 3 Child SP       IP Call Site 4 00dcef18 7696f262 [HelperMethodFrame: 00dcef18] System.Diagnostics.Debugger.BreakInternal() 5 00dcef94 705bf195 System.Diagnostics.Debugger.Break() [f:\dd\ndp\clr\src\BCL\system\diagnostics\debugger.cs @ 91] 6  7 00dcefbc 02f40905 Example_3_1_3.Program.Main(System.String[]) [E:\Visual Studio 2022\Source\Projects\......\Example_3_1_3\Program.cs @ 13] 8     LOCALS: 9         0x00dcefd0 = [b]0x030b2510[/b]10         0x00dcefcc = 0x030b39ac11         0x00dcefd8 = 0x02bf809812 13 00dcf154 70f1f036 [GCFrame: 00dcf154]
复制代码
            0x00dcefd0 = 0x030b2510,这个地址就是我们声明的 person 变量。既然由了对象的地址,只要用对象的地址,减去 0x4,就是同步块的地址,然后使用【dp】命令就可以查看了。   
  1. 1 0:000> dp 0x030b2510-0x4 l42 030b250c  [b]0ebf8098[/b] 01414e1c 030b24c8 00000014
复制代码
            第二行的第二列以前是0,表示没有任何数据,现在有值了。现在我们用这个值,减去我们得到的散列码,看看是什么。
  1. 1 0:000> ? 0ebf8098-2bf80982 Evaluate expression: 201326592 = [b]0c000000[/b]
复制代码
            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. 1 0:001> !dumpheap -type Person2  Address       MT     Size3 [b]033824c8[/b] 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. 1 0:001> dp 033824c8-0x4 l42 033824c4  [b]08000007[/b] 014d4e60 00000000 00000000
复制代码
            08000007 就是同步块索引的值,08是一个掩码,表示是同步块索引,07就是线程 Id。我们可以使用【!syncblk】命令来验证。
  1. 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     [b]7[/b] 01567124            3         1 0157c340 f8     [b]9[/b]   033824c8 Example_3_1_4.Person(被锁的对象是 person)
复制代码
              3:(一个线程持有锁,一个等待锁)
  1. 5 ----------------------------- 6 Total           7 7 CCW             1 8 RCW             2 9 ComClassFactory 010 Free            0
复制代码
            这里是9,为什么我们的程序输出是3,3是托管线程的编号。9是windbg 标识的号码。
  1. 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 Context  Domain   Count Apt Exception10    0    1 3d4c 01512ba8     2a020 Preemptive  03388254:00000000 0150ca30 1     MTA 11    5    2 324c 0154f738     2b220 Preemptive  00000000:00000000 0150ca30 0     MTA (Finalizer) 12   [b] 9    3   f8 0157c340   3029220 Preemptive  03387214:00000000 0150ca30 1     MTA (Threadpool Worker) [/b]13   11    4  264 0157cd28   3029220 Preemptive  0338A21C:00000000 0150ca30 0     MTA (Threadpool Worker)
复制代码
             我们可以切换到9好线程,看看他的线程栈。
  1. 1 0:001> ~~[f8]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=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206 5 ntdll!NtWaitForMultipleObjects+0xc: 6 778e166c c21400          ret     14h 7  8  9 0:009> !clrstack10 OS Thread Id: 0xf8 (9)11 Child SP       IP Call Site12 05e2f444 778e166c [GCFrame: 05e2f444] 13 05e2f524 778e166c [HelperMethodFrame_1OBJ: 05e2f524] System.Threading.Monitor.Enter(System.Object)14 05e2f59c 7076377b System.IO.TextReader+SyncTextReader.ReadLine() [f:\dd\ndp\clr\src\BCL\system\io\textreader.cs @ 363]15 05e2f5ac 705c1845 System.Console.ReadLine() [f:\dd\ndp\clr\src\BCL\system\console.cs @ 1984]16 05e2f5b4 016f0ae8 Example_3_1_4.Program+c.b__1_0() [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\Example_3_1_4\Program.cs @ 17]17 05e2f600 6fe8d4bb System.Threading.Tasks.Task.InnerInvoke() [f:\dd\ndp\clr\src\BCL\system\threading\Tasks\Task.cs @ 2884]18 05e2f60c 6fe8b731 System.Threading.Tasks.Task.Execute() [f:\dd\ndp\clr\src\BCL\system\threading\Tasks\Task.cs @ 2498]19 05e2f630 6fe8b6fc System.Threading.Tasks.Task.ExecutionContextCallback(System.Object) [f:\dd\ndp\clr\src\BCL\system\threading\Tasks\Task.cs @ 2861]20 05e2f634 6fe28604 System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean) [f:\dd\ndp\clr\src\BCL\system\threading\executioncontext.cs @ 980]21 05e2f6a0 6fe28537 System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean) [f:\dd\ndp\clr\src\BCL\system\threading\executioncontext.cs @ 928]22 05e2f6b4 6fe8b4b2 System.Threading.Tasks.Task.ExecuteWithThreadLocal(System.Threading.Tasks.Task ByRef) [f:\dd\ndp\clr\src\BCL\system\threading\Tasks\Task.cs @ 2827]23 05e2f718 6fe8b357 System.Threading.Tasks.Task.ExecuteEntry(Boolean) [f:\dd\ndp\clr\src\BCL\system\threading\Tasks\Task.cs @ 2767]24 05e2f728 6fe8b29d System.Threading.Tasks.Task.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem() [f:\dd\ndp\clr\src\BCL\system\threading\Tasks\Task.cs @ 2704]25 05e2f72c 6fdfeb7d System.Threading.ThreadPoolWorkQueue.Dispatch() [f:\dd\ndp\clr\src\BCL\system\threading\threadpool.cs @ 820]26 05e2f77c 6fdfe9db System.Threading._ThreadPoolWaitCallback.PerformWaitCallback() [f:\dd\ndp\clr\src\BCL\system\threading\threadpool.cs @ 1161]27 05e2f99c 70f1f036 [DebuggerU2MCatchHandlerFrame: 05e2f99c]
复制代码
        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. 1 0:000> !dumpheap -type Person2  Address       MT     Size3 [b]02d72508[/b] 01004e1c       16     4 5 Statistics:6       MT    Count    TotalSize Class Name7 01004e1c        1           16 Example_3_1_5.Person8 Total 1 objects
复制代码
            我们由了 Person 对象的指针地址,就可以通过这个地址查看它的方法表的信息了。
  1. 1 0:000> !DumpObj /d 02d72508 2 Name:        Example_3_1_5.Person 3 [b]MethodTable: 01004e1c[/b] 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 6fa424e4  4000001        4        System.String  0 instance 02d724c8 k__BackingField10 6fa442a8  4000002        8         System.Int32  1 instance       20 k__BackingField
复制代码
            当然,我们通过【dp】命令也能证明类型句柄的信息。标红的 01004e1c 就是方法表的地址。
  1. 1 0:000> dp 02d72508 l42 02d72508  [b]01004e1c[/b] 02d724c8 00000014 00000000
复制代码
            我们可以【!dumpmt -md】列出所有的方法描述信息。
  1. 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. 1 0:006> !dumpheap -type Person2          Address               MT           Size3     [b]026828409f60[/b]     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. 1 0:006> !do 026828409f60 2 Name:        Example_3_1_5_1.Person 3 [b]MethodTable: 00007ffb8a239c50[/b] 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 00007ffb8a12fd10  4000004        8        System.String  0 instance 0000026828409f10 k__BackingField11 00007ffb8a0ae8d0  4000005       10         System.Int32  1 instance               20 k__BackingField
复制代码
            执行命令 【dt coreclr!MethodTable 00007ffb8a239c50】,00007ffb8a239c50就是方法表的地址。
  1. 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 : [0]  "080@"22    =00007ffb`e9ea9fb0 c_NonVirtualSlotsOffsets : [0]  "080@8@@H080@"23    =00007ffb`e9ea9fa0 c_ModuleOverrideOffsets : [0]  "080@8@@H8@@H@HHP080@8@@H080@"24    =00007ffb`e9ebb648 c_OptionalMembersStartOffsets : [0]  "@@@@@@@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. 1 0:006> !dumpheap -type Person2          Address               MT           Size3     [b]026828409f60[/b]     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. 1 0:006> !do 026828409f60 2 Name:        Example_3_1_5_1.Person 3 [b]MethodTable: 00007ffb8a239c50[/b] 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 00007ffb8a12fd10  4000004        8        System.String  0 instance 0000026828409f10 k__BackingField11 00007ffb8a0ae8d0  4000005       10         System.Int32  1 instance               20 k__BackingField
复制代码
              执行以上命令,我们得到了 Person 对象的方法表,然后我们使用【!dumpmt】查看方法表详情。
  1. 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 [b]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)[/b]
复制代码
            执行命令后,红色标记的就是方法描述符,我们可以点击去查看 MethodDesc 详情。我们执行【!dumpmd】命令,查看 MethodDesc。
  1. 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 [b]     CodeAddr:           00007ffb8a1407c0[/b]  (MinOptJitted)14      NativeCodeVersion:  0000000000000000
复制代码
              由于代码已经编译了,所以是有地址的,我们可以执行【!u】命令查看set_Name()方法的汇编代码。
  1. 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,[rsp+30h] 6 00007ffb`8a1407cb 48894d10        mov     qword ptr [rbp+10h],rcx 7 00007ffb`8a1407cf 48895518        mov     qword ptr [rbp+18h],rdx 8 00007ffb`8a1407d3 833d16ca0c0000  cmp     dword ptr [00007ffb`8a20d1f0],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 [rbp+10h]12 00007ffb`8a1407e5 488d4a08        lea     rcx,[rdx+8]13 00007ffb`8a1407e9 488b5518        mov     rdx,qword ptr [rbp+18h]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. 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】命令清理一下调试器显示的过多信息,自己来决定,我是会清理的。
  1. 1 0:006> !dumpheap -type Person 2  Address       MT     Size 3 029d2508 00e64e1c       16      4  5 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 6fa424e4  4000001        4        System.String  0 instance 029d24c8 k__BackingField20 6fa442a8  4000002        8         System.Int32  1 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:    100001  33 Transparency:        Critical34 NumInstanceFields:   235 NumStaticFields:     036       MT    Field   Offset                 Type VT     Attr    Value Name37 6fa424e4  4000001        4        System.String  0 instance           k__BackingField38 6fa442a8  4000002        8         System.Int32  1 instance           k__BackingField
复制代码
View Code            
四、总结
    终于完成了,这篇文章写了好几天,看底层的东西,需要耐性和坚持。写完了,感觉还是收获不小的,对 Net 底层的细节了解更多了。学习是艰苦的过程,还挺费时费力的。就写到这里了,不忘初心,继续努力。
来源:https://www.cnblogs.com/PatrickLiu/archive/2023/10/30/17788818.html
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

举报 回复 使用道具