这是我的《Advanced .Net Debugging》这个系列的第三篇文章。这个系列的每篇文章写的周期都要很长,因为每篇文章都是原书的一章内容(太长的就会分开写)。再者说,原书写的有点早,有些内容还是需要修正的,调试每个案例,这都是需要时间的。今天这篇文章的标题虽然叫做“基本调试任务”,但是这章的内容还是挺多的。我本来想用一篇文章把这个章节写完,我发现是不可能的,于是就分“上“和”下”用两篇来写。既然,我们要调试我们的 .Net 应用程序,那必须掌握一些调试技巧、方法和工具。我们习惯了使用 Visual Studio IDE 的调试技巧,比如:单步调试、下断点、过程调试等,但是,有些时候,VS 是使用不了的。那我们也必须学习如何使用 Windbg 的命令,在没有 VS IDE 的情况下,如何调试我们的程序,如何设置断点、恢复执行、中断执行、退出调试回话,如何为 JIT 编译的方法设置断点,如何为没有被 JIT 编译的方法设置断点,为泛型方法设置断点等等。如果我们想成为一名合格程序员,这些调试技巧都是必须要掌握的。
如果在没有说明的情况下,所有代码的测试环境都是 Net 8.0,如果有变动,我会在项目章节里进行说明。好了,废话不多说,开始我们今天的调试工作。
调试环境我需要进行说明,以防大家不清楚,具体情况我已经罗列出来。
操作系统:Windows Professional 10
调试工具:Windbg Preview(Debugger Client:1.2306.1401.0,Debugger engine:10.0.25877.1004)和 NTSD(10.0.22621.2428 AMD64)
下载地址:可以去Microsoft Store 去下载
开发工具:Microsoft Visual Studio Community 2022 (64 位) - Current版本 17.8.3
Net 版本:.Net 8.0
CoreCLR源码:源码下载
说明一下,这个系列内容安排有些变动,我把基础知识和眼见为实放在了一起,讲什么内容,立刻就将讲的内容做一个眼见为实验证,这样做更便于大家理解,我认为这样会更好一些,不用在文章里来回跑了。
命令行调试器要想成功使用,必须先安装 MSVC,想要了解详情,可以去微软的官网:https://learn.microsoft.com/zh-cn/cpp/build/building-on-the-command-line?view=msvc-170,如果我们使用的 Visual Studio 2022,本身也有命令行工具,我们就可以直接使用。安装如图:
二、目录结构
为了让大家看的更清楚,也为了自己方便查找,我做了一个目录结构,可以直观的查看文章的布局、内容,可以有针对性查看。
2.1、调试器以及调试目标
A、基础知识
B、眼见为实
1)、使用 NTSD 调试器
2)、使用 NTSD 附加进程
3)、使用 TList.exe 显示进程 Id
4)、使用 Windbg 调试
2.2、符号
A、基础知识
B、眼见为实
2.3、控制调试目标的执行
2.3.1、中断执行
2.3.2、恢复执行
A、基础知识
B、眼见为实
2.3.3、单步调试代码
A、基础知识
B、眼见为实
2.3.4、退出调试回话
A、基础知识
B、眼见为实
2.4、加载托管代码的调试扩展的命令
2.4.1、加载 SOS 调试器扩展
A、基础知识
B、眼见为实
2.4.2、加载 SOSEX 调试器扩展
2.5、控制 CLR 的调试
2.6、设置断点
2.6.1、在非托管代码中设置断点
A、基础知识
B、眼见为实
1)、使用【NTSD】调试
2)、使用【Windbg Preview】调试
2.6.2、在 JIT 编译的函数上设置断点
A、基础知识
B、眼见为实
1)、使用【NTSD】调试
2)、使用【Windbg Preview】调试
2.6.3、在还没有被 JIT 编译的函数上设置断点
A、基础知识
B、眼见为实
1)、使用【NTSD】调试
2)、使用【Windbg Preview】调试
2.6.4、在预编译的程序集上设置断点
2.6.5、在泛型方法上设置断点
A、基础知识
B、眼见为实
1)、使用【NTSD】调试
2)、使用【Windbg Preview】调试
三、调试源码
废话不多说,本节是调试的源码部分,没有代码,当然就谈不上测试了,调试必须有载体。
3.1、ExampleCore_3_1_1
1 namespace ExampleCore_3_1_1 2 { 3 internal class Program 4 { 5 static void Main(string[] args) 6 { 7 Console.WriteLine("Welcome to Advanced .Net Debugging!"); 8 Console.Read(); 9 } 10 } 11 }
3.2、ExampleCore_3_1_2
1 using System.Diagnostics; 2 3 namespace ExampleCore_3_1_2 4 { 5 internal class Program 6 { 7 static void Main(string[] args) 8 { 9 Console.WriteLine("第一次执行,并开始中断执行!"); 10 Debugger.Break(); 11 Console.WriteLine("第二次执行,并开始中断执行!"); 12 Debugger.Break(); 13 Console.WriteLine("第三次执行,并开始中断执行!"); 14 Debugger.Break(); 15 16 Console.WriteLine("恢复执行调试完毕!"); 17 Console.ReadLine(); 18 } 19 } 20 }
3.3、ExampleCore_3_1_3
1 using System.Diagnostics; 2 3 namespace ExampleCore_3_1_3 4 { 5 internal class Program 6 { 7 static void Main(string[] args) 8 { 9 Sum1(10); 10 Debugger.Break(); 11 12 int i = 10; 13 int j = 20; 14 15 var sum = Sum1(i); 16 Console.WriteLine($"sum={sum},i={i},j={j}"); 17 18 Console.ReadLine(); 19 } 20 21 private static int Sum1(int a) 22 { 23 var i = a; 24 var j = 11; 25 int sum = Sum2(i, j); 26 27 return sum; 28 } 29 30 private static int Sum2(int a, int b) 31 { 32 var i = a; 33 var j = b; 34 var k = 13; 35 36 var sum = Sum3(i, j, k); 37 return sum; 38 } 39 40 private static int Sum3(int i, int j, int k) 41 { 42 return i + j + k; 43 } 44 } 45 }
3.4、ExampleCore_3_1_4
1 namespace ExampleCore_3_1_4 2 { 3 internal class Program 4 { 5 static void Main(string[] args) 6 { 7 //第一次调用函数 8 Console.WriteLine("Press any key(1st instance function)"); 9 Console.ReadKey(); 10 BreakPoint bp = new BreakPoint(); 11 bp.AddAndPrint(10, 5); 12 13 //第二次调用函数 14 Console.WriteLine("Press any key(2nd instance function)"); 15 Console.ReadKey(); 16 bp = new BreakPoint(); 17 bp.AddAndPrint(100, 50); 18 } 19 } 20 21 internal class BreakPoint 22 { 23 public void AddAndPrint(int a, int b) 24 { 25 int res = a + b; 26 Console.WriteLine("Adding {0}+{1}={2}", a, b, res); 27 } 28 } 29 }
3.5、ExampleCore_3_1_5
1 using System.Diagnostics; 2 3 namespace ExampleCore_3_1_5 4 { 5 internal class Program 6 { 7 static void Main(string[] args) 8 { 9 Debugger.Break(); 10 11 var mylist = new MyList<int>(); 12 13 mylist.Add(10); 14 15 Console.ReadLine(); 16 } 17 } 18 19 public class MyList<T> 20 { 21 public T[] arr = new T[10]; 22 23 public void Add(T t) 24 { 25 arr[0] = t; 26 } 27 } 28 }
四、基础知识
在这一段内容中,有的小节可能会包含两个部分,分别是 A 和 B,也有可能只包含 A,如果只包含 A 部分,A 字母会省略。A 是【基础知识】,讲解必要的知识点,B 是【眼见为实】,通过调试证明讲解的知识点。
4.1、调试器以及调试目标
A、知识介绍
在任何调试中都包含两个组件:调试器和调试目标。
调试器:是一个引擎,我们必须通过这个引擎和调试目标进行交互。所有与调试目标之间的交互操作(如:设置断点、观察状态等),都可以通过调试器的命令完成,而调试器将在调试目标的环境中执行这些命令。
调试目标:一般指我们编写的程序,对于 .Net程序员来说就是,或者是要调试的程序。
它们之间的关系,有一张图可以更好的表现他们之间的关系。如图:
B、眼见为实
在【眼见为实】这个章节里,有些调试动作是一样的,我就不每个节点都写了。我就写在这里了。首先编译好自己的项目,根据自己的喜好,可以切换到编译项目目录下,也可以直接输入项目所在目录,接着就可以进行项目调试了。我使用的命令行工具是【Developer Command Prompt for VS 2022】。
1)、使用NTSD调试器
调试源码:ExampleCore_3_1_1
我们命令行工具中输入命令:ntsd,打开新窗口。效果如图:
NTSD 的新窗口。
如果没有指定任何参数,只能显示一组可用的选项。我们将我们的项目完整路径和项目名称作为输入参数。执行【ntsd E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_1\bin\Debug\net8.0\ExampleCore_3_1_1.exe】,弹出新窗口,如图:
新的 NTSD 窗口,如图:
上面这个截图分三个部分:第一部分是符号文件搜索路径,如图:
第二部分:加载的所有模块,表示应用程序所需要的模块都已经加载完毕。如图:
第三部分:中断指令异常。每当调试器启动一个进程或者调试器附加到一个进程的时候,调试器都会注入一个中断指令,这条指令将使调试目标停止运行。断点指令的作用:使用户与调试器和调试目标进行交互。这里是 int 3 中断。如图:
调试器的命令提示符是:X:Y>,X 表示当前正在被调试的活动目标(在大多数调试器中,这个值为 0),Y 表示导致调试器中断的线程 ID。如图:
2)、使用 NTSD 附加进程
调试源码:ExampleCore_3_1_1
当在调试器下启动有问题的引用程序的时候,这种调试方式很有作用。如果是引用程序已经运行起来了,那我们该如何调试呢?我们可以通过给【ntsd】命令,加上 -p 命令,就可以附加进程了。比如:你写的一个 Web 服务已经成功运行起来了。随着时间的推移,这个 Web 服务开始表现出一些怪怪的行为,你希望当程序具有这总奇怪行为的时候对它进行调试,附加调试就可以大展拳脚了。
-p 参数告诉调试器希望调试一个正在运行的进程。对于这个参数后,再跟上要调试进程的 Id 就可以了。
执行命令【ntsd -p 14624】,如图:
打开新的调试器窗口,如图:
下面很有很多内容,就不显示了。
3)、使用 TList.exe 显示进程 Id。
调试源码:ExampleCore_3_1_1
如果我们想获取一个进程的 id,可以有很多方法,我们可以使用 Windows 调试工具集中的 tlist.exe,tlist.exe 会输出所有运行的进程名称和 ID。启动我们的 ExampleCore_3_1_1.exe,在控制台输出:Welcome to Advanced .Net Debugging!,我们打开命令工具【Developer Command Prompt for vs 2022】,输出命令 tlist,显示如下:
我们进程的信息如下:
4)、使用 Windbg 调试
调试源码:ExampleCore_3_1_1
编译好我们的项目,打开【Windbg Preview】调试器。依次点击【文件】--->【Launch executable】加载我们的项目文件:ExampleCore_3_1_1.exe,选择【打开】按钮,成功加载并进入调试器界面。
点击【文件】按钮,切换界面。
点击【Launch executable】按钮打开新窗口。如图:
进入调试器界面,如图:
我们就可以在下方的命令框中输入命令,调试程序了。如图:
以上是在调试器中启动有问题的引用程序的流程,如果想使用【Windbg Preview】附加进程该怎么办呢?其实,也很简单,编译项目,打开调试器,依次点击【文件】--->【Attach to process】,如图:
打开选择进程的窗口,在右侧。
之后就是进入调试器界面,使用方法就一样了。
4.2、符号
A、知识介绍
符号文件:是一种辅助数据,它包含了对引用程序代码的一些标注信息,这些信息在调试过程中非常有用。如果没有这些辅助数据,那获得的信息只有引用程序的二进制文件了。对二进制代码进行调试是非常困难的,因为你无法看到代码中的函数名、数据结构名等。符号文件的扩展名通常是 .pdb。
在符号文件中包含很多重要的信息,例如:行号和局部变量的名称等,它能极大的提高调试的效率。
符号文件有两种类型:私有符号文件(Private)和公有符号文件(Public)。
私有符号文件:是大多数开发人员在日常工作中使用的符号文件,其中包含了调试会话中所需要的所有符号信息。私有符号文件一般是和我们编译的程序存放在一起的,调试器在开始调试的时候,会自动加载他们。如图:
公有符号文件:这类型的符号文件只是有选择的包含了一些符号信息,这会使调试工作困难一点。比如:在 Microsoft 符号服务器上存储了一些公有符号文件。每当将调试器指向 Microsoft 符号服务器时,都可以下载这些符号文件,并在调试会话中使用它们。
之所以有【私有符号文件】和【公有符号文件】之分,主要是为了保护知识产权。私有符号中包含大量底层技术信息,就很容易对应用程序进行逆向工程。公有符号就不存在这样的问题,既可以调试,又不会泄露核心技术信息。
B、眼见为实
.sympath(+) 命令的使用,加号表示不会替换,而是追加。
调试源码:ExampleCore_3_1_1
编译好我们的项目,打开【Windbg Preview】工具,依次打开【文件】--->【Launch executable】,加载我们的项目文件:ExampleCore_3_1_1.exe,进入调试器。直接执行【.sympath】命令,就可以看到当前的符号文件信息。
1 0:000> .sympath 2 Symbol search path is: srv* 3 Expanded Symbol search path is: cache*;SRV*https://msdl.microsoft.com/download/symbols 4 5 ************* Path validation summary ************** 6 Response Time (ms) Location 7 Deferred srv*
【.sympath】命令可以设置符号文件路径。在命令后跟上具体的符号文件所在的地址。
1 0:007> .sympath E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_1\bin\Debug\net8.0\ 2 Symbol search path is: E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_1\bin\Debug\net8.0\ 3 Expanded Symbol search path is: e:\visual studio 2022\source\projects\advanceddebug.netframework.test\examplecore_3_1_1\bin\debug\net8.0\ 4 5 ************* Path validation summary ************** 6 Response Time (ms) Location 7 OK E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_1\bin\Debug\net8.0\
虽然,我们设置新的符号文件的路径,但是并不会从这个路径中加载任何符号。如果想要加载符号信息,必须执行【.reload】命令。我们还是设置回去吧,防止以后有错误。
1 0:007> .sympath srv* 2 Symbol search path is: srv* 3 Expanded Symbol search path is: cache*;SRV*https://msdl.microsoft.com/download/symbols 4 5 ************* Path validation summary ************** 6 Response Time (ms) Location 7 Deferred srv*
如果使用【.sympath】命令设置错了符号文件地址,使用【.reload】也无法加载符号文件,我们可以使用【.symfix】命令,修复问题就可以了。
1 0:007> .symfix 2 DBGHELP: Symbol Search Path: cache*;SRV*https://msdl.microsoft.com/download/symbols 3 SYMSRV: BYINDEX: 0x17 4 C:\ProgramData\Dbg\sym 5 ntdll.pdb 6 63E12347526A46144B98F8CF61CDED791 7 SYMSRV: PATH: C:\ProgramData\Dbg\sym\ntdll.pdb\63E12347526A46144B98F8CF61CDED791\ntdll.pdb 8 SYMSRV: RESULT: 0x00000000 9 DBGHELP: ntdll - public symbols 10 C:\ProgramData\Dbg\sym\ntdll.pdb\63E12347526A46144B98F8CF61CDED791\ntdll.pdb 11 SYMSRV: BYINDEX: 0x18 12 C:\ProgramData\Dbg\sym 13 kernel32.pdb 14 85A257DB4B7B82F2E19AD96AB7BB116A1 15 SYMSRV: PATH: C:\ProgramData\Dbg\sym\kernel32.pdb\85A257DB4B7B82F2E19AD96AB7BB116A1\kernel32.pdb 16 SYMSRV: RESULT: 0x00000000 17 DBGHELP: KERNEL32 - public symbols 18 C:\ProgramData\Dbg\sym\kernel32.pdb\85A257DB4B7B82F2E19AD96AB7BB116A1\kernel32.pdb
这里有这么多输出,是因为我执行了【!sym noisy】命令,开启了显示符号加载的详细信息,如果不想显示,可以使用【!sym quiet 】命令。
1 0:007> !sym quiet 2 quiet mode - symbol prompts on 3 0:007> .symfix
4.3、控制调试目标的执行
在任何调试会话中,能够控制调试目标的执行是非常重要的。我们可以设置断点,然后恢复程序的执行知道断点处,在此可以查看应用程序的状态,单步跟踪到函数内部,然后在恢复执行等。
4.3.1、中断执行
调试器中断程序的方式有很多种,我举三种最常用的中断执行的方式。
1)、如果我们使用的命令行调试器,可以使用【ctrl+c】组合键手动方式中断调试目标的执行,例如:调试死锁问题。
2)、给我们的应用程序设置断点来中断调试目标的执行。通过设置断点,可以很方便的使调试器在执行流程的任意位置上中断执行。
3)、抛出异常可以使调试器中断执行。
4.3.2、恢复执行
A、知识介绍
当调试器中断执行时(可能是触发了断点或者其他的事件),可以使用【g】命令回复调试器的执行。如果【g】命令不带任何参数,只是回复调试目标的执行,直到下一次发生某个调试事件。
B、眼见为实
1)、使用【g】命令恢复执行。
调试源码:ExampleCore_3_1_2
编译好我们的项目,打开我们的命令工具,输入命令【ntsd E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_2\bin\Debug\net8.0\ExampleCore_3_1_2.exe】,打开调试器新窗口。
打开新的调试器窗口,如图:
触发初始断点是调试器的默认行为,也是调试人员开始分析应用程序的最早时机。此时,调试目标会停止执行并等待输入命令。此刻,我们可以输入【g】命令,恢复执行,调试器输出如图:
调试源码:ExampleCore_3_1_2
如果不希望调试器在初始启动时停止程序的执行,可以在启动调试器时使用 -g 命令开关,每当调试器退出时,调试器也将停止执行,可以使用 -G(大写)命令开关,避免在进程结束时触发最终的断点。
编译好我们的项目,打开我们的命令行工具,使用【ntsd -g E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_2\bin\Debug\net8.0\ExampleCore_3_1_2.exe】命令,启动调试器。如图:
打开新的调试器窗口,这次已经输出“第一次执行,并开始中断执行!”,如图:
如果我们使用 -G 命令开关,执行命令【ntsd -G E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_2\bin\Debug\net8.0\ExampleCore_3_1_2.exe】,调试器在初始会中断执行。如图:
4.3.3、单步调试代码
我们使用过 VS IDE 的调试功能,快捷键有:F10,F11,F9等,调试器也为我们提供了类似的命令。但是,需要注意:如果在调试托管代码时使用非托管调试器,那么通常是对 JIT 编译器产生的机器代码进行单步调试。在少数情况下,例如当需要分析 CLR 本身的代码时,才可能需要对常规非托管代码进行单步调试。有两个主要的命令可用于单步调试代码,分别是命令 p(step)和 t(trace)。
A、知识介绍
1)、p 命令
p(step):命令其实就是 VS 中的 f10 快捷键,单步执行,遇到函数也是当成一条指令执行,不会进入函数体。
2)、t 命令
t(trace):命令其实就是 VS 的 f11 快捷键,它是一种进入函数的单步执行调试。
3)、pc 命令
pc(Step to Next Call) 就是一直运行直到遇到 call 为止,不会进入函数体,call 是一个函数调用,汇编指令。
4)、tc 命令
tc(Trace to Next Call) 和 pc 不同的是,tc 会进入方法体,直到遇到 call 为止。
5)、pt 命令
pt(Step to Next Return) 如果有方法会进入方法内部递归处理,遇到下一个 ret 为止。
6)、tt 命令
tt(Trace to Next Return) 会进入函数体直到遇到 ret 为止。递归的意思。
B、眼见为实
调试源码:ExampleCore_3_1_3
调试任务:使用【bp】命令设置断点。
这一节的演示,使用【Windbg Preview】,我没有使用的是【NSTD】调试器,其他是一样的,只不过一个是有界面的,一个是没界面的。有些操作是一样的,我就写在这里了。
编译好我们的项目,打开【Windbg Preview】,依次点击【文件】--->【Launch executable】,加载我们的项目文件:ExampleCore_3_1_3.exe,进入调试器。界面的内容太多,我们可以使用【.cls】命令,清空调试器的界面。我们再使用【g】命令,继续运行调试器,我们现在查看一下托管代码的调用栈,执行命令【!clrstack】。
0:000> g ModLoad: 00007ff9`454c0000 00007ff9`454f0000 C:\Windows\System32\IMM32.DLL ModLoad: 00007ff8`8d8e0000 00007ff8`8d938000 C:\Program Files\dotnet\host\fxr\8.0.0\hostfxr.dll ModLoad: 00007ff8`812d0000 00007ff8`81334000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.0\hostpolicy.dll ModLoad: 00007ff8`80de0000 00007ff8`812cb000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.0\coreclr.dll ModLoad: 00007ff9`454f0000 00007ff9`45619000 C:\Windows\System32\ole32.dll ModLoad: 00007ff9`44b10000 00007ff9`44e64000 C:\Windows\System32\combase.dll ModLoad: 00007ff9`45d60000 00007ff9`45e35000 C:\Windows\System32\OLEAUT32.dll ModLoad: 00007ff9`444f0000 00007ff9`4456f000 C:\Windows\System32\bcryptPrimitives.dll (3994.1028): Unknown exception - code 04242420 (first chance) ModLoad: 00007ff8`7fb10000 00007ff8`807a8000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.0\System.Private.CoreLib.dll ModLoad: 00007ff8`7f950000 00007ff8`7fb08000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.0\clrjit.dll ModLoad: 00007ff9`44120000 00007ff9`44133000 C:\Windows\System32\kernel.appcore.dll ModLoad: 0000021a`398c0000 0000021a`398c8000 E:\Visual Studio 2022\.\ExampleCore_3_1_3\bin\Debug\net8.0\ExampleCore_3_1_3.dll ModLoad: 0000021a`398d0000 0000021a`398de000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.0\System.Runtime.dll ModLoad: 00007ff8`7f920000 00007ff8`7f948000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.0\System.Console.dll (3994.1028): Break instruction exception - code 80000003 (first chance) KERNELBASE!wil::details::DebugBreak+0x2: 00007ff9`44799202 cc int 3 0:000> !clrstack OS Thread Id: 0x1028 (0) Child SP IP Call Site 00000020B9B7E6A8 00007ff944799202 [HelperMethodFrame: 00000020b9b7e6a8] System.Diagnostics.Debugger.BreakInternal() 00000020B9B7E7B0 00007ff87ff360aa System.Diagnostics.Debugger.Break() [/_/src/coreclr/./Debugger.cs @ 18] 00000020B9B7E7E0 00007ff8213f197f ExampleCore_3_1_3.Program.Main(System.String[]) [E:\Visual Studio 2022\.\ExampleCore_3_1_3\Program.cs @ 10]
我们找到了红色标注的【Program.Main()】方法的地址:00007ff8213f197f,有了这个地址,我们就可以对这个地址下一个断点。
1 0:000> bp 00007ff8213f197f
设置好断点后,我们就可以使用【g】命令,继续运行调试器。
1)、p、pc、pt 命令的使用
调试源码:ExampleCore_3_1_3
我们设置好了断点,就可以开始我们的调试工作了。继续使用【g】运行调试器,调试器会在【Debugger.Break()】这行代码暂停,效果如图:
我们可以使用【p】命令,单步执行,到了第15行代码,会直接跳过而执行,不会进入方法。当然,这个过程要执行多次【p】命令。
1 0:000> p 2 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Main+0x50: 3 00007ff8`213f1980 c745fc0a000000 mov dword ptr [rbp-4],0Ah ss:00000020`b9b7e84c=00000000 4 0:000> p 5 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Main+0x57: 6 00007ff8`213f1987 c745f814000000 mov dword ptr [rbp-8],14h ss:00000020`b9b7e848=00000000 7 0:000> p 8 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Main+0x5e: 9 00007ff8`213f198e 8b4dfc mov ecx,dword ptr [rbp-4] ss:00000020`b9b7e84c=0000000a 10 0:000> p 11 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Main+0x61: 12 00007ff8`213f1991 ff1531520a00 call qword ptr [00007ff8`21496bc8] ds:00007ff8`21496bc8=00007ff8213f1a20 13 0:000> p 14 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Main+0x67: 15 00007ff8`213f1997 8945c0 mov dword ptr [rbp-40h],eax ss:00000020`b9b7e810=00000000 16 0:000> p 17 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Main+0x6a: 18 00007ff8`213f199a 8b4dc0 mov ecx,dword ptr [rbp-40h] ss:00000020`b9b7e810=00000022 19 0:000> p 20 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Main+0x6d: 21 00007ff8`213f199d 894df4 mov dword ptr [rbp-0Ch],ecx ss:00000020`b9b7e844=00000000 22 0:000> p 23 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Main+0x70: 24 00007ff8`213f19a0 488d4dc8 lea rcx,[rbp-38h]
【pc】命令调试:
前面的操作一样,查看堆栈,设置断点,开始运行,到断点出暂停。
【pc】命令很简单,我们直接输入【pc】,代码直接会运行到【var sum = Sum1(i)】,如图:
中间的代码是直接跳过的。
【pt】命令调试:前面的操作一样,查看堆栈,设置断点,开始运行,到断点出暂停。
我又增加了一些断点,断点如图:
接着如图:
执行【pt】命令的过程如下,执行【g】命令,到【Debugger.Break()】这样代码处中断执行。
1 0:000> g 2 Breakpoint 0 hit 3 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Main+0x4f: 4 00007ff8`2101197f 90 nop
效果如图:
继续执行【pt】命令,会在【var sum = Sum1(i)】这行带出中断执行。
1 0:000> g 2 Breakpoint 1 hit 3 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Main+0x5e: 4 00007ff8`20af198e 8b4dfc mov ecx,dword ptr [rbp-4] ss:000000ef`fb17e6ac=0000000a
执行效果如图:
继续执行【pt】命令,会进入【Sum1()】方法内部,在断点处中断执行。
1 0:000> pt 2 Breakpoint 2 hit 3 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Sum1+0x24: 4 00007ff8`20af1a84 90 nop
执行效果如图:
继续执行【pt】命令,会到【int sum = Sum2(i, j)】这行代码中断执行。
1 0:000> pt 2 Breakpoint 8 hit 3 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Sum1+0x32: 4 00007ff8`20af1a92 8b4dfc mov ecx,dword ptr [rbp-4] ss:000000ef`fb17e62c=0000000a
执行效果如图:
继续执行【pt】命令,会进入【Sum2()】方法内部。
1 0:000> pt 2 Breakpoint 4 hit 3 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Sum2+0x2c: 4 00007ff8`20af1afc 90 nop
执行效果如图:
继续执行【pt】命令,会到【var sum=Sum3(i,j,k)】这行代码处中断执行。
1 0:000> pt 2 Breakpoint 5 hit 3 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Sum2+0x40: 4 00007ff8`20af1b10 8b4dfc mov ecx,dword ptr [rbp-4] ss:000000ef`fb17e5dc=0000000a
执行效果如图:
继续执行【pt】命令,会进入【Sum3()】方法内部。
1 0:000> pt 2 Breakpoint 13 hit 3 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Sum3+0x27: 4 00007ff8`20af1b77 90 nop
执行效果如图:
继续执行【pt】命令,执行到43行代码处中断执行。
1 0:000> pt 2 Breakpoint 7 hit 3 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Sum3+0x35: 4 00007ff8`20af1b85 8b45fc mov eax,dword ptr [rbp-4] ss:000000ef`fb17e58c=00000022
执行效果如图:
最后我们执行一个【pt】命令,也就是【Sum3()】方法结束,遇到【ret】,调试器中断执行。
1 0:000> pt 2 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Sum3+0x3d: 3 00007ff8`20af1b8d c3 ret
这里我增加了很多断点,是为了测试是否会进入方法内部。执行【pt】命令,如果有方法调用会进入方法内部,知道遇到【ret】为止。
2)、t、tc、tt 命令的使用
调试源码:ExampleCore_3_1_3
【t】命令使用:
我们进入调试器,设置断点,使用【g】命令运行调试器。调试器会在 Program.Main() 方法的【Debugger.Break()】这行代码中断执行。
1 0:000> g 2 Breakpoint 0 hit 3 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Main+0x4f: 4 00007ff8`20dd197f 90 nop
执行效果如图:
继续运行【t】命令,单步执行,遇到【Sum1(i)】方法,就会进入方法内部进行单步调试。
【t】命令很简单,就像 VS 的 F11快捷键一样,按一下执行一条命令。
【tc】命令使用:
当我们在调试器中使用【bp】命令设置好断点后,就可以看是测试命令了。
1 0:000> g 2 Breakpoint 0 hit 3 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Main+0x4f: 4 00007ff8`20e0197f 90 nop
执行【g】命令,调试器会在 Program.Main() 方法的【Debugger.Break()】这行代码出中断执行。执行效果如图:
我们继续执行【tc】命令,它会到【var sum = Sum1(i)】这行代码处中断执行,因为调用 Sum1方法是通过【call】指令的。
1 0:000> tc 2 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Main+0x61: 3 00007ff8`20e01991 ff1531520a00 call qword ptr [00007ff8`20ea6bc8] ds:00007ff8`20ea6bc8=00007ff820e01a60
执行效果如图:
再次执行【tc】命令,调试器会在Sum1方法内的【int sum = Sum2(i, j)】这行代码处中断执行。
1 0:000> tc 2 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Sum1+0x38: 3 00007ff8`20e01a98 ff1542510a00 call qword ptr [00007ff8`20ea6be0] ds:00007ff8`20ea6be0=00007ff820e01ad0
执行效果如图:
就不继续了,下一个中断执行点是Sum2方法【var sum = Sum3(i, j, k)】这行代码,这个命令很简单。
【tt】命令使用:
当我们在调试器中设置到断点后,就可以开始调试了,测试我们的命令了。
我们使用【g】命令运行调试器,调试器会在Program.Main方法的【Debugger.Break()】这行代码处中断执行。
1 0:000> g 2 Breakpoint 0 hit 3 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Main+0x4f: 4 00007ff8`20e0197f 90 nop
执行效果如图:
继续执行【tt】命令,会进入Sum1方法内部。
1 0:000> tt 2 Breakpoint 2 hit 3 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Sum1+0x24: 4 00007ff8`20e01a84 90 nop
执行效果如图:
再次继续执行【tt】命令,会进入Sum2方法内部。
1 0:000> tt 2 Breakpoint 4 hit 3 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Sum2+0x2c: 4 00007ff8`20e01afc 90 nop
执行效果如图:
再次继续执行【tt】命令,会进入Sum3方法内部。
1 0:000> tt 2 Breakpoint 6 hit 3 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Sum3+0x27: 4 00007ff8`20e01b77 90 nop
执行效果如图:
当我们再次运行【tt】命令,调试器会在【43】行中断执行。再次执行【tt】命令,遇到Sum3方法的返回命令【ret】则为止。
1 0:000> tt 2 Breakpoint 7 hit 3 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Sum3+0x35: 4 00007ff8`20e01b85 8b45fc mov eax,dword ptr [rbp-4] ss:00000045`d7f7e52c=00000022 5 0:000> tt 6 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Sum3+0x3d: 7 00007ff8`20e01b8d c3 ret
这个命令也不复杂,大家慢慢体会吧。
4.3.4、退出调试回话
在执行完一个调试会话后,可以有很多方式退出调试回话,这里演示主要是以命令行调试器为主。
A、知识介绍
1)、q(quit):结束调试会话+调试程序退出
调试会话结束,应用程序也会退出。
2)、qd(quit and detach):结束调试会话+调试程序继续运行
调试会话结束,应用程序保持运行态,不会退出。
B、眼见为实
这里调试我使用的是【ntsd】,没有使用【Windbg Preview】,使用是一样的。
1)、q 命令退出
调试源码:ExampleCore_3_1_2
编译好我们的项目,执行命令【ntsd E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_2\bin\Debug\net8.0\ExampleCore_3_1_2.exe】加载调试器。如图:
开启新的调试器窗口,我们可以输入【q】命令,查看结果。如图:
按回车,调试器也关闭了,程序也关了。
2)、qd 命令退出
调试源码:ExampleCore_3_1_2
编译好我们的项目,通过命令【ntsd E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_2\bin\Debug\net8.0\ExampleCore_3_1_2.exe】加载调试器。如图:
打开新的调试器窗口,输入命令【qd】,按回车,如图:
效果很明显,不用多说了。
4.4、加载托管代码调试的扩展命令
4.4.1、加载 SOS 调试器扩展
A、知识介绍
SOS 调试器扩展 DLL 与程序使用的 CLR 版本是是相关的。因此,在发布每个 CLR 主版本的同时,都会发布一个新版本的 SOS 调试扩展。以确保这个 DLL 可以使用该版本 CLR 的新功能。SOS 扩展作为运行时的一部分发布的,它的路径位于:%systemRoot%Microsoft.Net\Framework\<framework version>\sos.dll。
在非托管调试器中可以使用两类命令,一类是:元命令,另一类是:扩展命令。
元命令:指在调试器引擎中内置的命令,当使用该命令的时候,必须在命令前加上英文点号。如:.cls。如果想要列出所有的元命令,可以使用【.help】命令。
扩展命令:指在调试器引擎之外的独立的 dll 中实现的,这些 DLL 也被称为调试器扩展。在使用扩展命令的时候,命令前面加上前缀“!”。如:!clrstack。
无论是【NTSD】还是【Windbg Preview】,现在会自动加载 SOS.DLL,以前的老版本需要使用【.load】加载 SOS.DLL。
B、眼见为实
使用【.load】加载 SOS.DLL
1 0:000> .load C:\Windows\Microsoft.NET\Framework64\v4.0.30319\sos.dll
4.4.2、加载 SOSEX 调试器扩展
这个调试器扩展很好用,但是也很可惜,它只支持 Net Framework 版本。在最新的 .Net 版本是抛弃的,不能使用了。如果想查看调试过程,可以查看我的另外一个系列【Net 高级调试】中有一篇文章,地址:https://www.cnblogs.com/PatrickLiu/p/17788840.html
4.5、控制 CLR 的调试
如果我们想在托管代码调试的过程总输出各种信息(例如:SOS 命令的输出),我们可以加载一个辅助 DLL,称为:mscordacwks.dll。加载【mscordacwks.dll】的路径取决于倍加再到进程中【mscorwks.dll】的路径。在【实时调试】中一般没有问题,两个 dll 版本是一致的。如果是【事后调试】,可能会出现版本不一致的情况,我们可以使用元命令:cordll 来解决。
比如:.cordll -lp c:\x\y\z,这样就能告诉调试器从 c:\x\y\z 目录下加载 mscordacwks.dll。
这节的内容太老了,没有太多的价值,大家可以直接忽略,因为 NET8.0 跨平台版本的 dll 已经发生了变化,但是命令还是可以使用的。
4.6、设置断点
设置断点的目的就是为了告诉目标程序在执行到了断点处停止执行。断点可以使用开发人员分析程序在执行流中的状态,并且找到出现问题的根本原因。在非托管代码中设置断点很容易,因为我们知道了代码的位置,于是就可以使用【bp】命令在代码位置处设置断点了。
4.6.1、在非托管代码中设置断点
A、基础知识
这次我们使用 notepad.exe 做这个调试,因为它是非托管程序,源码一次编译,就会生成可以执行的机器代码了,它的方法的地址就是已知的了,有了方法的地址,我们就可以使用【bp】命令直接设置断点了。
B、眼见为实
1)、使用【NTSD】调试
我们先打开【notepad.exe】应用程序,然后,执行【tlist】命令,获取 notepad 应用程序的 id,效果如图:
执行命令【ntsd -p 9580】,附加 notepad 应用程序的进程,打开调试器。
1 D:\Program Files\Microsoft Visual Studio\2022\Community>ntsd -p 9580
执行效果如图:
打开调试器窗口,如下:
这个时候,我们打开的 notepad 是不能操作的,因为调试器已经中断执行了。
执行【X notepad!*Save*】命令,查找 Notepad 的包含 Save 关键字的方法。
1 0:001> X notepad!*Save* 2 00007ff6`510e86a0 notepad!ShowOpenSaveDialog (void) 3 00007ff6`510ec5cc notepad!InitLegacyOpenSaveEncodingComboBox (void) 4 00007ff6`510ffd54 notepad!TraceFileSaveStart (void __cdecl TraceFileSaveStart(void)) 5 00007ff6`510f047c notepad!SaveGlobals (void __cdecl SaveGlobals(void)) 6 00007ff6`510f0314 notepad!RegGetIntSaveDefault (unsigned long __cdecl .....) 7 00007ff6`510ec7a0 notepad!NpLegacySaveDialogHookProc (unsigned __int64 __cdecl ....)) 8 00007ff6`511013a8 notepad!FileSaveDialog_GetSelectedEnterpriseId (FileSaveDialog_GetSelectedEnterpriseId) 9 00007ff6`510e8b8c notepad!InvokeLegacySaveDialog (long __cdecl InvokeLegacySaveDialog(unsigned short const *,...) 10 00007ff6`510fef58 notepad!RestartHandler::TryAutosaveOpenedDocument (public: bool __cdecl RestartHandler::TryAutosaveOpenedDocument(void)) 11 00007ff6`510ee780 notepad!SaveFile (bool __cdecl SaveFile(struct HWND__ *,.....)) 12 00007ff6`510ea2e8 notepad!CheckSave (int __cdecl CheckSave(void)) 13 00007ff6`511123bc notepad!fInSaveAsDlg = <no type information> 14 00007ff6`510ea124 notepad!CheckSaveTaskDlgBox (int __cdecl CheckSaveTaskDlgBox(unsigned short const *)) 15 00007ff6`510ffdd4 notepad!TraceFileSaveComplete (void __cdecl TraceFileSaveComplete(struct _NP_FileInfo *,int)) 16 00007ff6`510e8d90 notepad!InvokeSaveDialog (long __cdecl InvokeSaveDialog(struct HWND__ *,...)) 17 00007ff6`511105c0 notepad!szSaveCaption = <no type information> 18 00007ff6`5110508c notepad!_imp_load_GetSaveFileNameW (__imp_load_GetSaveFileNameW) 19 00007ff6`51111640 notepad!g_ftSaveAs = <no type information> 20 00007ff6`51107570 notepad!CLSID_FileSaveDialog = <no type information> 21 00007ff6`510ff258 notepad!RestartHandler::TryRestoreAutosavedDocument (public: bool __cdecl...) 22 00007ff6`511150b0 notepad!_imp_GetSaveFileNameW = <no type information>
代码中有些【...】这样的省略号,表示内容太长,省略了。
红色标注的就是我们找到了 notepad 保存功能的方法名称和地址。我们直接执行【bp notepad!SaveFile】命令或者【bp 00007ff6`510ea2e8】命令,都可以在 SaveFile 方法上下断点。1 0:001> bp notepad!SaveFile
下完断点后,我们【g】继续执行。但是,此时调试器的光标在闪动,我们打开的 notepad 窗口也可以使用了。效果如图:
我们在 notepad 窗口中随意写一些文字,点击【文件】-->【保存】,就会触发断点。效果如图:
此时的 notepad 应用程序的窗口是不能使用的,因为在断点出已经中断执行了。
我们继续使用【g】命令,继续调试器的运行,notepad 才可以正常使用,文件也保存成功。
我们先打开一个 notepad.exe 应用程序。然后再打开【Windbg Preview】,依次点击【文件】--->【Attach to process】,在窗口右侧【进程列表】框中选择 notepad 进程,点击【附加】,进入调试器。
我们使用【X notepad!*Save*】命令,查找 notepad 的保存数据的方法。
1 0:002> X notepad!*Save* 2 00007ff6`510e86a0 notepad!ShowOpenSaveDialog (void) 3 00007ff6`510ec5cc notepad!InitLegacyOpenSaveEncodingComboBox (void) 4 00007ff6`510ffd54 notepad!TraceFileSaveStart (void __cdecl TraceFileSaveStart(void)) 5 00007ff6`510f047c notepad!SaveGlobals (void __cdecl SaveGlobals(void)) 6 00007ff6`510f0314 notepad!RegGetIntSaveDefault (unsigned long __cdecl ...) 7 00007ff6`510ec7a0 notepad!NpLegacySaveDialogHookProc (unsigned __int64 __cdecl ...) 8 00007ff6`511013a8 notepad!FileSaveDialog_GetSelectedEnterpriseId (FileSaveDialog_GetSelectedEnterpriseId) 9 00007ff6`510e8b8c notepad!InvokeLegacySaveDialog (long __cdecl ...) 10 00007ff6`510fef58 notepad!RestartHandler::TryAutosaveOpenedDocument (public: bool __cdecl RestartHandler::TryAutosaveOpenedDocument(void)) 11 00007ff6`510ee780 notepad!SaveFile (bool __cdecl SaveFile(struct HWND__ *,class ...)) 12 00007ff6`510ea2e8 notepad!CheckSave (int __cdecl CheckSave(void)) 13 00007ff6`511123bc notepad!fInSaveAsDlg = <no type information> 14 00007ff6`510ea124 notepad!CheckSaveTaskDlgBox (int __cdecl CheckSaveTaskDlgBox(unsigned short const *)) 15 00007ff6`510ffdd4 notepad!TraceFileSaveComplete (void __cdecl TraceFileSaveComplete(struct _NP_FileInfo *,int)) 16 00007ff6`510e8d90 notepad!InvokeSaveDialog (long __cdecl InvokeSaveDialog(struct HWND__ *...)) 17 00007ff6`511105c0 notepad!szSaveCaption = <no type information> 18 00007ff6`5110508c notepad!_imp_load_GetSaveFileNameW (__imp_load_GetSaveFileNameW) 19 00007ff6`51111640 notepad!g_ftSaveAs = <no type information> 20 00007ff6`51107570 notepad!CLSID_FileSaveDialog = <no type information> 21 00007ff6`510ff258 notepad!RestartHandler::TryRestoreAutosavedDocument (public: bool __cdecl...) 22 00007ff6`511150b0 notepad!_imp_GetSaveFileNameW = <no type information>
红色标注的就是我们要找的方法名和地址。此时,notepad的窗口是不可以使用的。使用【bp 00007ff6`510ee780】命令下断点。
1 0:002> bp 00007ff6`510ee780
继续【g】,运行调试器,我们操作 notepad窗口,随意输入文字,然后点击【文件】--->【保存】,调试器运行,在 SaveFile 方法的断点出停止执行,notepad 窗口也不能使用了。
我们使用【g】命令,继续运行,notepad 保存成功。
4.6.2、在 JIT 编译的托管函数上下断点
A、知识介绍
非托管方法设置断点很容易,因为代码都已经被编译了,代码的地址就是已知的。但是,托管代码要进行两次编译才能运行。我们想要给代码设置断点,必须先找到代码的位置。这一节我们讨论已经编译的函数如何设置断点,既然已经编译了,说明代码的地址就是可以直接找到的,设置断点就很容易了。
JIT 编译器编译了一个函数并将其放在内存中。如果我们知道了 JIT 编译器保存机器代码的位置,我们就可以使用调试器命令【bp】设置断点了。
我们想要给一个函数下断点,必须先判断该方法是否已经编译,如果要判断方法是否已经编译,我们可以使用【!name2ee module_name item_name】或者【!name2ee module_name!item_name】命令查找我们要判断的方法。
module_name:需要分析的模块的名称,后缀名可以忽略。
item_name:表示要获取信息的类型名或者方法名,这个名称必须是完全限定名,就是要包含完整的命名空间。
B、眼见为实
调试任务:在第二次调用 AddAndPrint 方法的时候设置断点。为什么选择第二次,第一次已经执行过了,说明已经编译了。第二次就是使用编译的机器码。
1)、使用【NTSD】调试
调试源码:ExampleCore_3_1_4
执行【ntsd E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_4\bin\Debug\net8.0\ExampleCore_3_1_4.exe】命令,开启调试器。
打开【ntsd】调试器窗口。
我们使用【g】命令,运行调试器,直到调试器显示【Press any key(1st instance function)】暂停,等待输入。
我们按下任意键,程序继续执行,直到调试器输出【Press any key(2nd instance function)】。此时,我们按下【ctrl+c】进入调试器的中断模式。
现在,我们可以使用【!name2ee ExampleCore_3_1_4!ExampleCore_3_1_4.BreakPoint.AddAndPrint】命令找到方法的是否编译的信息。
1 0:002> !name2ee ExampleCore_3_1_4!ExampleCore_3_1_4.BreakPoint.AddAndPrint 2 Module: 00007ffccc24e0a0 3 Assembly: ExampleCore_3_1_4.dll 4 Token: 0000000006000003 5 MethodDesc: 00007ffccc279398 6 Name: ExampleCore_3_1_4.BreakPoint.AddAndPrint(Int32, Int32) 7 JITTED Code Address: 00007ffccc1c1a90
红色标注的说明代码已经编译了,地址是:00007ffccc1c1a90,如果不信,我们可以使用【u】命令确认一下。
1 0:002> !U 00007ffccc1c1a90 2 Normal JIT generated code 3 ExampleCore_3_1_4.BreakPoint.AddAndPrint(Int32, Int32) 4 ilAddr is 000002B7069720AC pImport is 000001A39F836140 5 Begin 00007FFCCC1C1A90, size b7 6 >>> 00007ffc`cc1c1a90 55 push rbp 7 00007ffc`cc1c1a91 4883ec40 sub rsp,40h 8 00007ffc`cc1c1a95 488d6c2440 lea rbp,[rsp+40h] 9 00007ffc`cc1c1a9a c5d857e4 vxorps xmm4,xmm4,xmm4 10 00007ffc`cc1c1a9e c5f97f65e0 vmovdqa xmmword ptr [rbp-20h],xmm4 11 00007ffc`cc1c1aa3 c5f97f65f0 vmovdqa xmmword ptr [rbp-10h],xmm4 12 00007ffc`cc1c1aa8 48894d10 mov qword ptr [rbp+10h],rcx 13 00007ffc`cc1c1aac 895518 mov dword ptr [rbp+18h],edx 14 00007ffc`cc1c1aaf 44894520 mov dword ptr [rbp+20h],r8d 15 00007ffc`cc1c1ab3 833d6ec8080000 cmp dword ptr [00007ffc`cc24e328],0 16 00007ffc`cc1c1aba 7405 je 00007ffc`cc1c1ac1 17 00007ffc`cc1c1abc e84fefc75f call coreclr!JIT_DbgIsJustMyCode (00007ffd`2be40a10) 18 00007ffc`cc1c1ac1 90 nop 19 00007ffc`cc1c1ac2 8b4d18 mov ecx,dword ptr [rbp+18h] 20 00007ffc`cc1c1ac5 034d20 add ecx,dword ptr [rbp+20h] 21 00007ffc`cc1c1ac8 894dfc mov dword ptr [rbp-4],ecx 22 00007ffc`cc1c1acb 48b9881113ccfc7f0000 mov rcx,7FFCCC131188h (MT: System.Int32) 23 00007ffc`cc1c1ad5 e8469ab55f call coreclr!JIT_TrialAllocSFastMP_InlineGetThread (00007ffd`2bd1b520) 24 00007ffc`cc1c1ada 488945f0 mov qword ptr [rbp-10h],rax 25 00007ffc`cc1c1ade 488b4df0 mov rcx,qword ptr [rbp-10h] 26 00007ffc`cc1c1ae2 8b4518 mov eax,dword ptr [rbp+18h] 27 00007ffc`cc1c1ae5 894108 mov dword ptr [rcx+8],eax 28 00007ffc`cc1c1ae8 48b9881113ccfc7f0000 mov rcx,7FFCCC131188h (MT: System.Int32) 29 00007ffc`cc1c1af2 e8299ab55f call coreclr!JIT_TrialAllocSFastMP_InlineGetThread (00007ffd`2bd1b520) 30 00007ffc`cc1c1af7 488945e8 mov qword ptr [rbp-18h],rax 31 00007ffc`cc1c1afb 488b4de8 mov rcx,qword ptr [rbp-18h] 32 00007ffc`cc1c1aff 8b4520 mov eax,dword ptr [rbp+20h] 33 00007ffc`cc1c1b02 894108 mov dword ptr [rcx+8],eax 34 00007ffc`cc1c1b05 48b9881113ccfc7f0000 mov rcx,7FFCCC131188h (MT: System.Int32) 35 00007ffc`cc1c1b0f e80c9ab55f call coreclr!JIT_TrialAllocSFastMP_InlineGetThread (00007ffd`2bd1b520) 36 00007ffc`cc1c1b14 488945e0 mov qword ptr [rbp-20h],rax 37 00007ffc`cc1c1b18 4c8b4de0 mov r9,qword ptr [rbp-20h] 38 00007ffc`cc1c1b1c 8b55fc mov edx,dword ptr [rbp-4] 39 00007ffc`cc1c1b1f 41895108 mov dword ptr [r9+8],edx 40 00007ffc`cc1c1b23 4c8b4de0 mov r9,qword ptr [rbp-20h] 41 00007ffc`cc1c1b27 488b55f0 mov rdx,qword ptr [rbp-10h] 42 00007ffc`cc1c1b2b 4c8b45e8 mov r8,qword ptr [rbp-18h] 43 00007ffc`cc1c1b2f 48b9880a8d9bf7020000 mov rcx,2F79B8D0A88h ("Adding {0}+{1}={2}") 44 00007ffc`cc1c1b39 ff15d12c0d00 call qword ptr [00007ffc`cc294810] 45 00007ffc`cc1c1b3f 90 nop 46 00007ffc`cc1c1b40 90 nop 47 00007ffc`cc1c1b41 4883c440 add rsp,40h 48 00007ffc`cc1c1b45 5d pop rbp 49 00007ffc`cc1c1b46 c3 ret
在反汇编代码的第一部分很清楚的表明方法的名称,并且是 JIT 生成的。第四行【Begin 00007FFCCC1C1A90, size b7】表示方法的起始地址和生成代码的大小。
设置断点,执行命令【 bp 00007ffccc1c1a90】。
1 0:002> bp 00007ffccc1c1a90
断点设置成功后,但是我在运行调试的时候出错,还没有找到原因和解决办法,如果有知道原因的,不吝赐教。
2)、使用【Windbg Preview】调试
调试源码:ExampleCore_3_1_4
编译好我们的项目,打开【Windbg Preview】,依次点击【文件】--->【Launch executable】,加载我们的项目文件:ExampleCore_3_1_4.exe,进入到调试器。
先执行【g】命令,运行调试器。等我们的控制台输出:Press any key(1st instance function),我们在按任意键继续。
控制台程序如图:
我们按【回车键】,如下:
这时,我们回到【Windbg Preview】调试器中,调试窗口是这样的,如图:
我们点击【Break】按钮,让调试器进入中断模式。
1 (39a0.944): Break instruction exception - code 80000003 (first chance) 2 ntdll!DbgBreakPoint: 3 00007ffd`f89ee880 cc int 3
我们使用【!name2ee ExampleCore_3_1_4!ExampleCore_3_1_4.BreakPoint.AddAndPrint】命令,查看方法的信息。
0:001> !name2ee ExampleCore_3_1_4!ExampleCore_3_1_4.BreakPoint.AddAndPrint Module: 00007ffcccd0e0a0 Assembly: ExampleCore_3_1_4.dll Token: 0000000006000003 MethodDesc: 00007ffcccd39398 Name: ExampleCore_3_1_4.BreakPoint.AddAndPrint(Int32, Int32) JITTED Code Address: 00007ffcccc81c40
JITTED 表示已经是编译过的,编译的地址是:00007ffcccc81c40。当然,我们可以使用【!U 00007ffcccc81c40】命令查看汇编代码。
1 0:001> !U 00007ffcccc81c40 2 Normal JIT generated code (说明是 JIT 生成的) 3 ExampleCore_3_1_4.BreakPoint.AddAndPrint(Int32, Int32)(方法的名称,说明我们获取的地址是对的) 4 ilAddr is 00000217571620AC pImport is 00000214BF4B0480 5 Begin 00007FFCCCC81C40, size b7(代码的开始地址和生成代码的大小) 6 7 E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_4\Program.cs @ 24: 8 >>> 00007ffc`ccc81c40 55 push rbp 9 00007ffc`ccc81c41 4883ec40 sub rsp,40h 10 00007ffc`ccc81c45 488d6c2440 lea rbp,[rsp+40h] 11 00007ffc`ccc81c4a c5d857e4 vxorps xmm4,xmm4,xmm4 12 00007ffc`ccc81c4e c5f97f65e0 vmovdqa xmmword ptr [rbp-20h],xmm4 13 00007ffc`ccc81c53 c5f97f65f0 vmovdqa xmmword ptr [rbp-10h],xmm4 14 00007ffc`ccc81c58 48894d10 mov qword ptr [rbp+10h],rcx 15 00007ffc`ccc81c5c 895518 mov dword ptr [rbp+18h],edx 16 00007ffc`ccc81c5f 44894520 mov dword ptr [rbp+20h],r8d 17 00007ffc`ccc81c63 833dbec6080000 cmp dword ptr [00007ffc`ccd0e328],0 18 00007ffc`ccc81c6a 7405 je 00007ffc`ccc81c71 19 00007ffc`ccc81c6c e89fedc95f call coreclr!JIT_DbgIsJustMyCode (00007ffd`2c920a10) 20 00007ffc`ccc81c71 90 nop 21 22 E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_4\Program.cs @ 25: 23 00007ffc`ccc81c72 8b4d18 mov ecx,dword ptr [rbp+18h] 24 00007ffc`ccc81c75 034d20 add ecx,dword ptr [rbp+20h] 25 00007ffc`ccc81c78 894dfc mov dword ptr [rbp-4],ecx 26 27 E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_4\Program.cs @ 26: 28 00007ffc`ccc81c7b 48b98811bfccfc7f0000 mov rcx,7FFCCCBF1188h (MT: System.Int32) 29 00007ffc`ccc81c85 e89698b75f call coreclr!JIT_TrialAllocSFastMP_InlineGetThread (00007ffd`2c7fb520) 30 00007ffc`ccc81c8a 488945f0 mov qword ptr [rbp-10h],rax 31 00007ffc`ccc81c8e 488b4df0 mov rcx,qword ptr [rbp-10h] 32 00007ffc`ccc81c92 8b4518 mov eax,dword ptr [rbp+18h] 33 00007ffc`ccc81c95 894108 mov dword ptr [rcx+8],eax 34 00007ffc`ccc81c98 48b98811bfccfc7f0000 mov rcx,7FFCCCBF1188h (MT: System.Int32) 35 00007ffc`ccc81ca2 e87998b75f call coreclr!JIT_TrialAllocSFastMP_InlineGetThread (00007ffd`2c7fb520) 36 00007ffc`ccc81ca7 488945e8 mov qword ptr [rbp-18h],rax 37 00007ffc`ccc81cab 488b4de8 mov rcx,qword ptr [rbp-18h] 38 00007ffc`ccc81caf 8b4520 mov eax,dword ptr [rbp+20h] 39 00007ffc`ccc81cb2 894108 mov dword ptr [rcx+8],eax 40 00007ffc`ccc81cb5 48b98811bfccfc7f0000 mov rcx,7FFCCCBF1188h (MT: System.Int32) 41 00007ffc`ccc81cbf e85c98b75f call coreclr!JIT_TrialAllocSFastMP_InlineGetThread (00007ffd`2c7fb520) 42 00007ffc`ccc81cc4 488945e0 mov qword ptr [rbp-20h],rax 43 00007ffc`ccc81cc8 4c8b4de0 mov r9,qword ptr [rbp-20h] 44 00007ffc`ccc81ccc 8b55fc mov edx,dword ptr [rbp-4] 45 00007ffc`ccc81ccf 41895108 mov dword ptr [r9+8],edx 46 00007ffc`ccc81cd3 4c8b4de0 mov r9,qword ptr [rbp-20h] 47 00007ffc`ccc81cd7 488b55f0 mov rdx,qword ptr [rbp-10h] 48 00007ffc`ccc81cdb 4c8b45e8 mov r8,qword ptr [rbp-18h] 49 00007ffc`ccc81cdf 48b9880a82ed57020000 mov rcx,257ED820A88h ("Adding {0}+{1}={2}") 50 00007ffc`ccc81ce9 ff15212b0d00 call qword ptr [00007ffc`ccd54810] 51 00007ffc`ccc81cef 90 nop 52 53 E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_4\Program.cs @ 27: 54 00007ffc`ccc81cf0 90 nop 55 00007ffc`ccc81cf1 4883c440 add rsp,40h 56 00007ffc`ccc81cf5 5d pop rbp 57 00007ffc`ccc81cf6 c3 ret
这里比【NTSD】好看的多,不多说了。
我们使用【bp 00007ffcccc81c40】命令,设置断点。
1 0:001> bp 00007ffcccc81c40 2 3 0:001> g 4 Breakpoint 0 hit 5 ExampleCore_3_1_4!ExampleCore_3_1_4.BreakPoint.AddAndPrint: 6 00007ffc`ccc81c40 55 push rbp
我们进入 AddAndPrint 方法的断点出了,我们就可以使用【p】或者【t】命令就行调试了。
4.6.3、在还没有被 JIT 编译的托管函数上下断点
A、知识介绍
非托管方法设置断点很容易,因为代码都已经被编译了,代码的地址就是已知的。但是,托管代码要进行两次编译才能运行。我们想要给代码设置断点,必须先找到代码的位置。这一节我们讨论在未编译的函数上如何设置断点,我们就不能使用【bp】命令,需要使用另外一个命令【bpmd(MethodDesc:方法描述符)】,它能自动找出被 JIT 编译后代码正确地址,并且,可以仅根据完整的方法名来设置断点。
【bpmd(MethodDesc:方法描述符)】命令可以用来在还没有被 JIT编译的代码上设置断点,它设置的是一个延迟断点,设置断点时位置是未知的,只有在将来某个事件发生时,才会真正的设置断点。【bpmd(MethodDesc:方法描述符)】命令是通过注册内部的 CLR JIT 编译通知来实现延迟断点的。当调试器收到 JIT 编译通知时,它会检查这个通知是否和现有的某一个延迟断点相关,如果相关,那么就会在函数执行之前就会使断点生效。而且,【bpmd(MethodDesc:方法描述符)】命令还会接受模块加载通知,这就意味着在设置断点时甚至可以不需要加载程序集。当程序集加载的时候,这个命令会再次得到通知,并检查是否有某个延迟断点位于这个模块中,如果有,便会激活这个断点。
最后需要注意一点,如果方法采用了 overload 来修饰,那么【bpmd】命令将在所有重载方法上设置断点。
B、眼见为实
1)、使用【NTSD】调试
调试源码:ExampleCore_3_1_4
调试任务:在 AddAndPrint 方法第一次执行前设置断点。
执行命令【D:\Program Files\Microsoft Visual Studio\2022\Community>ntsd E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_4\bin\Debug\net8.0\ExampleCore_3_1_4.exe】打开调试器窗口。
我们直接【g】运行调试器,看到调试器中输出:Press any key(1st instance function)
我们使用【ctrl+c】进入调试器中断模式。我们使用【!name2ee ExampleCore_3_1_4!ExampleCore_3_1_4.BreakPoint.AddAndPrint】命令,查看方法是否已经编译。
1 0:009> !name2ee ExampleCore_3_1_4!ExampleCore_3_1_4.BreakPoint.AddAndPrint 2 Module: 00007ffccdfde0a0 3 Assembly: ExampleCore_3_1_4.dll 4 Token: 0000000006000003 5 MethodDesc: 00007ffcce009398 6 Name: ExampleCore_3_1_4.BreakPoint.AddAndPrint(Int32, Int32) 7 Not JITTED yet. Use !bpmd -md 00007FFCCE009398 to break on run.
我们使用【!bpmd -md 00007FFCCE009398】命令设置断点。
1 0:009> !bpmd -md 00007FFCCE009398 2 MethodDesc = 00007FFCCE009398 3 Adding pending breakpoints...
我们断点设置成功。但是我在运行调试的时候出错,还没有找到原因和解决办法,如果有知道原因的,不吝赐教。
1 0:008> g 2 g(3308.f8): CLR notification exception - code e0444143 (first chance) 3 JITTED ExampleCore_3_1_4!ExampleCore_3_1_4.BreakPoint.AddAndPrint(Int32, Int32) 4 Setting breakpoint: bp 00007FFCCDF51A90 [ExampleCore_3_1_4.BreakPoint.AddAndPrint(Int32, Int32)] 5 Unable to insert breakpoint 0 at 00007ffc`cdf51a90, Win32 error 0n998 6 "内存位置访问无效。" 7 The breakpoint was set with BP. If you want breakpoints 8 to track module load/unload state you must use BU. 9 bp0 at 00007ffc`cdf51a90 failed 10 WaitForEvent failed, Win32 error 0n998 11 内存位置访问无效。 12 KERNELBASE!RaiseException+0x69: 13 00007ffd`f5fb3e49 0f1f440000 nop dword ptr [rax+rax]
2)、使用【Windbg Preview】调试
调试源码:ExampleCore_3_1_4
调试任务:在 AddAndPrint 方法第一次执行前设置断点。
编译好我们的项目,打开【Windbg Preview】,依次点击【文件】--->【Launch executable】,加载我们的项目文件:ExampleCore_3_1_4.exe,进入调试器。
我们使用【g】命令,继续运行,我们的控制台程序输出:Press any key(1st instance function),这是第一次输出,AddAndPrint 方法还没有执行,也就还没有编译。
我们点击【Break】按钮,中断执行,我们先证明 AddAndPrint 这个方法还没有编译,执行【!name2ee ExampleCore_3_1_4!ExampleCore_3_1_4.BreakPoint.AddAndPrint】命令。
1 0:001> !name2ee ExampleCore_3_1_4!ExampleCore_3_1_4.BreakPoint.AddAndPrint 2 Module: 00007ffccdfce0a0 3 Assembly: ExampleCore_3_1_4.dll 4 Token: 0000000006000003 5 MethodDesc: 00007ffccdff9398(这个就是方法描述符) 6 Name: ExampleCore_3_1_4.BreakPoint.AddAndPrint(Int32, Int32) 7 Not JITTED yet. Use !bpmd -md 00007FFCCDFF9398 to break on run.
Not JITTED yet:表示未编译。【Use !bpmd -md 00007FFCCDFF9398 to break on run.】这句话是说可以通过使用【bpmd】命令和方法描述符来设置一个断点。
1 0:001> !bpmd -md 00007ffccdff9398 2 MethodDesc = 00007FFCCDFF9398 3 Adding pending breakpoints...
当我们使用【g】命令继续执行,并在控制台应用程序中按下【回车键】,调试器输出如下:
1 0:001> g 2 (21e8.31c8): CLR notification exception - code e0444143 (first chance) 3 JITTED ExampleCore_3_1_4!ExampleCore_3_1_4.BreakPoint.AddAndPrint(Int32, Int32) 4 Setting breakpoint: bp 00007FFCCDF41C40 [ExampleCore_3_1_4.BreakPoint.AddAndPrint(Int32, Int32)] 5 Breakpoint 0 hit 6 ExampleCore_3_1_4!ExampleCore_3_1_4.BreakPoint.AddAndPrint: 7 00007ffc`cdf41c40 55 push rbp
需要注意的是 notification 部分的输出,调试器已经接受到了 CLR 通知异常(e0444143),JIT 编译方法的时候,重新设置了断点在地址:00007FFCCDF41C40,并且成功在断点处中断执行。
4.6.4、在预编译的程序集中设置断点
.NET 代码也需要在进程的上下文中执行。JIT 编译器将程序集的 IL 代码编译为机器代码,每当 CLR 访问同一段代码时,CLR 首先检查它是否已经被编译了,如果是,则重用已编译的代码。当然,当进程结束了,JIT 编译器生成的所有机器代码也会随之消失。当下一次需要执行程序集时,JIT 编译器再重新对相同的代码进行编译。
预编译程序集是与某个程序集对应的非托管映像,其中全部的代码已经全部被编译为机器代码。如果 CLR 需要执行这个程序集中的代码,并且这个程序集在机器上有一个非托管的映像,就会直接跳过 JIT 编译步骤,并直接从这个非托管映像中加载机器代码。
需要说明一点,这本书写的有点早,那个时候只有 .NET Framework 平台,NGEN 也是针对 .NET Framework 平台的。我这个系列是针对 .NET 8,也就是跨平台的版本,所以是不能直接使用 NGEN 生成预编译的程序集的。如果想生成跨平台的预编译程序集,需要使用 CrossGen。
NET 6 引入了 CrossGen2,它是已被删除的 CrossGen 的后继版本。 CrossGen 和 CrossGen2 是用于提供预先 (AOT) 编译的工具,可改进应用的启动时间。 CrossGen2 是用 C# (而不是 C++)编写的,可执行之前的版本无法实现的分析和优化。 如果想了解 CrossGen2,可以去微软官网:https://devblogs.microsoft.com/dotnet/conversation-about-crossgen2/
4.6.5、在泛型方法上设置断点
A、知识介绍
如果我们想对泛型类型的方法下断点,最首要的任务就是找到泛型类型的名称和方法的名称,找到之后,我们就可以下断点了。找泛型类型的名称和方法的名称有两种办法,第一种是通过命令,第二种是我们可以使用 ILSpy 找到。
B、眼见为实
我们想要在泛型类型的方法上下断点,首要的任务是找到泛型类型的名称和方法的名称,这是关键。
1)、使用【NTSD】调试
a、我们通过 Windbg 和 SOS 的命令找到类型的名称。
编译好我们的项目,打开【Visual Studio 2022 Developer Command Prompt】命令行工具,输入命令【NTSD E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_5\bin\Debug\net8.0\ExampleCore_3_1_5.exe】,打开调试器。
使用【g】命令,运行调试器。
1 0:000> g 2 ModLoad: 00007ffe`90a90000 00007ffe`90ac0000 C:\Windows\System32\IMM32.DLL 3 ModLoad: 00007ffe`4f1d0000 00007ffe`4f229000 C:\Program Files\dotnet\host\fxr\8.0.2\hostfxr.dll 4 ModLoad: 00007ffe`3b840000 00007ffe`3b8a4000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\hostpolicy.dll 5 ModLoad: 00007ffe`28ab0000 00007ffe`28f98000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\coreclr.dll 6 ModLoad: 00007ffe`914d0000 00007ffe`915f9000 C:\Windows\System32\ole32.dll 7 ModLoad: 00007ffe`91f30000 00007ffe`92284000 C:\Windows\System32\combase.dll 8 ModLoad: 00007ffe`918a0000 00007ffe`91975000 C:\Windows\System32\OLEAUT32.dll 9 ModLoad: 00007ffe`902c0000 00007ffe`9033f000 C:\Windows\System32\bcryptPrimitives.dll 10 (3b74.3fac): Unknown exception - code 04242420 (first chance) 11 ModLoad: 00007ffe`27be0000 00007ffe`2886c000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll 12 ModLoad: 00007ffe`27a20000 00007ffe`27bd9000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\clrjit.dll 13 ModLoad: 00007ffe`8f900000 00007ffe`8f913000 C:\Windows\System32\kernel.appcore.dll 14 ModLoad: 000001ba`47fc0000 000001ba`47fc8000 E:\Visual Studio 2022\...\ExampleCore_3_1_5\bin\Debug\net8.0\ExampleCore_3_1_5.dll 15 ModLoad: 000001ba`47fd0000 000001ba`47fde000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Runtime.dll 16 ModLoad: 00007ffe`75c60000 00007ffe`75c88000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Console.dll 17 (3b74.3fac): Break instruction exception - code 80000003 (first chance)
按【ctrl+c】组合键进入中断模式。
输入【!dumpdomain】命令查看应用程序域详情,该命令会列出每个应用程序域中加载的所有程序集和模块。
1 0:000> !dumpdomain 2 -------------------------------------- 3 System Domain: 00007ffe28f460d0 4 LowFrequencyHeap: 00007FFE28F465A8 5 HighFrequencyHeap: 00007FFE28F46638 6 StubHeap: 00007FFE28F466C8 7 Stage: OPEN 8 Name: None 9 -------------------------------------- 10 Domain 1: 000001ba465a1ff0 11 LowFrequencyHeap: 00007FFE28F465A8 12 HighFrequencyHeap: 00007FFE28F46638 13 StubHeap: 00007FFE28F466C8 14 Stage: OPEN 15 Name: clrhost 16 Assembly: 000001ba46564d20 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll] 17 ClassLoader: 000001BA46564DB0 18 Module 19 00007ffdc8f44000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll 20 21 Assembly: 000001ba465506e0 [E:\Visual Studio 2022\...\ExampleCore_3_1_5\bin\Debug\net8.0\ExampleCore_3_1_5.dll] 22 ClassLoader: 000001BA46550FC0 23 Module 24 00007ffdc912e0a0 E:\Visual Studio 2022\...\ExampleCore_3_1_5\bin\Debug\net8.0\ExampleCore_3_1_5.dll 25 26 Assembly: 000001ba465507e0 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Runtime.dll] 27 ClassLoader: 000001BA46550870 28 Module 29 00007ffdc912fbc8 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Runtime.dll 30 31 Assembly: 000001ba47f97460 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Console.dll] 32 ClassLoader: 000001BA47F97DE0 33 Module 34 00007ffdc91597f0 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Console.dll
我们找到了模块,就可以将模块中所有的类型输出来,可以使用【!dumpmodule -mt 00007ffdc912e0a0】命令。
1 0:000> !dumpmodule -mt 00007ffdc912e0a0 2 Name: E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_5\bin\Debug\net8.0\ExampleCore_3_1_5.dll 3 Attributes: PEFile 4 TransientFlags: 00209011 5 Assembly: 000001ba465506e0 6 BaseAddress: 000001BA47FC0000 7 PEFile: 000001BA4654FCD0 8 ModuleId: 00007FFDC912E458 9 ModuleIndex: 0000000000000001 10 LoaderHeap: 00007FFE28F46598 11 TypeDefToMethodTableMap: 00007FFDC9134320 12 TypeRefToMethodTableMap: 00007FFDC9134340 13 MethodDefToDescMap: 00007FFDC9134470 14 FieldDefToDescMap: 00007FFDC9134498 15 MemberRefToDescMap: 00007FFDC91343D0 16 FileReferencesMap: 0000000000000000 17 AssemblyReferencesMap: 00007FFDC91344B8 18 MetaData start address: 000001BA47FC20A8 (1612 bytes) 19 20 Types defined in this module 21 22 MT TypeDef Name 23 ------------------------------------------------------------------------------ 24 00007ffdc91500e8 0x02000002 ExampleCore_3_1_5.Program 25 00007ffdc91593e0 0x02000003 ExampleCore_3_1_5.MyList`1 26 27 Types referenced in this module 28 29 MT TypeRef Name 30 ------------------------------------------------------------------------------ 31 00007ffdc8fd5fa8 0x0200000d System.Object 32 00007ffdc9159700 0x02000010 System.Diagnostics.Debugger 33 00007ffdc915ab08 0x02000011 System.Console
红色标注的就是我们要查找泛型类型真实的名称。有了类型,我们继续可以使用【!dumpmt -md 00007ffdc91593e0】命令,输出它所有方法。
1 0:000> !dumpmt -md 00007ffdc91593e0 2 EEClass: 00007FFDC9161F48 3 Module: 00007FFDC912E0A0 4 Name: ExampleCore_3_1_5.MyList`1 5 mdToken: 0000000002000003 6 File: E:\Visual Studio 2022\...\ExampleCore_3_1_5\bin\Debug\net8.0\ExampleCore_3_1_5.dll 7 BaseSize: 0x18 8 ComponentSize: 0x0 9 DynamicStatics: false 10 ContainsPointers true 11 Slots in VTable: 6 12 Number of IFaces in IFaceMap: 0 13 -------------------------------------- 14 MethodDesc Table 15 Entry MethodDesc JIT Name 16 00007FFDC8FE0048 00007FFDC8FD5F38 NONE System.Object.Finalize() 17 00007FFDC8FE0060 00007FFDC8FD5F48 NONE System.Object.ToString() 18 00007FFDC8FE0078 00007FFDC8FD5F58 NONE System.Object.Equals(System.Object) 19 00007FFDC8FE00C0 00007FFDC8FD5F98 NONE System.Object.GetHashCode() 20 00007FFDC914B948 00007FFDC91593B8 NONE ExampleCore_3_1_5.MyList`1..ctor() 21 00007FFDC914B930 00007FFDC91593A8 NONE ExampleCore_3_1_5.MyList`1.Add(!0)
红色标记就是我们要查找的 Add 方法,有了方法的地址,我们就可以使用【!bpmd ExampleCore_3_1_5 ExampleCore_3_1_5.MyList`1.Add】命令为其下断点了。
1 0:000> !bpmd ExampleCore_3_1_5 ExampleCore_3_1_5.MyList`1.Add 2 MethodDesc = 00007FFDC91593A8 3 Adding pending breakpoints...
断点设置成功。但是我在运行调试的时候出错,还没有找到原因和解决办法,如果有知道原因的,不吝赐教。
1 0:000> g 2 (3b74.3fac): CLR notification exception - code e0444143 (first chance) 3 JITTED ExampleCore_3_1_5!ExampleCore_3_1_5.MyList`1[[System.Int32, System.Private.CoreLib]].Add(Int32) 4 Setting breakpoint: bp 00007FFDC90A1A50 [ExampleCore_3_1_5.MyList`1[[System.Int32, System.Private.CoreLib]].Add(Int32)] 5 Unable to insert breakpoint 0 at 00007ffd`c90a1a50, Win32 error 0n998 6 "内存位置访问无效。" 7 The breakpoint was set with BP. If you want breakpoints 8 to track module load/unload state you must use BU. 9 bp0 at 00007ffd`c90a1a50 failed 10 WaitForEvent failed, Win32 error 0n998 11 内存位置访问无效。 12 KERNELBASE!RaiseException+0x69: 13 00007ffe`8fb03e49 0f1f440000 nop dword ptr [rax+rax]
b、我们可以使用 ILSpy 或者 SnPay 来查找泛型类型的名称和方法的名称。
和使用【Windbg Preview】这节的内容一样。
2)、使用【Windbg Preview】调试
调试源码:ExampleCore_3_1_5
a、我们通过 Windbg 和 SOS 的命令找到类型的名称。
编译程序集后,泛型类型一定在这个程序集的模块中。然后我们再在这个模块中打印出所有的类型,就可以找到这个类型了。
编译好我们的项目,打开【Windbg Preview】,依次点击【文件】--->【Launch executable】,加载我们的项目文件:ExampleCore_3_1_5.exe。进入到调试器后,我们使用【g】命令运行调试器,调试器会在 Program 类型的 Main 方法的【Debugger.Break()】这个行代码中断执行。我们点击【break】按钮,进入调试模式。
我们现在这个程序集中查找模块信息,我们可以使用【!dumpdomain】命令。
1 0:000> !dumpdomain 2 -------------------------------------- 3 System Domain: 00007ffe226360d0 4 LowFrequencyHeap: 00007FFE226365A8 5 HighFrequencyHeap: 00007FFE22636638 6 StubHeap: 00007FFE226366C8 7 Stage: OPEN 8 Name: None 9 -------------------------------------- 10 Domain 1: 000001a1469ddc40 11 LowFrequencyHeap: 00007FFE226365A8 12 HighFrequencyHeap: 00007FFE22636638 13 StubHeap: 00007FFE226366C8 14 Stage: OPEN 15 Name: clrhost 16 Assembly: 000001a146a2e010 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll] 17 ClassLoader: 000001A146A2E0A0 18 Module 19 00007ffdc2634000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll 20 21 Assembly: 000001a1484c2bf0 [E:\Visual Studio 2022\...\ExampleCore_3_1_5\bin\Debug\net8.0\ExampleCore_3_1_5.dll] 22 ClassLoader: 000001A1484C2C80 23 Module 24 00007ffdc281e0a0 E:\Visual Studio 2022\Source\...\ExampleCore_3_1_5\bin\Debug\net8.0\ExampleCore_3_1_5.dll 25 26 Assembly: 000001a146976520 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Runtime.dll] 27 ClassLoader: 000001A1469765B0 28 Module 29 00007ffdc281fbc8 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Runtime.dll
00007ffdc281e0a0 这个地址就是我们程序集(ExampleCore_3_1_5.dll)的模块地址。我们找到了模块,就可以将模块中所有的类型输出来,可以使用【!dumpmodule -mt 】命令。
1 0:000> !dumpmodule -mt 00007ffdc281e0a0 2 Name: E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_5\bin\Debug\net8.0\ExampleCore_3_1_5.dll 3 Attributes: PEFile 4 TransientFlags: 00209011 5 Assembly: 000001a1484c2bf0 6 BaseAddress: 000001A1468F0000 7 PEAssembly: 000001A14696F6A0 8 ModuleId: 00007FFDC281E458 9 ModuleIndex: 0000000000000001 10 LoaderHeap: 00007FFE22636598 11 TypeDefToMethodTableMap: 00007FFDC2824320 12 TypeRefToMethodTableMap: 00007FFDC2824340 13 MethodDefToDescMap: 00007FFDC2824470 14 FieldDefToDescMap: 00007FFDC2824498 15 MemberRefToDescMap: 00007FFDC28243D0 16 FileReferencesMap: 0000000000000000 17 AssemblyReferencesMap: 00007FFDC28244B8 18 MetaData start address: 000001A1468F20A8 (1612 bytes) 19 20 Types defined in this module 21 22 MT TypeDef Name 23 ------------------------------------------------------------------------------ 24 00007ffdc28400e8 0x02000002 ExampleCore_3_1_5.Program 25 00007ffdc28493e0 0x02000003 ExampleCore_3_1_5.MyList`1 26 27 Types referenced in this module 28 29 MT TypeRef Name 30 ------------------------------------------------------------------------------ 31 00007ffdc26c5fa8 0x0200000d System.Object 32 00007ffdc2849700 0x02000010 System.Diagnostics.Debugger 33 00007ffdc284ab08 0x02000011 System.Console
ExampleCore_3_1_5.MyList`1 就是泛型类型编译后的名称。红色标注的就是我们要查找泛型类型真实的名称。有了类型,我们继续可以使用【!dumpmt -md 00007ffdc28493e0】命令,输出它所有方法。
1 0:000> !dumpmt -md 00007ffdc28493e0 2 EEClass: 00007ffdc2851f48 3 Module: 00007ffdc281e0a0 4 Name: ExampleCore_3_1_5.MyList`1 5 mdToken: 0000000002000003 6 File: E:\Visual Studio 2022\...\ExampleCore_3_1_5\bin\Debug\net8.0\ExampleCore_3_1_5.dll 7 AssemblyLoadContext: Default ALC - The managed instance of this context doesn't exist yet. 8 BaseSize: 0x18 9 ComponentSize: 0x0 10 DynamicStatics: false 11 ContainsPointers: true 12 Slots in VTable: 6 13 Number of IFaces in IFaceMap: 0 14 -------------------------------------- 15 MethodDesc Table 16 Entry MethodDesc JIT Name 17 00007FFDC26D0048 00007ffdc26c5f38 NONE System.Object.Finalize() 18 00007FFDC26D0060 00007ffdc26c5f48 NONE System.Object.ToString() 19 00007FFDC26D0078 00007ffdc26c5f58 NONE System.Object.Equals(System.Object) 20 00007FFDC26D00C0 00007ffdc26c5f98 NONE System.Object.GetHashCode() 21 00007FFDC283B948 00007ffdc28493b8 NONE ExampleCore_3_1_5.MyList`1..ctor() 22 00007FFDC283B930 00007ffdc28493a8 NONE ExampleCore_3_1_5.MyList`1.Add(!0)
红色标记就是我们要查找的 Add 方法,有了方法的地址,我们就可以使用【bpmd】命令为其下断点了。
1 0:000> !bpmd ExampleCore_3_1_5 ExampleCore_3_1_5.MyList`1.Add 2 MethodDesc = 00007FFDC28493A8 3 Adding pending breakpoints...
断点设置成功后,我们使用【g】命令,程序继续运行,就可以在断点处暂停。
1 0:000> g 2 (2c88.2734): CLR notification exception - code e0444143 (first chance) 3 JITTED ExampleCore_3_1_5!ExampleCore_3_1_5.MyList`1[[System.Int32, System.Private.CoreLib]].Add(Int32) 4 Setting breakpoint: bp 00007FFDC2791A50 [ExampleCore_3_1_5.MyList`1[[System.Int32, System.Private.CoreLib]].Add(Int32)] 5 Breakpoint 0 hit 6 ExampleCore_3_1_5!ExampleCore_3_1_5.MyList<int>.Add: 7 00007ffd`c2791a50 55 push rbp
断点效果如图:
b、我们可以使用 ILSpy 或者 SnPay 来查找泛型类型的名称和方法的名称。
我们可以使用 ILSpy 或者 snSpy 查看泛型类型和方法的名称。我们打开【ILSpy】工具,加载我们的 ExampleCore_3_1_5.dll 文件。再左侧,依次点击【Metadata】--->【Tables】--->【TypeDef】,在右侧就能看到这个程序集中定义的所有的类型名称。如图:
我们知道了类型的名称,然后就是查找方法的名称。也很简单。
现在我们知道了泛型类型的名称和方法的名称,就可以直接设置断点了。
编译好我们的项目,打开【Windbg Preview】,依次点击【文件】--->【Launch executable】,加载我们的项目文件:ExampleCore_3_1_5.exe。进入到调试器后,我们使用【g】命令运行调试器,调试器会在 Program 类型的 Main 方法的【Debugger.Break()】这个行代码中断执行。我们点击【break】按钮,进入调试模式。
我们执行命令【!bpmd ExampleCore_3_1_5 ExampleCore_3_1_5.MyList`1.Add】,就可以直接下断点了。
1 0:000> !bpmd ExampleCore_3_1_5 ExampleCore_3_1_5.MyList`1.Add 2 MethodDesc = 00007FFDC2DE93A8 3 Adding pending breakpoints...
断点设置成功后,我们使用【g】命令,程序继续运行,就可以在断点处暂停。
1 0:000> g 2 (10b4.42c4): CLR notification exception - code e0444143 (first chance) 3 JITTED ExampleCore_3_1_5!ExampleCore_3_1_5.MyList`1[[System.Int32, System.Private.CoreLib]].Add(Int32) 4 Setting breakpoint: bp 00007FFDC2D31A50 [ExampleCore_3_1_5.MyList`1[[System.Int32, System.Private.CoreLib]].Add(Int32)] 5 Breakpoint 0 hit 6 ExampleCore_3_1_5!ExampleCore_3_1_5.MyList<int>.Add: 7 00007ffd`c2d31a50 55 push rbp
断点设置成功,我们也完成我们的任务。
五、总结
这篇文章终于写完了,是这篇文章的“上”篇写完了,“下”篇还没有开始呢,这篇文章写作周期也不短,内容实在多。Net 高级调试这条路,也刚刚起步,还有很多要学的地方。皇天不负有心人,努力,不辜负自己,我相信付出就有回报,再者说,学习的过程,有时候,虽然很痛苦,但是,学有所成,学有所懂,这个开心的感觉还是不可言喻的。不忘初心,继续努力。做自己喜欢做的,开心就好。