Net 高级调试之十:轻量级代码生成的调试

这篇具有很好参考价值的文章主要介绍了Net 高级调试之十:轻量级代码生成的调试。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

一、简介
    今天是《Net 高级调试》的第十篇文章。说起来,高级调试,调试的内容还是挺多的,技巧也不少,但是,要想做一个合格的高级调试人员,还需要掌握如何调试动态生成的IL代码。今天要探讨的高级调试的技巧是如何调试通过 Emit 动态生成 IL 代码。可能有人会问,我们不是编写 C# 代码,或者是 VB.Net 代码吗?怎么还要动态生成 IL 代码,这些工作不是编译器做的吗?当然,一般情况是这样的,但是,当我们编写一些高性能的框架的时候,使用 IL 代码编写也是常事。既然也可以直接使用 IL 编写代码,那对它的调试也是少不了的,调试机会虽然很少,具有这个本领,等遇到这样的问题,就不至于慌乱了,俗话说的好:艺多不压身。当然了,第一次看视频或者看书,是很迷糊的,不知道如何操作,还是那句老话,一遍不行,那就再来一遍,还不行,那就再来一遍,俗话说的好,书读千遍,其意自现。
     如果在没有说明的情况下,所有代码的测试环境都是 Net Framewok 4.8,但是,有时候为了查看源码,可能需要使用 Net Core 的项目,我会在项目章节里进行说明。好了,废话不多说,开始我们今天的调试工作。

       调试环境我需要进行说明,以防大家不清楚,具体情况我已经罗列出来。
          操作系统:Windows Professional 10
          调试工具:Windbg Preview(可以去Microsoft Store 去下载)
          开发工具:Visual Studio 2022
          Net 版本:Net Framework 4.8
          CoreCLR源码:源码下载

二、基础知识

    1、动态代码调试
        动态代码调试的机会虽然不多,但是掌握动态代码调试的技巧还是很有必要的,俗话说的好,艺多不压身。当我们遇到有这样写代码,由此引发的问题的时候,我们也可以做到遇事不惊。
        今天我们就讨论三种调试的技巧。

    2、三种调试策略

        2.1、捕获 JIT 的 CompileMethod 方法。
            C# 代码的编译过程分为两个阶段,第一个阶段就是编译器的阶段,这个阶段编译器将我们编写的C#源代码编译成 IL 代码,第二阶段,就是在运行的时候,JIT 将 IL 代码编译为机器代码,我们的程序就运行了。现在是动态生成的 IL 代码,没有第一个阶段,直接就是第二个 JIT 编译的阶段,我们可以尝试在 JIT 的某个方法中设置断点进行拦截,拿到方法的描述符,也就是 MD,有了 MD,我们就可以使用 bp md 命令,为这个方法下断点,这样就可以对动态生成的代码进行调试了。

        2.2、从代码中获取委托的函数指针(我的测试没有实现)。
            这种方法比较简单,但需要在方法中加一行代码,具有一定的破坏性。代码:Marshal.GetFunctionPointerForDelegate(委托实例).ToInt64()。

        2.3、在动态的代码中注入 Debugger.Break()。
            我们在很多的调试中都会用到 Debugger.Break() 函数,让程序中断到调试器,如果这个动态代码,我们具有完整的控制权,我们就可以通过注入这个方法:Debugger.Break(),实现 int 3 中断,进而获取想要的数据。代码如下:IL.Emit(OpCodes.Call,typeof(Debugger).GetMethod("Break"))。

    3、程序集
        3.1、程序集崩溃
            程序集泄露是指什么呢?就是程序的内存暴涨,产生大量的程序集。

三、调试过程
    废话不多说,这一节是具体的调试操作的过程,又可以说是眼见为实的过程,在开始之前,我还是要啰嗦两句,这一节分为两个部分,第一部分是测试的源码部分,没有代码,当然就谈不上测试了,调试必须有载体。第二部分就是根据具体的代码来证实我们学到的知识,是具体的眼见为实。

    1、测试源码
        以下项目代码就是在眼见为实用到的测试用例。
        1.1、Example_10_1_1
Net 高级调试之十:轻量级代码生成的调试Net 高级调试之十:轻量级代码生成的调试
 1 namespace Example_10_1_1
 2 {
 3     internal class Program
 4     {
 5         private delegate int AddDelegate(int a, int b);
 6         static void Main(string[] args)
 7         {
 8             var dynamicAdd = new DynamicMethod("Add", typeof(int), new Type[] { typeof(int), typeof(int) });
 9             var il = dynamicAdd.GetILGenerator();
10             il.Emit(OpCodes.Ldarg_0);
11             il.Emit(OpCodes.Ldarg_1);
12             il.Emit(OpCodes.Add);
13             il.Emit(OpCodes.Ret);
14 
15             var addDelegate = (AddDelegate)dynamicAdd.CreateDelegate(typeof(AddDelegate));
16             Console.WriteLine(addDelegate(10, 20));
17         }
18     }
19 }
View Code

        1.2、Example_10_1_2
Net 高级调试之十:轻量级代码生成的调试Net 高级调试之十:轻量级代码生成的调试
 1 namespace Example_10_1_2
 2 {
 3     internal class Program
 4     {
 5         private delegate int AddDelegate(int a, int b);
 6         static void Main(string[] args)
 7         {
 8             var dynamicAdd = new DynamicMethod("Add", typeof(int), new Type[] { typeof(int), typeof(int) });
 9             var il = dynamicAdd.GetILGenerator();
10             il.Emit(OpCodes.Ldarg_0);
11             il.Emit(OpCodes.Ldarg_1);
12             il.Emit(OpCodes.Add);
13             il.Emit(OpCodes.Ret);
14 
15             var addDelegate = (AddDelegate)dynamicAdd.CreateDelegate(typeof(AddDelegate));
16 
17             Console.WriteLine("Function Pointer:0x{0:x16}", Marshal.GetFunctionPointerForDelegate(addDelegate).ToInt64());
18 
19             Debugger.Break();
20 
21             Console.WriteLine(addDelegate(10, 20));
22         }
23     }
24 }
View Code

        1.3、Example_10_1_3
Net 高级调试之十:轻量级代码生成的调试Net 高级调试之十:轻量级代码生成的调试
 1 namespace Example_10_1_3
 2 {
 3     internal class Program
 4     {
 5         private delegate int AddDelegate(int a, int b);
 6         static void Main(string[] args)
 7         {
 8             var dynamicAdd = new DynamicMethod("Add", typeof(int), new Type[] { typeof(int), typeof(int) });
 9             var il = dynamicAdd.GetILGenerator();
10 
11             il.Emit(OpCodes.Call,typeof(Debugger).GetMethod("Break"));
12             il.Emit(OpCodes.Ldarg_0);
13             il.Emit(OpCodes.Ldarg_1);
14             il.Emit(OpCodes.Add);
15             il.Emit(OpCodes.Ret);
16 
17             var addDelegate = (AddDelegate)dynamicAdd.CreateDelegate(typeof(AddDelegate));
18 
19             Console.WriteLine(addDelegate(10, 20));
20         }
21     }
22 }
View Code

        1.4、Example_10_1_4
Net 高级调试之十:轻量级代码生成的调试Net 高级调试之十:轻量级代码生成的调试
 1 namespace Example_10_1_4
 2 {
 3     internal class Program
 4     {
 5         //XmlSerializer序列化程序集泄露解决办法。
 6         //static readonly XmlSerializer xmlSerializer = new XmlSerializer(typeof(Customer), new XmlRootAttribute("FabrikamCustomer"));
 7         static void Main(string[] args)
 8         {
 9             var xml = @"<FabrikamCustomer><Id>001</Id><FirstName>John</FirstName><LastName>Dow</LastName></FabrikamCustomer>";
10 
11             Enumerable.Range(0, 30000)
12                 .Select(i => GetCustomer(i, "FabrikamCustomer", xml))
13                 .ToList();
14 
15             Console.WriteLine("处理完成!");
16             Console.ReadLine();
17         }
18 
19         public static Customer GetCustomer(int i, string rootElementName, string xml)
20         {
21             var xmlSerializer = new XmlSerializer(typeof(Customer), new XmlRootAttribute("FabrikamCustomer"));
22             using (var textReader = new StringReader(xml))
23             {
24                 using (var xmlReader = XmlReader.Create(textReader))
25                 {
26                     Console.WriteLine(i);
27 
28                     return (Customer)xmlSerializer.Deserialize(xmlReader);
29                 }
30             }
31         }
32     }
33 
34     public class Customer
35     {
36         public string Id { get; set; }
37         public string FirstName { get; set; }
38         public string LastName { get; set; }
39     }
40 }
View Code

    2、眼见为实
        项目的所有操作都是一样的,所以就在这里说明一下,但是每个测试例子,都需要重新启动,并加载相应的应用程序,加载方法都是一样的。流程如下:我们编译项目,打开 Windbg,点击【文件】----》【launch executable】附加程序,打开调试器的界面,程序已经处于中断状态。

        2.1、捕获 JIT 的 CompileMethod 方法,为 Add 方法设置断点,进行调试。
            测试源码:Example_10_1_1

            Net 高级调试之十:轻量级代码生成的调试

            上图是我们的代码,我们使用【!mbp】命令在18行下断点,【mbp】命令是 SOSEX的扩展命令,执行前必须加载 SOSEX.dll,我们需要使用【g】命令,继续运行程序,然后到达指定断点处停止后。

1 0:000> !mbp Program.cs 18
2 The CLR has not yet been initialized in the process.
3 Breakpoint resolution will be attempted when the CLR is initialized.

            Net 高级调试之十:轻量级代码生成的调试

            我们已经在我们设置的断点处暂停。我们开始对 CompileMethod 这个方法下断点。首先我们通过【lm(load module)】命令找到clrjit.dll 这个dll。

 1 0:000> lm
 2 start    end        module name
 3 00f80000 00f88000   Example_10_1_1 C (service symbols: CLR Symbols with PDB: C:\ProgramData\...\Example_10_1_1.pdb)        
 4 5bd10000 5bdaf000   apphelp    (deferred)             
 5 6dbc0000 6efce000   mscorlib_ni   (service symbols: CLR Symbols with PDB: C:\ProgramData\...88BB3460CAC52\mscorlib.pdb)
 6 6efd0000 6f780000   clr        (pdb symbols)          C:\ProgramData\Dbg\sym\clr.pdb\5138062248484B79BCF6F6B3F3B59A3D2\clr.pdb
 7 721b0000 7223d000   mscoreei   (pdb symbols)          C:\ProgramData\Dbg\sym\mscoreei.pdb\3353E43482934F9AB820643DF51EC4692\mscoreei.pdb
 8 72240000 72292000   MSCOREE    (pdb symbols)          C:\ProgramData\Dbg\sym\mscoree.pdb\3A1E9FD59D013FF42905FC8655F33DCB1\mscoree.pdb
 9 722d0000 7235a000   clrjit     (deferred)             
10 72380000 7242b000   ucrtbase_clr0400   (deferred)             
11 ......        
12 77810000 77828000   win32u     (deferred)             
13 77830000 77ab0000   combase    (deferred)             
14 77ac0000 77c62000   ntdll      (pdb symbols)          C:\ProgramData\Dbg\sym\wntdll.pdb\DBC8C8F74C0E3696E951B77F0BB8569F1\wntdll.pdb
15 
16 Unloaded modules:
17 77830000 77ab0000   combase.dll

             红色标注的,说明CLR 和 JIT 都已经加载了。然后,我们通过【x】命令查找 CompileMethod方法。

1 0:000> x clrjit!*CompileMethod*
2 722d3700          clrjit!CILJit::compileMethod (class ICorJitInfo *, struct CORINFO_METHOD_INFO *, unsigned int, unsigned char **, unsigned long *)

            我们找到了 CompileMethod 方法,给这个方法下一个断点。

1 0:000> bp clrjit!CILJit::compileMethod

            我们【g】了两次,已经在 CILJit::compileMethod 方法处断住了。

 1 0:000> g
 2 Breakpoint 1 hit
 3 eax=00000002 ebx=0133ef84 ecx=03383840 edx=00000000 esi=033824c8 edi=0133eedc
 4 eip=016c0a3d esp=0133ee8c ebp=0133eee8 iopl=0         nv up ei pl zr na pe nc
 5 cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
 6 Example_10_1_1!COM+_Entry_Point <PERF> (Example_10_1_1+0x740a3d):
 7 016c0a3d b9144e6601      mov     ecx,1664E14h
 8 0:000> g
 9 Breakpoint 0 hit
10 eax=72354698 ebx=80000004 ecx=722d3700 edx=00005c10 esi=7234b3fc edi=0133e970
11 eip=722d3700 esp=0133e7ac ebp=0133e804 iopl=0         nv up ei ng nz na po nc
12 cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000282
13 clrjit!CILJit::compileMethod:
14 722d3700 55              push    ebp

            我们在 CILJit::compileMethod 这个方法已经断住了,然后我们使用【!kb】命令,查看一下它的参数。

1 0:000> kb 1
2  # ChildEBP RetAddr      Args to Child              
3 00 0133e804 6f09ccc3     72354698 0133e970 0133e8e8 clrjit!CILJit::compileMethod [f:\dd\ndp\clr\src\jit32\ee_il_dll.cpp @ 151] 

            72354698 0133e970 0133e8e8 这个三个参数的第三参数其实就是包含方法描述符的地址。

1 0:000> dp 0133e8e8 l1
2 0133e8e8  01665384

            我们可以使用【!dumpmd】命令查看这个方法的信息。

1 0:000> !dumpmd 01665384
2 Method Name:  DynamicClass.Add(Int32, Int32)(我们要查找的方法)
3 Class:        016652f0
4 MethodTable:  01665344
5 mdToken:      06000000
6 Module:       01664eac
7 IsJitted:     no
8 CodeAddr:     ffffffff(说明方法还没有编译)
9 Transparency: Transparent

            我们使用【k】命令,看看我的调用栈。

 1 0:000> k
 2  # ChildEBP RetAddr      
 3 00 0133e804 6f09ccc3     clrjit!CILJit::compileMethod [f:\dd\ndp\clr\src\jit32\ee_il_dll.cpp @ 151] 
 4 01 0133e804 6f09cd9b     clr!invokeCompileMethodHelper+0x10b
 5 02 0133e84c 6f09cdf8     clr!invokeCompileMethod+0x3d
 6 03 0133e8b8 6f09d0a7     clr!CallCompileMethodWithSEHWrapper+0x39
 7 04 0133ec78 6f09caac     clr!UnsafeJitFunction+0x431
 8 05 0133ed6c 6f09e703     clr!MethodDesc::MakeJitWorker+0x40b
 9 06 0133ede4 6f08f78f     clr!MethodDesc::DoPrestub+0x5f3
10 07 0133ee60 6efdf4bb     clr!PreStubWorker+0xe0
11 08 0133ee84 016c0aab     clr!ThePreStub+0x11
12 09 0133eee8 6efdf036     Example_10_1_1!COM+_Entry_Point <PERF> (Example_10_1_1+0x740aab) [E:\Visual Studio 2022\Program.cs @ 19] 
13 0a 0133eef4 6efe22da     clr!CallDescrWorkerInternal+0x34
14 0b 0133ef48 6efe859b     clr!CallDescrWorkerWithHandler+0x6b
15 0c 0133efb0 6f18b11b     clr!MethodDescCallSite::CallTargetWorker+0x16a
16 0d 0133f0d4 6f18b7fa     clr!RunMain+0x1b3
17 0e 0133f340 6f18b727     clr!Assembly::ExecuteMainMethod+0xf7
18 0f 0133f824 6f18b8a8     clr!SystemDomain::ExecuteMainMethod+0x5ef
19 10 0133f87c 6f18b9ce     clr!ExecuteEXE+0x4c
20 11 0133f8bc 6f187305     clr!_CorExeMainInternal+0xdc
21 12 0133f8f8 721bfa84     clr!_CorExeMain+0x4d
22 13 0133f930 7224e81e     mscoreei!_CorExeMain+0xd6
23 14 0133f940 72254338     MSCOREE!ShellShim__CorExeMain+0x9e
24 15 0133f958 765ff989     MSCOREE!_CorExeMain_Exported+0x8
25 16 0133f958 77b27084     KERNEL32!BaseThreadInitThunk+0x19
26 17 0133f9b4 77b27054     ntdll!__RtlUserThreadStart+0x2f
27 18 0133f9c4 00000000     ntdll!_RtlUserThreadStart+0x1b

            我们生成的 Add 方法,会首先进入  clr!MethodDesc::DoPrestub 桩函数,开始引导JIT编译,我们就在这个方法设断点。

1 0:000> bp 6f08f78f

            我们【g】继续运行,就会在 DoPrestub 方法断住。我们在通过【!dumpmd】命令查看方法的描述符。

 1 0:000> g
 2 Breakpoint 0 hit
 3 eax=016c0470 ebx=00000000 ecx=6f08f6aa edx=01665384 esi=01703978 edi=01665384
 4 eip=6f08f78f esp=0133edf0 ebp=0133ee60 iopl=0         nv up ei pl zr na pe nc
 5 cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
 6 clr!PreStubWorker+0xe0:(断点处)
 7 6f08f78f 8bf8            mov     edi,eax
 8 0:000> !dumpmd 01665384
 9 Method Name:  DynamicClass.Add(Int32, Int32)
10 Class:        016652f0
11 MethodTable:  01665344
12 mdToken:      06000000
13 Module:       01664eac
14 IsJitted:     yes
15 CodeAddr:     05940050(方法已经编译)
16 Transparency: Transparent

            我们有了CodeAddr: 05940050 方法的地址,我们就可以在这个地址上下断点。

1 0:000> bp 05940050
2 0:000> g
3 Breakpoint 1 hit
4 eax=016c0470 ebx=0133ef84 ecx=0000000a edx=00000014 esi=033824c8 edi=0133eedc
5 eip=05940050 esp=0133ee88 ebp=0133eee8 iopl=0         nv up ei pl zr na pe nc
6 cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
7 05940050 8bc1            mov     eax,ecx

            我们下了断点,【g】继续运行。执行进入 Add 方法第一行就被断住了,我们可以使用【!clrstack】命令,来验证。

1 0:000> !clrstack
2 OS Thread Id: 0x4168 (0)
3 Child SP       IP Call Site
4 0133ee88 05940050 DynamicClass.Add(Int32, Int32)(这就是我们动态生成的方法)
5 0133ee8c 016c0aab Example_10_1_1.Program.Main(System.String[]) [E:\Visual Studio 2022\Example_10_1_1\Program.cs @ 19]
6 0133f054 6efdf036 [GCFrame: 0133f054] 

           我们已经成功命中了 Add 方法,还是挺不容易的。

1 0:000> !U /d 05940050
2 Normal JIT generated code
3 DynamicClass.Add(Int32, Int32)
4 Begin 05940050, size 5
5 >>> 05940050 8bc1            mov     eax,ecx
6 05940052 03c2            add     eax,edx
7 05940054 c3              ret

            ecx 是方法的第一个参数,edx:是方法的第二个参数
            mov     eax, ecx    将 ecx 赋值给 eax,
            add     eax, edx    将 edx 和 eax 相加,eax 作为返回值返回
            ret
            我们对方法代码也做了一些解释。

        2.2、通过委托的函数指针找方法的描述符。
            测试源码:Example_10_1_2
            我们通过 Windbg 直接加载 Example_10_1_2 项目,【g】命令直接运行,会到【Debugger.Break()】这行代码处暂停,也就是一个 int 3 中断。我们的程序输出:Function Pointer:0x0000000004cd062e。然后我们使用【u 0x0000000004cd062e】命令,查看汇编代码。

1 0:000> u 0x0000000004cd062e
2 04cd062e b81806cd04      mov     eax,4CD0618h
3 04cd0633 e9e4c9dbfb      jmp     00a8d01c
4 04cd0638 ab              stos    dword ptr es:[edi]
5 04cd0639 ab              stos    dword ptr es:[edi]
6 04cd063a ab              stos    dword ptr es:[edi]
7 04cd063b ab              stos    dword ptr es:[edi]
8 04cd063c ab              stos    dword ptr es:[edi]
9 04cd063d ab              stos    dword ptr es:[edi]

            4CD0618h 红色标注的就是包含方法描述符的地址。我们使用【dp 4CD0618h l1】命令,查看详情。

1 0:000> dp 4CD0618h l1
2 04cd0618  00b20480

            00b20480 这个地址就是方法描述符,我们可以使用【!dumpmd 00b20480】命令。

0:000> dp 4CD0618h L1
04cd0618  00b20480
0:000> !U 00b20480
Unmanaged code
00b20480 e88bec4b6e      call    clr!PrecodeFixupThunk (6efdf110)
00b20485 5e              pop     esi
00b20486 001b            add     byte ptr [ebx],bl
00b20488 e883ec4b6e      call    clr!PrecodeFixupThunk (6efdf110)
00b2048d 5e              pop     esi
00b2048e 091a            or      dword ptr [edx],ebx
00b20490 e87bec4b6e      call    clr!PrecodeFixupThunk (6efdf110)
00b20495 5e              pop     esi
00b20496 1219            adc     bl,byte ptr [ecx]
00b20498 e873ec4b6e      call    clr!PrecodeFixupThunk (6efdf110)

            我这里没有得到 Add 的方法描述符。没有实现。

            
        2.3、在动态代码中注入 Debugger.Break()实现代码的调试。
            测试源码:Example_10_1_3
            我们通过 windbg 正常加载项目,直接【g】运行,会在16行处暂停,如图:
            Net 高级调试之十:轻量级代码生成的调试

             执行效果如下:

1 0:000> !clrstack
2 OS Thread Id: 0x482c (0)
3 Child SP       IP Call Site
4 00b5f3a8 760df262 [HelperMethodFrame: 00b5f3a8] System.Diagnostics.Debugger.BreakInternal()
5 00b5f424 6e74f195 System.Diagnostics.Debugger.Break() [f:\dd\ndp\clr\src\BCL\system\diagnostics\debugger.cs @ 91]
6 00b5f44c 00f10053 DynamicClass.Add(Int32, Int32)
7 00b5f458 00ee0b56 Example_10_1_3.Program.Main(System.String[]) [E:\Visual Studio 2022\...\Example_10_1_3\Program.cs @ 24]
8 00b5f654 6efdf036 [GCFrame: 00b5f654] 

            00f10053 红色标注的就是我们要找的Add 方法。我们可以使用【!u】命令查看它的汇编代码。

 1 0:000> !U /d 00f10053
 2 Normal JIT generated code
 3 DynamicClass.Add(Int32, Int32)
 4 Begin 00f10048, size 12
 5 00f10048 57              push    edi
 6 00f10049 56              push    esi
 7 00f1004a 8bf9            mov     edi,ecx
 8 00f1004c 8bf2            mov     esi,edx
 9 00f1004e e8e5f0836d      call    mscorlib_ni!System.Diagnostics.Debugger.Break (6e74f138)
10 >>> 00f10053 03fe            add     edi,esi
11 00f10055 8bc7            mov     eax,edi
12 00f10057 5e              pop     esi
13 00f10058 5f              pop     edi
14 00f10059 c3              ret


        2.4、程序集泄露
            测试源码:Example_10_1_4
            我们加载完 Example_10_1_4 项目,【g】继续运行,运行一段时间,我们点击【break】按钮暂停,当然,在运行的时候,我们可以通过任务管理器,查看这个应用程序的内存,发现内存一直在增长。
            我们使用【!dumpdomain】命令,查看一下应用程序域,不看不知道,一看吓一跳。

 1 0:006> !dumpdomain
 2 --------------------------------------
 3 System Domain:      6f71caf8
 4 LowFrequencyHeap:   6f71ce1c
 5 HighFrequencyHeap:  6f71ce68
 6 StubHeap:           6f71ceb4
 7 Stage:              OPEN
 8 Name:               None
 9 --------------------------------------
10 Shared Domain:      6f71c7a8
11 LowFrequencyHeap:   6f71ce1c
12 HighFrequencyHeap:  6f71ce68
13 StubHeap:           6f71ceb4
14 Stage:              OPEN
15 Name:               None
16 Assembly:           00c9d6f8 [C:\Windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll]
17 ClassLoader:        00c9d7c0
18   Module Name
19 6dbc1000    C:\Windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
20 
21 --------------------------------------
22 Domain 1:           00c4d7c8
23 LowFrequencyHeap:   00c4dc34
24 HighFrequencyHeap:  00c4dc80
25 StubHeap:           00c4dccc
26 Stage:              OPEN
27 SecurityDescriptor: 00c4ece0
28 Name:               Example_10_1_4.exe
29 Assembly:           00c9d6f8 [C:\Windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll]
30 ClassLoader:        00c9d7c0
31 SecurityDescriptor: 00c9b0d0
32   Module Name
33 6dbc1000    C:\Windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
34 
35 Assembly:           00caa098 [E:\Visual Studio 2022\Source\Projects\Example_10_1_4.exe]
36 ClassLoader:        00ca7268
37 SecurityDescriptor: 00ca7160
38   Module Name
39 00c04044    E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\Example_10_1_4\bin\Debug\Example_10_1_4.exe
40 
41 Assembly:           00cab1c0 [C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Core\v4.0_4.0.0.0__b77a5c561934e089\System.Core.dll]
42 ClassLoader:        00cab288
43 SecurityDescriptor: 00ca88e0
44   Module Name
45 6c591000    C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Core\v4.0_4.0.0.0__b77a5c561934e089\System.Core.dll
46 
47 Assembly:           00caafc0 [C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System\v4.0_4.0.0.0__b77a5c561934e089\System.dll]
48 ClassLoader:        00ca9b18
49 SecurityDescriptor: 00cac6f0
50   Module Name
51 6d041000    C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System\v4.0_4.0.0.0__b77a5c561934e089\System.dll
52 
53 Assembly:           00cabee0 [C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Xml\v4.0_4.0.0.0__b77a5c561934e089\System.Xml.dll]
54 ClassLoader:        00cb3bd0
55 SecurityDescriptor: 00cabe48
56   Module Name
57 6bc01000    C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Xml\v4.0_4.0.0.0__b77a5c561934e089\System.Xml.dll
58 
59 Assembly:           00cb8b80 [C:\Windows\Microsoft.Net\assembly\\System.Configuration.dll]
60 ClassLoader:        00cb7538
61 SecurityDescriptor: 00cb8ae8
62   Module Name
63 6dab1000    C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Configuration\v4.0_4.0.0.0__b03f5f7f11d50a3a\System.Configuration.dll
64 
65 Assembly:           00cc3418 (Dynamic) []
66 ClassLoader:        00cc34e0
67 SecurityDescriptor: 00cc3380
68   Module Name
69 00c08b6c    Dynamic Module
70 
71 Assembly:           00cc8f58 (Dynamic) []
72 ClassLoader:        00cc9020
73 SecurityDescriptor: 00cc8ec0
74   Module Name
75 00c0954c    Dynamic Module
76 
77 Assembly:           00cd49e0 (Dynamic) []
78 ClassLoader:        00cd4aa8
79 SecurityDescriptor: 00cd4948
80   Module Name
81 00c09ce4    Dynamic Module
82 
83 ......(我清理了大量的动态模块,显示太多了)

            我们查看一下模块的方法表,使用【!dumpmodule -mt】命令。

 1 0:006> !dumpmodule -mt 09e65b2c
 2 Name:       Unknown Module
 3 Attributes: Reflection 
 4 Assembly:   097f86c0
 5 LoaderHeap:              00000000
 6 TypeDefToMethodTableMap: 09e8195c
 7 TypeRefToMethodTableMap: 09e81970
 8 MethodDefToDescMap:      09e81984
 9 FieldDefToDescMap:       09e819ac
10 MemberRefToDescMap:      00000000
11 FileReferencesMap:       09e819fc
12 AssemblyReferencesMap:   09e81a10
13 
14 Types defined in this module
15 
16       MT  TypeDef Name
17 ------------------------------------------------------------------------------
18 09e65f68 0x02000002 <Unloaded Type>
19 09e6605c 0x02000003 <Unloaded Type>
20 09e660e4 0x02000004 <Unloaded Type>
21 09e66184 0x02000005 <Unloaded Type>
22 09e66260 0x02000006 <Unloaded Type>
23 
24 Types referenced in this module
25 
26       MT    TypeRef Name
27 ------------------------------------------------------------------------------

            我们查找红色标记 09e65f68 方法表的所有方法描述符。

 1 0:006> !dumpmt -md 09e65f68
 2 EEClass:         09e82bf8
 3 Module:          09e65b2c
 4 Name:            <Unloaded Type>
 5 mdToken:         02000002
 6 File:            Unknown Module
 7 BaseSize:        0x44
 8 ComponentSize:   0x0
 9 Slots in VTable: 8
10 Number of IFaces in IFaceMap: 0
11 --------------------------------------
12 MethodDesc Table
13    Entry MethodDe    JIT Name
14 6dfc97b8 6dbcc838 PreJIT System.Object.ToString()
15 6dfc96a0 6dd08978 PreJIT System.Object.Equals(System.Object)
16 6dfd21f0 6dd08998 PreJIT System.Object.GetHashCode()
17 6df84f2c 6dd089a0 PreJIT System.Object.Finalize()
18 09e4a47d 09e65f44   NONE Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializationWriterCustomer.InitCallbacks()
19 09e4a481 09e65f4c   NONE Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializationWriterCustomer..ctor()
20 09e4a475 09e65f2c   NONE Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializationWriterCustomer.Write3_FabrikamCustomer(...)
21 09e4a479 09e65f38   NONE Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializationWriterCustomer.Write2_Customer(... Boolean)

            这里大量的有 Serialization.GeneratedAssembly 这个方法,估计判断是程序集泄露。既然这么多程序集产生,应该就有一个创建程集的方法,我们使用【x】命令,查找一下。

1 0:006> x clr!*CreateDynamic*
2 6f385b06          clr!Assembly::CreateDynamicModule (public: class ReflectionModule * __thiscall Assembly::CreateDynamicModule(unsigned short const *,unsigned short const *,int,int *))
3 6f5b0aff          clr!HENUMInternal::CreateDynamicArrayEnum (public: static long __stdcall HENUMInternal::CreateDynamicArrayEnum(unsigned long,struct HENUMInternal * *))
4 6f166b49          clr!Assembly::CreateDynamic (public: static class Assembly * __stdcall Assembly::CreateDynamic(class AppDomain *,struct CreateDynamicAssemblyArgs *))
5 6f167440          clr!AppDomainNative::CreateDynamicAssembly (public: static class Object * __fastcall AppDomainNative::CreateDynamicAssembly(class AppDomainBaseObject *,class AssemblyNameBaseObject *,enum SecurityContextSource,int,int,class Array<unsigned char> *,class Array<unsigned char> *,class Object *,class Object *,class Object *,enum StackCrawlMark *,class Object *))
6 6f3dd86f          clr!DynamicMethodTable::CreateDynamicMethodTable (public: static void __stdcall DynamicMethodTable::CreateDynamicMethodTable(class DynamicMethodTable * *,class Module *,class AppDomain *))

            就是这个方法创建程序集,我们可以对这个方法下一个断点。

1 0:006> bp clr!Assembly::CreateDynamic

            【g】继续运行,在 CreateDynamic 方法内暂停。

1 0:006> g
2 Breakpoint 0 hit
3 eax=00c4d7c8 ebx=00000000 ecx=00c4d7c8 edx=008fecb4 esi=02ad7a5c edi=00000000
4 eip=6f166b49 esp=008fec3c ebp=008fed00 iopl=0         nv up ei pl nz na po nc
5 cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202
6 clr!Assembly::CreateDynamic:(成功断住)
7 6f166b49 6814070000      push    714h

            然后我们使用【k】命令,查看是谁调用了这个函数。

 1 0:000> k
 2 *** WARNING: Unable to verify checksum for C:\Windows\assembly\NativeImages_v4.0.30319_32\System.Xml\040fa6ee0be6d987f3e8edf9010ce68a\System.Xml.ni.dll
 3 *** WARNING: Unable to verify checksum for C:\Windows\assembly\NativeImages_v4.0.30319_32\System.Core\44e36f78b5e2f34aba2d7b5667796954\System.Core.ni.dll
 4  # ChildEBP RetAddr      
 5 00 010feef0 6f167543     clr!Assembly::CreateDynamic
 6 01 010feef0 6dfef391     clr!AppDomainNative::CreateDynamicAssembly+0xdf
 7 02 010fef78 6dfef261     mscorlib_ni!System.Reflection.Emit.AssemblyBuilder..ctor+0xed [f:\dd\ndp\clr\src\BCL\system\reflection\emit\assemblybuilder.cs @ 424] 
 8 03 010fefd4 6e016558     mscorlib_ni!System.Reflection.Emit.AssemblyBuilder.InternalDefineDynamicAssembly+0x89 [f:\dd\ndp\clr\src\BCL\system\reflection\emit\assemblybuilder.cs @ 569] 
 9 04 010feffc 6e016527     mscorlib_ni!System.AppDomain.InternalDefineDynamicAssembly+0x28 [f:\dd\ndp\clr\src\BCL\system\appdomain.cs @ 1515] 
10 05 010ff02c 6bd100bd     mscorlib_ni!System.AppDomain.DefineDynamicAssembly+0x2b [f:\dd\ndp\clr\src\BCL\system\appdomain.cs @ 1221] 
11 06 010ff044 6bd0fa1b     System_Xml_ni!System.Xml.Serialization.CodeGenerator.CreateAssemblyBuilder+0x8d
12 07 010ff0dc 6bd0f696     System_Xml_ni!System.Xml.Serialization.TempAssembly.GenerateRefEmitAssembly+0xcf
13 08 010ff110 6c09db2a     System_Xml_ni!System.Xml.Serialization.TempAssembly..ctor+0xae
14 09 010ff138 6c09da72     System_Xml_ni!System.Xml.Serialization.XmlSerializer.GenerateTempAssembly+0x6a
15 0a 010ff164 6c09d8e7     System_Xml_ni!System.Xml.Serialization.XmlSerializer..ctor+0xd2
16 0b 010ff18c 017710aa     System_Xml_ni!System.Xml.Serialization.XmlSerializer..ctor+0x2f
17 WARNING: Frame IP not in any known module. Following frames may be wrong.
18 0c 010ff1e8 01770ffc     0x17710aa
19 0d 010ff200 01770f88     0x1770ffc
20 0e 010ff210 6dfaaa67     0x1770f88
21 0f 010ff244 6c77c5de     mscorlib_ni!System.Collections.Generic.List`1..ctor+0xf7 [f:\dd\ndp\clr\src\BCL\system\collections\generic\list.cs @ 99] 
22 10 010ff258 017708f2     System_Core_ni+0x1ec5de
23 11 010ff288 6efdf036     0x17708f2
24 12 010ff294 6efe22da     clr!CallDescrWorkerInternal+0x34
25 13 010ff2e8 6efe859b     clr!CallDescrWorkerWithHandler+0x6b
26 14 010ff354 6f18b11b     clr!MethodDescCallSite::CallTargetWorker+0x16a
27 15 010ff478 6f18b7fa     clr!RunMain+0x1b3
28 16 010ff6e4 6f18b727     clr!Assembly::ExecuteMainMethod+0xf7
29 17 010ffbc8 6f18b8a8     clr!SystemDomain::ExecuteMainMethod+0x5ef
30 18 010ffc20 6f18b9ce     clr!ExecuteEXE+0x4c
31 19 010ffc60 6f187305     clr!_CorExeMainInternal+0xdc
32 1a 010ffc9c 721bfa84     clr!_CorExeMain+0x4d
33 1b 010ffcd4 7224e81e     mscoreei!_CorExeMain+0xd6
34 1c 010ffce4 72254338     MSCOREE!ShellShim__CorExeMain+0x9e
35 1d 010ffcfc 765ff989     MSCOREE!_CorExeMain_Exported+0x8
36 1e 010ffcfc 77b27084     KERNEL32!BaseThreadInitThunk+0x19
37 1f 010ffd58 77b27054     ntdll!__RtlUserThreadStart+0x2f
38 20 010ffd68 00000000     ntdll!_RtlUserThreadStart+0x1b

            或者我们使用【!clrstack】命令,查看一下调用堆栈,这个命令看的更清楚。

 1 0:000> !clrstack
 2 OS Thread Id: 0x3534 (0)
 3 Child SP       IP Call Site
 4 010fee44 6f166b49 [HelperMethodFrame_PROTECTOBJ: 010fee44] System.Reflection.Emit.AssemblyBuilder.nCreateDynamicAssembly(System.AppDomain, System.Reflection.AssemblyName, System.Security.Policy.Evidence, System.Threading.StackCrawlMark ByRef, System.Security.PermissionSet, System.Security.PermissionSet, System.Security.PermissionSet, Byte[], Byte[], System.Reflection.Emit.AssemblyBuilderAccess, System.Reflection.Emit.DynamicAssemblyFlags, System.Security.SecurityContextSource)
 5 010fef20 6dfef391 System.Reflection.Emit.AssemblyBuilder..ctor(System.AppDomain, System.Reflection.AssemblyName, System.Reflection.Emit.AssemblyBuilderAccess, System.String, System.Security.Policy.Evidence, System.Security.PermissionSet, System.Security.PermissionSet, System.Security.PermissionSet, System.Threading.StackCrawlMark ByRef, System.Collections.Generic.IEnumerable`1<System.Reflection.Emit.CustomAttributeBuilder>, System.Security.SecurityContextSource) [f:\dd\ndp\clr\src\BCL\system\reflection\emit\assemblybuilder.cs @ 424]
 6 010fefa8 6dfef261 System.Reflection.Emit.AssemblyBuilder.InternalDefineDynamicAssembly(System.Reflection.AssemblyName, System.Reflection.Emit.AssemblyBuilderAccess, System.String, System.Security.Policy.Evidence, System.Security.PermissionSet, System.Security.PermissionSet, System.Security.PermissionSet, System.Threading.StackCrawlMark ByRef, System.Collections.Generic.IEnumerable`1<System.Reflection.Emit.CustomAttributeBuilder>, System.Security.SecurityContextSource) [f:\dd\ndp\clr\src\BCL\system\reflection\emit\assemblybuilder.cs @ 569]
 7 010feffc 6e016558 System.AppDomain.InternalDefineDynamicAssembly(System.Reflection.AssemblyName, System.Reflection.Emit.AssemblyBuilderAccess, System.String, System.Security.Policy.Evidence, System.Security.PermissionSet, System.Security.PermissionSet, System.Security.PermissionSet, System.Threading.StackCrawlMark ByRef, System.Collections.Generic.IEnumerable`1<System.Reflection.Emit.CustomAttributeBuilder>, System.Security.SecurityContextSource) [f:\dd\ndp\clr\src\BCL\system\appdomain.cs @ 1515]
 8 010ff028 6e016527 System.AppDomain.DefineDynamicAssembly(System.Reflection.AssemblyName, System.Reflection.Emit.AssemblyBuilderAccess) [f:\dd\ndp\clr\src\BCL\system\appdomain.cs @ 1221]
 9 010ff038 6bd100bd System.Xml.Serialization.CodeGenerator.CreateAssemblyBuilder(System.AppDomain, System.String)
10 010ff04c 6bd0fa1b System.Xml.Serialization.TempAssembly.GenerateRefEmitAssembly(System.Xml.Serialization.XmlMapping[], System.Type[], System.String, System.Security.Policy.Evidence)
11 010ff0ec 6bd0f696 System.Xml.Serialization.TempAssembly..ctor(System.Xml.Serialization.XmlMapping[], System.Type[], System.String, System.String, System.Security.Policy.Evidence)
12 010ff128 6c09db2a System.Xml.Serialization.XmlSerializer.GenerateTempAssembly(System.Xml.Serialization.XmlMapping, System.Type, System.String, System.String, System.Security.Policy.Evidence)
13 010ff14c 6c09da72 System.Xml.Serialization.XmlSerializer..ctor(System.Type, System.Xml.Serialization.XmlAttributeOverrides, System.Type[], System.Xml.Serialization.XmlRootAttribute, System.String, System.String, System.Security.Policy.Evidence)
14 010ff184 6c09d8e7 System.Xml.Serialization.XmlSerializer..ctor(System.Type, System.Xml.Serialization.XmlRootAttribute)
15 010ff198 017710aa Example_10_1_4.Program.GetCustomer(Int32, System.String, System.String) [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\Example_10_1_4\Program.cs @ 27]
16 010ff1f4 01770ffc Example_10_1_4.Program+c__DisplayClass0_0.b__0(Int32) [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\Example_10_1_4\Program.cs @ 18]
17 010ff208 01770f88 System.Linq.Enumerable+WhereSelectEnumerableIterator`2[[System.Int32, mscorlib],[System.__Canon, mscorlib]].MoveNext()
18 010ff218 6dfaaa67 System.Collections.Generic.List`1[[System.__Canon, mscorlib]]..ctor(System.Collections.Generic.IEnumerable`1<System.__Canon>) [f:\dd\ndp\clr\src\BCL\system\collections\generic\list.cs @ 99]
19 010ff24c 6c77c5de System.Linq.Enumerable.ToList[[System.__Canon, mscorlib]](System.Collections.Generic.IEnumerable`1<System.__Canon>)
20 010ff260 017708f2 Example_10_1_4.Program.Main(System.String[]) [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\Example_10_1_4\Program.cs @ 17]
21 010ff3f8 6efdf036 [GCFrame: 010ff3f8] 

              我们终于找到问题了。代码中把解决办法已经写好了。


四、总结
    终于写完了,写作的过程是累并快乐着。学习过程真的没那么轻松,还好是自己比较喜欢这一行,否则真不知道自己能不能坚持下来。老话重谈,《高级调试》的这本书第一遍看,真的很晕,第二遍稍微好点,不学不知道,一学吓一跳,自己欠缺的很多。好了,不说了,不忘初心,继续努力,希望老天不要辜负努力的人。文章来源地址https://www.toymoban.com/news/detail-747051.html

到了这里,关于Net 高级调试之十:轻量级代码生成的调试的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 『SEQ日志』在 .NET中快速集成轻量级的分布式日志平台

    📣读完这篇文章里你能收获到 如何在Docker中部署 SEQ:介绍了如何创建和运行 SEQ 容器,给出了详细的执行操作 如何使用 NLog 接入 .NET Core 应用程序的日志:详细介绍了 NLog 和 NLog.Seq 来配置和记录日志的步骤 日志记录示例:博客提供了一个简单的日志记录示例,展示了如何在

    2024年02月11日
    浏览(43)
  • 尚硅谷Docker实战教程-笔记13【高级篇,Docker轻量级可视化工具Portainer】

    尚硅谷大数据技术-教程-学习路线-笔记汇总表【课程资料下载】 视频地址:尚硅谷Docker实战教程(docker教程天花板)_哔哩哔哩_bilibili 尚硅谷Docker实战教程-笔记01【基础篇,Docker理念简介、官网介绍、平台入门图解、平台架构图解】 尚硅谷Docker实战教程-笔记02【基础篇,Do

    2024年02月15日
    浏览(27)
  • Net 高级调试之十一:托管堆布局架构和对象分配机制

    一、简介 今天是《Net 高级调试》的第十一篇文章,这篇文章来的有点晚,因为,最近比较忙,就没时间写文章了。现在终于有点时间,继续开始我们这个系列。这篇文章我们主要介绍托管堆的架构,对象的分配机制,我们如何查找在托管堆上的对象,我学完这章,很多以前

    2024年02月05日
    浏览(27)
  • C#轻量级高并发物联网服务器接收程序源码,可对接数万设备,数据库可自行选择(EF6+SQLite或EF+MySQL),适合中高级开发者使用

    c#轻量级高并发物联网服务器接收程序源码(仅仅是接收硬件数据程序,没有web端,不是java,协议自己写,如果问及这些问题统统不回复。 ),对接几万个设备没问题,数据库采用ef6+sqlite,可改ef+MySQL.该程序只是源码使用示例,里面有使用方法,自己研究,难度属中上层不

    2024年04月11日
    浏览(83)
  • git轻量级服务器gogs、gitea,非轻量级gitbucket

    本文来源:git轻量级服务器gogs、gitea,非轻量级gitbucket, 或 gitcode/gogs,gitea.md 结论: gogs、gitea很相似 确实轻, gitbucket基于java 不轻, 这三者都不支持组织树(嵌套组织 nested group) 只能一层组织。 个人用,基于gogs、gitea,两层结构树 简易办法: 把用户当成第一层节点、该用户的

    2024年02月07日
    浏览(52)
  • 轻量灵动: 革新轻量级服务开发

    从 JDK 8 升级到 JDK 17 可以让你的应用程序受益于新的功能、性能改进和安全增强。下面是一些 JDK 8 升级到 JDK 17 的最佳实战: 1.1、确定升级的必要性:首先,你需要评估你的应用程序是否需要升级到 JDK 17。查看 JDK 17 的新特性、改进和修复的 bug,以确定它们对你的应用程序

    2024年02月07日
    浏览(35)
  • 轻量级 HTTP 请求组件

    Apache HttpClient 是著名的 HTTP 客户端请求工具——现在我们模拟它打造一套简单小巧的请求工具库, 封装 Java 类库里面的 HttpURLConnection 对象来完成日常的 HTTP 请求,诸如 GET、HEAD、POST 等等,并尝试应用 Java 8 函数式风格来制定 API。 组件源码在:https://gitee.com/sp42_admin/ajaxjs/tr

    2024年02月01日
    浏览(48)
  • 一种轻量级定时任务实现

    现在市面上有各式各样的分布式定时任务,每个都有其独特的特点,我们这边的项目因为一开始使用的是分布式开源调度框架TBSchedule,但是这个框架依赖ZK, 由于ZK的不稳定性和项目老旧无人维护 ,导致我们的定时任务会偶发出现异常,比如:任务停止、任务项丢失、任务不

    2024年02月14日
    浏览(27)
  • Tomcat轻量级服务器

    目录 1.常见系统架构  C-S架构 B-S架构 2.B-S架构系统的通信步骤 3.常见WEB服服务器软件 4.Tomcat服务器的配置 下载安装 环境变量配置 测试环境变量是否配置成功 测试Tomcat服务器是否配置成功  Tomcat窗口一闪而过的解决步骤 Tomcat解决乱码 介绍: C-S架构即Client/Server(客户端/服务

    2023年04月14日
    浏览(115)
  • Kotlin 轻量级Android开发

    Kotlin 是一门运行在 JVM 之上的语言。 它由 Jetbrains 创建,而 Jetbrains 则是诸多强大的工具(如知名的 Java IDE IntelliJ IDEA )背后的公司。 Kotlin 是一门非常简单的语言,其主要目标之一就是提供强大语言的同时又保持简单且精简的语法。 其主要特性如下所示: 轻量级:这一点对

    2024年02月07日
    浏览(43)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包