二、基础知识
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
1.2、Example_8_1_2
2、眼见为实
项目的所有操作都是一样的,所以就在这里说明一下,但是每个测试例子,都需要重新启动,并加载相应的应用程序,加载方法都是一样的。流程如下:我们编译项目,打开 Windbg,点击【文件】----》【launch executable】附加程序,打开调试器的界面,程序已经处于中断状态。我们需要使用【g】命令,继续运行程序,然后到达指定地点停止后,我们可以点击【break】按钮,就可以调试程序了。有时候可能需要切换到主线程,可以使用【~0s】命令。
2.1、查看 clr!CallDescrWorkerInternal+0x34 处的汇编代码。
测试源码:Example_8_1_1
说明一下,在这里用其他的程序都是可以的,只是查看汇编代码,可以查看非托管函数的、也可以查看托管函数的汇编代码。
接下来,我们先找一个非托管函数的,看看它们的汇编代码,到底是哪个函数,是随机的。
我们使用【k】命令,查看一下非托管的调用栈,然后从里面随便找一个函数,看看它的汇编代码。
调用栈还是很多的,列表里有: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】查看一下。
代码太多,不变展示。>>> 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[]】,代码如下:
如果想看代码详情,点开看吧,这里就不展示了。
数据大小为 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).
如果您发现该资源为电子书等存在侵权的资源或对该资源描述不正确等,可点击“私信”按钮向作者进行反馈;如作者无回复可进行平台仲裁,我们会在第一时间进行处理!
加入交流群
请使用微信扫一扫!