|
一、简介
这是我的《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、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 }
复制代码 2.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 }
复制代码 2.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 }
复制代码 2.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 }
复制代码 2.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 }
复制代码 三、基础知识和眼见为实
3.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】,如图:
打开选择进程的窗口,在右侧。
之后就是进入调试器界面,使用方法就一样了。
3.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
复制代码 【.symfix】命令后面可以跟一个参数,就是本地路径,用来缓存下载的符号文件,就不用调试器每次去下载相同的符号了,可以直接从本地加载。在默认情况下,如果没有指定本地路径缓存,那么调试器将使用调试软件包安装的路径下的 sym 文件夹。
3.3、控制调试目标的执行
在任何调试会话中,能够控制调试目标的执行是非常重要的。我们可以设置断点,然后恢复程序的执行知道断点处,在此可以查看应用程序的状态,单步跟踪到函数内部,然后在恢复执行等。
3.3.1、中断执行
调试器中断程序的方式有很多种,我举三种最常用的中断执行的方式。
1)、如果我们使用的命令行调试器,可以使用【ctrol+c】组合键手动方式中断调试目标的执行,例如:调试死锁问题。
2)、给我们的应用程序设置断点来中断调试目标的执行。通过设置断点,可以很方便的使调试器在执行流程的任意位置上中断执行。
3)、抛出异常可以使调试器中断执行。
3.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】命令,恢复执行,调试器输出如图:
2)、禁用初始和退出断点
调试源码: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】,调试器在初始会中断执行。如图:
如果没有使用 -G 命令开关,我们还需要输入命令才能退出。
3.3.3、单步调试代码
我们使用过 VS IDE 的调试功能,快捷键有:F10,F11,F9等,调试器也为我们提供了类似的命令。但是,需要注意:如果在调试托管代码时使用非托管调试器,那么通常是对 JIT 编译器产生的机器代码进行单步调试。
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、眼见为实
这一节的演示,使用【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 <strong>00007ff8213f197f</strong> 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
复制代码 这个命令也不复杂,大家慢慢体会吧。
3.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】,按回车,如图:
效果很明显,不用多说了。
3.4、加载托管代码调试的扩展命令
3.4.1、加载 SOS 调试器扩展
A、知识介绍
SOS 调试器的扩展 DLL 与程序使用的 CLR 版本是是相关的。因此,在发布每个 CLR 主版本的同时,都会发布一个新版本的 SOS 调试扩展。以确保这个 DLL 可以使用该版本 CLR 的新功能。SOS 扩展作为运行时的一部分发布的,它的路径位于:%systemRoot%Microsoft.Net\Framework\\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
复制代码 3.4.2、加载 SOSEX 调试器扩展
这个调试器扩展很好用,但是也很可惜,它只支持 Net Framework 版本。在最新的 .Net 版本是抛弃的,不能使用了。如果想查看调试过程,可以查看我的另外一个系列【Net 高级调试】中有一篇文章,地址:https://www.cnblogs.com/PatrickLiu/p/17788840.html
3.5、控制 CLR 的调试
如果我们想在托管代码调试的过程总输出各种信息(例如:SOS 命令的输出),我们可以加载一个辅助 DLL,称为:mscordacwks.dll。加载【mscordacwks.dll】的路径取决于倍加再到进程中【mscorwks.dll】的路径。在【实时调试】中一般没有问题,两个 dll 版本是一致的。如果是【事后调试】,可能会出现版本不一致的情况,我们可以使用元命令:cordll 来解决。
比如:.cordll -lp c:\x\y\z,这样就能告诉调试器从 c:\x\y\z 目录下加载 mscordacwks.dll。
3.6、设置断点
设置断点的目的就是为了告诉目标程序在执行到了断点处停止执行。断点可以使用开发人员分析程序在执行流中的状态,并且找到出现问题的根本原因。在非托管代码中设置断点很容易,因为我们知道了代码的位置,于是就可以使用【bp】命令在代码位置处设置断点了。
3.6.1、在非托管代码中设置断点。
这次我们使用 notepad.exe 做这个调试,因为它是非托管程序,代码已经编译成机器码了,代码地址有了,我们就可以使用【bp】命令直接设置断点了。
眼见为实:
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 <strong>00007ff6`510ee780</strong> <strong>notepad!SaveFile</strong> (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 才可以正常使用,文件也保存成功。
2)、使用【Windbg Preview】调试。
我们先打开一个 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 <strong>00007ff6`510ee780 notepad!SaveFile</strong> (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 保存成功。
3.6.2、在 JIT 编译的托管函数上下断点
A、知识介绍
非托管方法设置断点很容易,因为代码都已经被编译了,代码的地址就是已知的。但是,托管代码要进行两次编译才能运行。我们想要给代码设置断点,必须先找到代码的位置。这一节我们讨论已经编译的函数如何设置断点,既然已经编译了,说明代码的地址就是可以直接找到的,设置断点就很容易了。
JIT 编译器编译了一个函数并将其放在内存中。如果我们知道了 JIT 编译器保存机器代码的位置,我们就可以使用调试器命令【bp】设置断点了。
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 <strong>JITTED Code Address: 00007ffccc1c1a90</strong>
复制代码 红色标注的说明代码已经编译了,地址是:00007ffccc1c1a90,如果不信,我们可以使用【u】命令确认一下。- 1 0:002> !U 00007ffccc1c1a90
- 2 <strong>Normal JIT generated code
- </strong> 3 <strong>ExampleCore_3_1_4.BreakPoint.AddAndPrint(Int32, Int32)
- </strong> 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 <strong>Normal JIT generated code (说明是 JIT 生成的)
- </strong> 3 <strong>ExampleCore_3_1_4.BreakPoint.AddAndPrint(Int32, Int32)(方法的名称,说明我们获取的地址是对的)
- </strong> 4 ilAddr is 00000217571620AC pImport is 00000214BF4B0480
- 5 <strong>Begin 00007FFCCCC81C40, size b7(代码的开始地址和生成代码的大小)
- </strong> 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 <strong>ExampleCore_3_1_4!ExampleCore_3_1_4.BreakPoint.AddAndPrint:
- </strong>6 00007ffc`ccc81c40 55 push rbp
复制代码 我们进入 AddAndPrint 方法的断点出了,我们就可以使用【p】或者【t】命令就行调试了。
3.6.3、在还没有被 JIT 编译的托管函数上下断点
A、知识介绍
非托管方法设置断点很容易,因为代码都已经被编译了,代码的地址就是已知的。但是,托管代码要进行两次编译才能运行。我们想要给代码设置断点,必须先找到代码的位置。这一节我们讨论在未编译的函数上如何设置断点,我们就不能使用【bp】命令,需要使用另外一个命令【bpmd】,它能自动找出被 JIT 编译后代码正确地址,并且,可以仅根据完整的方法名来设置断点。
【bpmd】命令可以用来在还没有被 JIT编译的代码上设置断点,它设置的是一个延迟断点,设置断点时位置是未知的,只有在将来某个事件发生时,才会真正的设置断点。【bpmd】命令是通过注册内部的 CLR JIT 编译通知来实现延迟断点的。当调试器收到 JIT 编译通知时,它会检查这个通知是否和现有的某一个延迟断点相关,如果相关,那么就会在函数执行之前就会使断点生效。而且,【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 <strong>Unable to insert breakpoint 0 at 00007ffc`cdf51a90, Win32 error 0n998
- </strong> 6 "内存位置访问无效。"
- 7 <strong>The breakpoint was set with BP. If you want breakpoints
- </strong> 8 <strong>to track module load/unload state you must use BU.
- </strong> 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: <strong>00007ffccdff9398(这个就是方法描述符)
- </strong>6 Name: ExampleCore_3_1_4.BreakPoint.AddAndPrint(Int32, Int32)
- 7 <strong>Not JITTED yet</strong>. 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): <strong>CLR notification exception - code e0444143 (first chance)
- </strong>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 <strong>ExampleCore_3_1_4!</strong><strong>ExampleCore_3_1_4.BreakPoint.AddAndPrint</strong>:
- 7 00007ffc`cdf41c40 55 push rbp
复制代码 需要注意的是 notification 部分的输出,调试器已经接受到了 CLR 通知异常(e0444143),JIT 编译方法的时候,重新设置了断点在地址:00007FFCCDF41C40,并且成功在断点处中断执行。
3.6.4、在预编译的程序集中设置断点
.NET 代码也需要在进程的上下文中执行。JIT 编译器将程序集的 IL 代码编译为机器代码,每当 .NET 代码访问同一段代码时,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/
3.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 <strong>Assembly: 000001ba465506e0 [E:\Visual Studio 2022\...\ExampleCore_3_1_5\bin\Debug\net8.0\ExampleCore_3_1_5.dll]
- </strong>22 ClassLoader: 000001BA46550FC0
- 23 <strong> Module
- </strong>24 <strong>00007ffdc912e0a0</strong> 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 】命令。- 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 <strong>00007ffdc91593e0</strong> <strong>0x02000003</strong> <strong>ExampleCore_3_1_5.MyList`</strong><strong>1</strong>
- 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 <strong>ExampleCore_3_1_5.MyList`1.Add(!0)</strong>
复制代码 红色标记就是我们要查找的 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 <strong>Assembly: 000001a1484c2bf0 [E:\Visual Studio 2022\...\ExampleCore_3_1_5\bin\Debug\net8.0\ExampleCore_3_1_5.dll]
- </strong>22 ClassLoader: 000001A1484C2C80
- 23 <strong> Module
- </strong>24 <strong>00007ffdc281e0a0 E:\Visual Studio 2022\Source\...\ExampleCore_3_1_5\bin\Debug\net8.0\ExampleCore_3_1_5.dll
- </strong>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 <strong>00007ffdc28493e0 0x02000003</strong> <strong>ExampleCore_3_1_5.MyList`</strong>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 <strong>ExampleCore_3_1_5.MyList`1.Add(!0)</strong>
复制代码
红色标记就是我们要查找的 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 <strong>ExampleCore_3_1_5!ExampleCore_3_1_5.MyList<int>.Add:
- </strong>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 <strong>ExampleCore_3_1_5!ExampleCore_3_1_5.MyList<int>.Add:
- </strong>7 00007ffd`c2d31a50 55 push rbp
复制代码 断点设置成功,我们也完成我们的任务。
四、总结
这篇文章终于写完了,是这篇文章的“上”篇写完了,“下”篇还没有开始呢,这篇文章写作周期也不短,内容实在多。Net 高级调试这条路,也刚刚起步,还有很多要学的地方。皇天不负有心人,努力,不辜负自己,我相信付出就有回报,再者说,学习的过程,有时候,虽然很痛苦,但是,学有所成,学有所懂,这个开心的感觉还是不可言喻的。不忘初心,继续努力。做自己喜欢做的,开心就好。
来源:https://www.cnblogs.com/PatrickLiu/p/18036453
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作! |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
x
|