Net 高级调试之八:代码审查及杂项命令


风晓
风晓 2023-12-29 11:48:24 50441 赞同 0 反对 0
分类: 资源
一、简介     今天是《Net 高级调试》的第八篇文章。这篇文章设计的内容挺多的,比如:如何查看方法的汇编代码,如何获取方法的描述符,对象同步块的转储,对象方法表的转储,托管堆和垃圾回收器信息的转储,CLR 的版本,GC 模式,等等,内容挺多的。内容虽然挺多,但是这些都是高级调试的基础。虽然这些都是基础,如果这些掌握不好,以后的高级调试的道路,也不好走。当然了,第一次看视频或者看书,是很迷糊的,不知道如何操作,还是那句老话,一遍不行,那就再来一遍,还不行,那就再来一遍,俗话说的好,书读千遍,其意自现。      如果在没有说明的情况下,所有代码的测试环境都是 Net Framewok 4.8,但是,有时候为了查看源码,可能需要使用 Net Core 的项目,我会在项目章节里进行说明。好了,废话不多说,开始我们今天的调试工作。      调试环境我需要进行说明,以防大家不清楚,具体情况我已经罗列出来。           操作系统:Windows Professional 10           调试工具:Windbg Preview(可以去Microsoft Store 去下载)           开发工具:Visual Studio 2022           Net 版本:Net Framework 4.8           CoreCLR源码:源码下载

二、基础知识
    1、代码审查
        1.1、简介
            代码审查就是观察代码,代码由三种心态:机器代码、IL代码、C#代码。高级调试属于逆向分析,更多的是以 汇编代码 为主,如果对汇编无感,想学好 Net 高级调试,也是比较困难的。

        1.2、观察汇编代码
            1)u命令
                这个命令用来将非托管函数的机器代码转成汇编代码,当然,我们想要查看汇编代码,不一定非要使用 Windbg,我们也可以使用 Visual Studio IDE 的反汇编窗口。我们可以通过点击菜单【调试】--》【窗口】----》【反汇编】,打开反汇编窗口。
                

 

            2)!u命令
                这个命令是由 SOS 提供的,专门用于观察 托管函数 的汇编表示,接下来,我们查看一下 Main 方法的汇编代码。

        1.3、观察 IL 代码
            SOS 提供了一个叫 !dumpil 的命令用来将托管函数的汇编指令转成可读的 IL 代码。

        1.4、观察 C# 代码
            要观察 C# 代码,需要将内存中的 module 给剥离出来,然后使用 ILSpy 或者 DnSpy 等反编译工具反转即可。这就需要使用 Windbg 的【!savemodule】命令。大家也要防止 Dump 泄露,这个是严重的,别人就可以看到你的完整代码。
        
    2、杂项命令
        2.1、获取 CLR 版本、GC模式
            如果想获取 CLR 的版本,可以使用【!eeversion】命令,当然,我们也可以通过查看堆的数量,来了解 GC 模式,使用【!eeheap -gc】命令。

        2.2、查看线程栈对象
            如果我们想查看线程栈上的对象,一般都会使用【!clrstack -a】命令,但是这个命令只是看表面,不能映射到对象,那么我们可以使用【!dso】命令,这个命令可以把线程栈中的所有对象全部显示出来。

        2.3、观察对象引用根
            我们知道了如何查看对象引用根,我们就知道了为什么没有被 GC 回收,很轻松找到问题的所在,这个命令就是【!gcroot】。

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

    1、测试源码
        1.1、Example_8_1_1 

 View Code


        1.2、Example_8_1_2

 View Code

 
    2、眼见为实
        项目的所有操作都是一样的,所以就在这里说明一下,但是每个测试例子,都需要重新启动,并加载相应的应用程序,加载方法都是一样的。流程如下:我们编译项目,打开 Windbg,点击【文件】----》【launch executable】附加程序,打开调试器的界面,程序已经处于中断状态。我们需要使用【g】命令,继续运行程序,然后到达指定地点停止后,我们可以点击【break】按钮,就可以调试程序了。有时候可能需要切换到主线程,可以使用【~0s】命令。

        2.1、查看 clr!CallDescrWorkerInternal+0x34 处的汇编代码。
            测试源码:Example_8_1_1
            说明一下,在这里用其他的程序都是可以的,只是查看汇编代码,可以查看非托管函数的、也可以查看托管函数的汇编代码。
            接下来,我们先找一个非托管函数的,看看它们的汇编代码,到底是哪个函数,是随机的。
            我们使用【k】命令,查看一下非托管的调用栈,然后从里面随便找一个函数,看看它的汇编代码。

 View Code

            调用栈还是很多的,列表里有:00eff144 716322da clr!CallDescrWorkerInternal+0x34,这样一行代码,我们使用【u】命令查看一下。

复制代码
 1 0:000> u clr!CallDescrWorkerInternal+0x34
 2 clr!CallDescrWorkerInternal+0x34:
 3 7162f036 8b4b0c          mov     ecx,dword ptr [ebx+0Ch]
 4 7162f039 83f900          cmp     ecx,0
 5 7162f03c 740c            je      clr!CallDescrWorkerInternal+0x48 (7162f04a)
 6 7162f03e 83f904          cmp     ecx,4
 7 7162f041 7412            je      clr!CallDescrWorkerInternal+0x53 (7162f055)
 8 7162f043 83f908          cmp     ecx,8
 9 7162f046 7412            je      clr!CallDescrWorkerInternal+0x58 (7162f05a)
10 7162f048 eb06            jmp     clr!CallDescrWorkerInternal+0x4e (7162f050)
复制代码

            代码很清楚,可以自行查看。当然我们也可以在Windbg的【Disassembly】窗口查看汇编代码,效果如图:
                    

            【u】命令是从上向下看汇编代码,我们也可以使用【ub】命令,从下向上查看汇编代码。

复制代码
 1 0:000> ub clr!CallDescrWorkerInternal+0x34
 2 clr!CallDescrWorkerInternal+0x21:
 3 7162f023 83e804          sub     eax,4
 4 7162f026 ff30            push    dword ptr [eax]
 5 7162f028 49              dec     ecx
 6 7162f029 75f8            jne     clr!CallDescrWorkerInternal+0x21 (7162f023)
 7 7162f02b 8b4308          mov     eax,dword ptr [ebx+8]
 8 7162f02e 8b10            mov     edx,dword ptr [eax]
 9 7162f030 8b4804          mov     ecx,dword ptr [eax+4]
10 7162f033 ff5310          call    dword ptr [ebx+10h]
复制代码

             两个命令的效果,如图:
             

              如果我们想把一个函数的所有的汇编代码全部显示出来,我们可以使用【uf】命令,我们看看 clr!_CorExeMain 函数的汇编代码吧。

复制代码
 1 0:000> uf clr!_CorExeMain
 2 clr!_CorExeMain:
 3 717d72e0 6a14            push    14h
 4 717d72e2 6818737d71      push    offset clr!`dynamic atexit destructor for 'AppDataPathHolder''+0x27e0 (717d7318)
 5 717d72e7 e8349de4ff      call    clr!_SEH_prolog4 (71621020)
 6 717d72ec 33c0            xor     eax,eax
 7 717d72ee 8985e0ffffff    mov     dword ptr [ebp-20h],eax
 8 717d72f4 8985e4ffffff    mov     dword ptr [ebp-1Ch],eax
 9 717d72fa 8985fcffffff    mov     dword ptr [ebp-4],eax
10 717d7300 e81b460000      call    clr!_CorExeMainInternal (717db920)
11 717d7305 c785fcfffffffeffffff mov dword ptr [ebp-4],0FFFFFFFEh
12 717d730f 33c0            xor     eax,eax
13 717d7311 e8509de4ff      call    clr!_SEH_epilog4 (71621066)
14 717d7316 c3              ret
复制代码


        2.2、我们查看 Program 类型的 Main 方法的汇编代码。
            测试源码:Example_8_1_1
            

复制代码
 1 0:000> !clrstack
 2 OS Thread Id: 0x4164 (0)
 3 Child SP       IP Call Site
 4 006fee34 772b10fc [InlinedCallFrame: 006fee34] 
 5 006fee30 705f9b71 DomainNeutralILStubClass.IL_STUB_PInvoke(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte*, Int32, Int32 ByRef, IntPtr)
 6 006fee34 70d2b275 [InlinedCallFrame: 006fee34] Microsoft.Win32.Win32Native.ReadFile(.....) 7 006fee98 70d2b275 System.IO.__ConsoleStream.ReadFileNative(oolean, Int32 ByRef) [f:\dd\ndp\clr\src\BCL\system\io\__consolestream.cs @ 205]
 8 006feecc 70d2b17b System.IO.__ConsoleStream.Read(Byte[], Int32, Int32) [f:\dd\ndp\clr\src\BCL\system\io\__consolestream.cs @ 134]
 9 006feeec 705de6a3 System.IO.StreamReader.ReadBuffer() [f:\dd\ndp\clr\src\BCL\system\io\streamreader.cs @ 595]
10 006feefc 705deb5b System.IO.StreamReader.ReadLine() [f:\dd\ndp\clr\src\BCL\system\io\streamreader.cs @ 748]
11 006fef18 70e73786 System.IO.TextReader+SyncTextReader.ReadLine() [f:\dd\ndp\clr\src\BCL\system\io\textreader.cs @ 363]
12 006fef28 70cd1845 System.Console.ReadLine() [f:\dd\ndp\clr\src\BCL\system\console.cs @ 1984]
13 006fef30 025808d1 Example_8_1_1.Program.Main(System.String[]) [E:\Visual Studio 2022\..\Example_8_1_1\Program.cs @ 11]
14 006ff0d0 7162f036 [GCFrame: 006ff0d0] 
复制代码

            025808d1 Main 方法的地址,我们可以使用【!u】查看一下。

 View Code

            代码太多,不变展示。>>> 025808d1 8945e0 mov dword ptr [ebp-20h],eax,三个小于号,表示当前执行到的位置。


        2.3、使用【!dumpil】命令查看 IL 代码。
            测试源码:Example_8_1_1
            我们要想查看一个方法的 IL 代码,必须先找到 这个方法的 MD,也就是方法描述符。
            我们可以使用【!clrstack】命令找到 Main 方法的 IP,根据 IP 再找到 MD。

复制代码
 1 0:000> !clrstack
 2 OS Thread Id: 0x4164 (0)
 3 Child SP       IP Call Site
 4 006fee34 772b10fc [InlinedCallFrame: 006fee34] 
 5 006fee30 705f9b71 DomainNeutralILStubClass.IL_STUB_PInvoke(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte*, Int32, Int32 ByRef, IntPtr)
 6 006fee34 70d2b275 [InlinedCallFrame: 006fee34] Microsoft.Win32.Win32Native.ReadFile(... Int32 ByRef, IntPtr)
 7 006fee98 70d2b275 System.IO.__ConsoleStream.ReadFileNative(...) [f:\dd\ndp\clr\src\BCL\system\io\__consolestream.cs @ 205]
 8 006feecc 70d2b17b System.IO.__ConsoleStream.Read(Byte[], Int32, Int32) [f:\dd\ndp\clr\src\BCL\system\io\__consolestream.cs @ 134]
 9 006feeec 705de6a3 System.IO.StreamReader.ReadBuffer() [f:\dd\ndp\clr\src\BCL\system\io\streamreader.cs @ 595]
10 006feefc 705deb5b System.IO.StreamReader.ReadLine() [f:\dd\ndp\clr\src\BCL\system\io\streamreader.cs @ 748]
11 006fef18 70e73786 System.IO.TextReader+SyncTextReader.ReadLine() [f:\dd\ndp\clr\src\BCL\system\io\textreader.cs @ 363]
12 006fef28 70cd1845 System.Console.ReadLine() [f:\dd\ndp\clr\src\BCL\system\console.cs @ 1984]
13 006fef30 025808d1 Example_8_1_1.Program.Main(System.String[]) [E:\Visual Studio 2022\...\Example_8_1_1\Program.cs @ 11]
14 006ff0d0 7162f036 [GCFrame: 006ff0d0] 
复制代码

            025808d1 红色标注的就是 Main 方法的IP,我们使用【!ip2md】命令,就可以找到方法描述符Md了。

复制代码
 1 0:000> !ip2md 025808d1
 2 MethodDesc:   00c44d58
 3 Method Name:  Example_8_1_1.Program.Main(System.String[])
 4 Class:        00c41290
 5 MethodTable:  00c44d78
 6 mdToken:      06000001
 7 Module:       00c44044
 8 IsJitted:     yes
 9 CodeAddr:     02580848
10 Transparency: Critical
11 Source file:  E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\Example_8_1_1\Program.cs @ 11
复制代码

           MethodDesc: 00c44d58 红色标注的就是方法描述符的而地址。有了方法描述(MD),我们就可以使用【!dumpil】命令查看 IL 代码了。

复制代码
 1 0:000> !dumpil 00c44d58
 2 ilAddr = 00362050
 3 IL_0000: nop 
 4 IL_0001: ldc.i4.s 10
 5 IL_0003: ldc.i4.s 11
 6 IL_0005: call Example_8_1_1.Program::Sum
 7 IL_000a: stloc.0 
 8 IL_000b: ldstr "sum={0}"
 9 IL_0010: ldloc.0 
10 IL_0011: box System.Int32
11 IL_0016: call System.String::Format 
12 IL_001b: call System.Console::WriteLine 
13 IL_0020: nop 
14 IL_0021: call System.Console::ReadLine 
15 IL_0026: pop 
16 IL_0027: ret 
复制代码

            除了以上方法,我们也可以使用【!name2ee】命令达到同样的效果。那我么查看一下 Sum 方法 IL 代码。

复制代码
1 0:000> !name2ee Example_8_1_1!Example_8_1_1.Program.Sum
2 Module:      00c44044
3 Assembly:    Example_8_1_1.exe
4 Token:       06000002
5 MethodDesc:  00c44d64
6 Name:        Example_8_1_1.Program.Sum(Int32, Int32)
7 JITTED Code Address: 025808f0
复制代码

           MethodDesc: 00c44d64 红色标注的就是方法描述符的地址,有了这个地址,我们就可以使用【!dumpil】命令了。

复制代码
 1 0:000> !dumpil 00c44d64
 2 ilAddr = 00362084
 3 IL_0000: nop 
 4 IL_0001: ldarg.0 
 5 IL_0002: stloc.0 
 6 IL_0003: ldarg.1 
 7 IL_0004: stloc.1 
 8 IL_0005: ldloc.0 
 9 IL_0006: ldloc.1 
10 IL_0007: add 
11 IL_0008: stloc.2 
12 IL_0009: ldloc.2 
13 IL_000a: stloc.3 
14 IL_000b: br.s IL_000d
15 IL_000d: ldloc.3 
16 IL_000e: ret 
复制代码


        2.4、通过 Dump 查看 C# 源码。
            测试源码:Example_8_1_1
            我们想要查看 Example_8_1_1项目的源码,必须找到这个项目的【模块】,也就是 module,我们切换到主线程,也就是 0 号线程,使用命令【~0s】,使用命令【!clrstack】找到调用栈。

复制代码
 1 0:000> !clrstack
 2 OS Thread Id: 0x3438 (0)
 3 Child SP       IP Call Site
 4 00cfef94 772910fc [InlinedCallFrame: 00cfef94] 
 5 00cfef90 6d869b71 DomainNeutralILStubClass.IL_STUB_PInvoke(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte*, Int32, Int32 ByRef, IntPtr)
 6 00cfef94 6df9b275 [InlinedCallFrame: 00cfef94] Microsoft.Win32.Win32Native.ReadFile(... Int32 ByRef, IntPtr)
 7 00cfeff8 6df9b275 System.IO.__ConsoleStream.ReadFileNative(.. Int32 ByRef) [f:\dd\ndp\clr\src\BCL\system\io\__consolestream.cs @ 205]
 8 00cff02c 6df9b17b System.IO.__ConsoleStream.Read(Byte[], Int32, Int32) [f:\dd\ndp\clr\src\BCL\system\io\__consolestream.cs @ 134]
 9 00cff04c 6d84e6a3 System.IO.StreamReader.ReadBuffer() [f:\dd\ndp\clr\src\BCL\system\io\streamreader.cs @ 595]
10 00cff05c 6d84eb5b System.IO.StreamReader.ReadLine() [f:\dd\ndp\clr\src\BCL\system\io\streamreader.cs @ 748]
11 00cff078 6e0e3786 System.IO.TextReader+SyncTextReader.ReadLine() [f:\dd\ndp\clr\src\BCL\system\io\textreader.cs @ 363]
12 00cff088 6df41845 System.Console.ReadLine() [f:\dd\ndp\clr\src\BCL\system\console.cs @ 1984]
13 00cff090 013e08d1 Example_8_1_1.Program.Main(System.String[]) [E:\Visual Studio 2022\...\Example_8_1_1\Program.cs @ 11]
14 00cff228 6fdbf036 [GCFrame: 00cff228] 
复制代码

            013e08d1 红色标注的地址就是 Main 方法的 IP,有了 IP,我们可以通过 IP 找到 MD(方法描述符),在方法描述符里就包含了module 信息。

复制代码
 1 0:000> !ip2md 013e08d1
 2 MethodDesc:   00f14d58
 3 Method Name:  Example_8_1_1.Program.Main(System.String[])
 4 Class:        00f11290
 5 MethodTable:  00f14d78
 6 mdToken:      06000001
 7 Module:       00f14044
 8 IsJitted:     yes
 9 CodeAddr:     013e0848
10 Transparency: Critical
11 Source file:  E:\Visual Studio 2022\...\Example_8_1_1\Program.cs
复制代码

             Module: 00f14044 这个信息就是模块信息,我们就可以使用【!savemodule】命令,保存文件了。

1 0:000> !savemodule 00f14044 F:\Test\SaveDump\test.dll
2 3 sections in file
3 section 0 - VA=2000, VASize=818, FileAddr=200, FileSize=a00
4 section 1 - VA=4000, VASize=5f0, FileAddr=c00, FileSize=600
5 section 2 - VA=6000, VASize=c, FileAddr=1200, FileSize=200

            文件就保存下来了,截图效果:
            

            有了这个文件,我们就可以使用 ILSpy 反编译工具查看源码了,很简单,就不写了。


        2.5、如何查看 CLR 版本和 GC 模式。
            测试源码:Example_8_1_1

1 0:000> !eeversion
2 4.8.4300.0 retail
3 Workstation mode(工作站模式,一般指:Winform,WPF,Console等桌面应用)
4 SOS Version: 4.8.4300.0 retail build

            还有一种方式查看 GC 模式,我们可以查看 托管堆的个数,一个堆的就是工作站模式,其他就是服务器模式。

复制代码
 1 0:000> !eeheap -gc
 2 Number of GC Heaps: 1(这里就表示是工作站模式)
 3 generation 0 starts at 0x02d91018
 4 generation 1 starts at 0x02d9100c
 5 generation 2 starts at 0x02d91000
 6 ephemeral segment allocation context: none
 7  segment     begin  allocated      size
 8 02d90000  02d91000  02d95ff4  0x4ff4(20468)
 9 Large object heap starts at 0x03d91000
10  segment     begin  allocated      size
11 03d90000  03d91000  03d95558  0x4558(17752)
12 Total Size:              Size: 0x954c (38220) bytes.
13 ------------------------------
14 GC Heap Size:    Size: 0x954c (38220) bytes.
复制代码

 

        2.6、查看线程栈对象。
            测试源码:Example_8_1_1

复制代码
 1 0:000> !dso
 2 OS Thread Id: 0x3438 (0)
 3 ESP/REG  Object   Name
 4 00CFEFB0 02d94d64 Microsoft.Win32.SafeHandles.SafeFileHandle
 5 00CFEFD4 02d94d64 Microsoft.Win32.SafeHandles.SafeFileHandle
 6 00CFEFE0 02d94d64 Microsoft.Win32.SafeHandles.SafeFileHandle
 7 00CFF008 02d94d64 Microsoft.Win32.SafeHandles.SafeFileHandle
 8 00CFF030 02d94da0 System.IO.StreamReader
 9 00CFF034 02d94da0 System.IO.StreamReader
10 00CFF04C 02d95124 System.IO.TextReader+SyncTextReader
11 00CFF068 02d95124 System.IO.TextReader+SyncTextReader
12 00CFF078 02d924bc System.String[]
13 00CFF090 02d924e4 System.Int32
14 00CFF094 02d924c8 System.String    sum={0}
15 00CFF09C 02d93a4c System.String    sum=21
16 00CFF0A0 02d924e4 System.Int32
17 00CFF0AC 02d924bc System.String[]
18 00CFF124 02d924bc System.String[]
19 00CFF260 02d924bc System.String[]
20 00CFF28C 02d924bc System.String[]
21 00CFF358 02d9141c System.AppDomain
22 00CFF674 03d92338 System.Object[]    (System.Object[])
23 00CFF6B8 03d92338 System.Object[]    (System.Object[])
24 00CFF6BC 03d92338 System.Object[]    (System.Object[])
25 00CFF7CC 02d91238 System.SharedStatics
复制代码


        2.7、如何查看对象的引用根。
            测试源码:Example_8_1_2
            第一步,我们先在托管堆中查找 Byte[] 数组对象,执行命令【!dumpheap -type Byte[]】,代码如下:

 View Code

            如果想看代码详情,点开看吧,这里就不展示了。
            数据大小为 100012 的项目,一种有 100 项,因为我们 For 循环了一百次,为什么大小不是 100000,而是 100012 ,因为每个对象都有两个附加成员。有了成员列表,我们可以在这个列表中,任意选择一个项,在 Address 列,选一个地址,针对这个地址,我们使用【!gcroot】命令,就能看到我们想要的结果。
            我们选择这个列表中最后一项,地址是:042e71a8

复制代码
1 0:006> !gcroot 042e71a8
2 HandleTable:
3     00bc13ec (pinned handle)(static 底层是标记了 pinned,也就是这个 handle 持有 System.Object[] 数组)
4     -> 03973568 System.Object[](这个对象是 CLR 创建的,持有这个 List 对象。)
5     -> 029724c8 System.Collections.Generic.List`1[[System.Byte[], mscorlib]](这个就是我们声明的 list 类型,底层是一个 Byte 二维数组)
6     -> 02972740 System.Byte[][](这个数组持有 042e71a8 这个元素)
7     -> 042e71a8 System.Byte[](这个是我选择的最后一个元素值)
8 
9 Found 1 unique roots (run '!GCRoot -all' to see all roots).
复制代码

如果您发现该资源为电子书等存在侵权的资源或对该资源描述不正确等,可点击“私信”按钮向作者进行反馈;如作者无回复可进行平台仲裁,我们会在第一时间进行处理!

评价 0 条
风晓L1
粉丝 1 资源 2038 + 关注 私信
最近热门资源
银河麒麟桌面操作系统备份用户数据  130
统信桌面专业版【全盘安装UOS系统】介绍  128
银河麒麟桌面操作系统安装佳能打印机驱动方法  120
银河麒麟桌面操作系统 V10-SP1用户密码修改  108
麒麟系统连接打印机常见问题及解决方法  27
最近下载排行榜
银河麒麟桌面操作系统备份用户数据 0
统信桌面专业版【全盘安装UOS系统】介绍 0
银河麒麟桌面操作系统安装佳能打印机驱动方法 0
银河麒麟桌面操作系统 V10-SP1用户密码修改 0
麒麟系统连接打印机常见问题及解决方法 0
作者收入月榜
1

prtyaa 收益393.62元

2

zlj141319 收益218元

3

1843880570 收益214.2元

4

IT-feng 收益210.13元

5

风晓 收益208.24元

6

777 收益172.71元

7

Fhawking 收益106.6元

8

信创来了 收益105.84元

9

克里斯蒂亚诺诺 收益91.08元

10

技术-小陈 收益79.5元

请使用微信扫码

加入交流群

请使用微信扫一扫!