Visual Studio调试指南---仅调试具有仅我的代码的用户代码
仅我的代码是一种 Visual Studio 调试功能,可自动执行对系统、框架和其他非用户代码的调用。 在 "调用堆栈" 窗口中,仅我的代码将这些调用折叠到 [外部代码] 帧中。在 .NET、 C++和 JavaScript 项目中,仅我的代码的工作方式有所不同。
启用或禁用“仅我的代码”
对于大多数编程语言,默认情况下启用仅我的代码。
- 若要在 Visual Studio 中启用或禁用仅我的代码,请在 "工具" > 选项"(或"调试 > 选项") >调试 > 常规",选择或取消选择 "启用仅我的代码"。
注意:启用仅我的代码是一项全局设置,适用于所有语言的所有 Visual Studio 项目。
“仅我的代码”调试
在调试会话期间,"模块" 窗口显示调试器将哪个代码模块视为我的代码(用户代码)以及其符号加载状态。
![模块 窗口中的用户代码](../debugger/media/dbg_justmycode_module.png ""模块" 窗口中的用户代码")
在 "调用堆栈" 或 "任务" 窗口中,仅我的代码将非用户代码折叠成标记为 [External Code]
的灰色批注代码框架。
若要查看折叠的 [外部代码] 帧中的代码,请在 "调用堆栈" 或 "任务" 窗口中单击右键,然后从上下文菜单中选择 "显示外部代码"。 展开的外部代码行替换 [外部代码] 框架。
在 "调用堆栈" 窗口中双击展开的外部代码行会在源代码中突出显示以绿色显示的调用代码行。 对于 Dll 或未找到或加载的其他模块,可能会打开 "符号或源找不到" 页。
.NET 仅我的代码
在 .NET 项目中,仅我的代码使用符号( .pdb)文件和程序优化来对用户和非用户代码进行分类。 .NET 调试器将优化的二进制文件和非加载 .pdb文件视为非用户代码。
这三个编译器属性还会影响 .NET 调试器认为是用户代码:
- DebuggerNonUserCodeAttribute 通知调试器,它应用到的代码不是用户代码。
- DebuggerHiddenAttribute 对调试器隐藏代码,即使“仅我的代码”关闭;
- DebuggerStepThroughAttribute 通知调试器遍历应用到的代码,而不是单步执行代码。
.NET 调试器将所有其他代码视为用户代码。
在 .NET 调试期间:
- 调试 > 单步执行(或按F11)在非用户代码上逐过程执行代码,并将代码移到用户代码的下一行。
- 调试 > 非用户代码上的 "跳出" (或Shift +F11)运行到用户代码的下一行。
如果没有更多的用户代码,调试将继续,直到它结束、到达另一个断点或引发错误。
如果调试器在非用户代码中中断(例如,在非用户代码中使用 "调试" > "全部中断" 和 "暂停"),则不会显示 "无源" 窗口。 然后,你可以使用 "调试 > 步骤" 命令来执行用户代码的下一行。
如果非用户代码中出现未经处理的异常,调试器将在生成异常的用户代码行处中断。
如果对异常启用了第一次机会异常,则调用用户代码行在源代码中以绿色突出显示。 "调用堆栈" 窗口显示标记为 [外部代码] 的带批注的帧。
C++“仅我的代码”
从 Visual Studio 2017 15.8 版开始,还支持代码单仅我的代码。 此功能还要求使用/JMC (仅我的代码调试)编译器开关。 默认情况下,在项目中C++启用此开关。 对于 "调用堆栈" 窗口和仅我的代码中的调用堆栈支持,不需要/JMC 开关。
若要归类为用户代码,必须由调试器加载包含用户代码的二进制文件的 PDB (使用 "模块" 窗口进行检查)。
对于调用堆栈行为(如 "调用堆栈" 窗口中的), C++中的仅我的代码仅将这些函数视为非用户代码:
- 在其符号文件中去除了源信息的函数。
- 符号文件指示没有对应于堆栈帧的源文件的函数。
- %VsInstallDirectory%\Common7\Packages\Debugger\Visualizers文件夹中 * natjmc文件中指定的函数。
对于代码单步执行行为, C++仅我的代码仅将这些函数视为非用户代码:
- 调试器中尚未加载相应的 PDB 文件的函数。
- %VsInstallDirectory%\Common7\Packages\Debugger\Visualizers文件夹中 * natjmc文件中指定的函数。
为了仅我的代码中的代码步进支持C++ ,必须在 Visual Studio 15.8 Preview 3 或更高版本中使用 MSVC 编译器来编译代码,并且必须启用/JMC 编译器开关(默认情况下启用)。 有对于使用较旧的编译器编译的代码, . natstepfilter files 是自定义代码单步执行的唯一方法,该方法与仅我的代码无关。
调试C++期间:
- 调试 > 单步执行(或按F11)在非用户代码上逐过程执行代码,并将代码移到用户代码的下一行。
- 调试 > 非用户代码上的 "跳出" (或Shift +F11)运行到用户代码的下一行。
如果没有更多的用户代码,调试将继续,直到它结束、到达另一个断点或引发错误。
如果调试器在非用户代码中中断(例如,在非用户代码中使用 "调试" > "全部中断" 和 "暂停"),则在非用户代码中继续执行。
如果调试器遇到异常,则它会在异常上停止,无论它是在用户代码还是非用户代码中。 "异常设置" 对话框中的用户未处理的选项将被忽略。
自C++定义调用堆栈和代码单步执行行为
对于C++项目,可以指定模块、源文件和函数,"调用堆栈" 窗口将其视为非用户代码,方法是在 * natjmc文件中指定它们。 如果使用的是最新的编译器,此自定义也适用于代码单步执行(请参阅 C++仅我的代码)。
- 若要为 Visual Studio 计算机的所有用户指定非用户代码,请将natjmc文件添加到 %VsInstallDirectory%\Common7\Packages\Debugger\Visualizers文件夹。
- 若要为单个用户指定非用户代码,请将natjmc文件添加到 %USERPROFILE%\My 文档 \ < Visual Studio 版本 > \visualizers文件夹中。
Natjmc文件是具有以下语法的 XML 文件:
Visual Studio 调试器指南---自定义调试视图
在 Visual Studio 调试器中创建数据的自定义视图C++/C#
调试器提供了许多用于检查和修改程序状态的工具。 这些工具中的大多数仅在中断模式下有效。
在变量窗口和数据提示中创建数据的自定义视图
许多调试器窗口(如 "自动" 和 "监视" 窗口)都允许您检查变量。 您可以自定义C++类型、托管对象以及您自己的类型在调试器变量窗口和数据提示中的显示方式。
创建自定义可视化工具
可视化工具使您能够以有意义的方式查看对象或变量的内容。 在 Visual Studio 调试器中,可视化工具是指可以使用放大镜图标打开的其他窗口。 例如,HTML 可视化工具显示 HTML 字符串如何在浏览器中进行解释和显示。 你可以通过数据提示、"监视" 窗口 、"自动" 窗口和 "局部变量" 窗口访问可视化工具。 "快速监视" 对话框还提供可视化工具。
自定义C++视图
使用 Natvis 框架在C++调试器中创建对象的自定义视图
Visual Studio Natvis 框架可以自定义本机类型在调试器变量窗口(例如局部变量、监视以及数据提示窗口)中显示的方式。 Natvis 的可视化功能可以让你创建的类型在调试期间更加直观清晰。
Natvis 替换了 Visual Studio 早期版本中的 autoexp.dat 文件,提供了 XML 语法、更好的诊断功能、版本控制功能以及多文件支持功能。
Natvis 可视化效果
你可以使用 Natvis 框架为自己创建的类型创建可视化规则,让开发人员在调试过程中更轻松地查看这些类型。
例如,下图显示的类型 Windows::UI::Xaml::Controls::TextBox 的变量在调试器窗口中未应用任何自定义可视化。
突出显示的行显示 Text
类的 TextBox
属性。 由于类的层次结构很复杂,因此很难找到这个属性。 调试器不知道如何解释自定义字符串类型,所以你看不到文本框中的字符串。
如果应用了 Natvis 自定义可视化工具规则,那么在变量窗口中,同样的TextBox
看起来就简单得多。 类的重要成员会显示在一起,并且调试器会显示自定义字符串类型的基础字符串值。
在 C++ 项目中使用 .natvis 文件
Natvis 使用Natvis文件来指定可视化规则。 Natvis文件是具有NATVIS扩展名的 XML 文件。 Natvis 架构在 %VSINSTALLDIR%\Xml\Schemas\natvis.xsd中定义。
.natvis 文件的基本结构由一个或多个代表可视化条目的 Type
元素构成。 每个 Type
元素的完全限定名称都在其 Name
属性中指定。
Visual Studio调试器指南---Natvis 语法参考
AutoVisualizer 元素
AutoVisualizer
元素是 .natvis 文件的根节点,并包含命名空间 xmlns:
属性。
Visual Studio调试器指南---多线程应用程序调试(一)
线程是操作系统向其授予处理器时间的指令序列。 在操作系统中运行的每个进程都包含至少一个线程。 包含多个线程的进程称为多线程。有多个处理器、多核处理器或超线程进程的计算机可以同时运行多个线程。 使用多个线程的并行处理可以极大地提高程序性能,但也可能导致调试变得更加困难,因为正在跟踪多个线程。
多线程处理可能会引入新类型的潜在 bug。 例如,两个或多个线程可能需要访问同一资源,但是一次只能有一个线程可以安全地访问该资源。 需要某种形式的互斥才能确保每次只有一个线程访问该资源。 如果未正确实现互斥,则可能会创建不会执行任何线程的死锁情况。 死锁通常是一个难以调试的问题。
用于调试多线程应用的工具
Visual Studio 提供不同的工具用于调试多线程应用程序。
对于线程,调试线程的主要工具有 "线程" 窗口、源窗口中的线程标记、"并行堆栈" 窗口、"并行监视" 窗口和 "调试位置" 工具栏。
对于使用任务并行库(TPL)或并发运行时的代码,用于调试的主要工具是 "并行堆栈" 窗口、"并行监视" 窗口和 "任务" 窗口,该窗口还支持JavaScript.
对于调试 GPU 上的线程,主要工具是“GPU 线程”窗口。
对于进程,主要工具是“附加到进程”对话框、“进程”窗口和“调试位置”工具栏。
Visual Studio 还提供功能强大的断点和跟踪点,在调试多线程应用程序时,这会很有用。 使用断点条件和筛选器将断点置于单个线程上。 使用跟踪点可以在不中断的情况下跟踪程序的执行,从而研究死锁之类的问题。
调试具有用户界面的多线程应用程序可能会特别困难。 可以考虑在另一台计算机上运行应用程序并使用远程调试。
使用 "线程" 窗口调试多线程应用
多个 Visual Studio 用户界面元素可帮助调试多线程应用。 下面介绍 "代码编辑器" 窗口、"调试位置" 工具栏和 "线程" 窗口中的多线程调试功能。
“启动调试”
在
代码
行上设置断点,方法是单击左侧的滚动条线,或选择线条并按F9。断点在代码行旁边的左侧滚动条中显示为红色圆圈。选择 "调试" > "开始调试",或按F5。
应用程序在调试模式下启动,并在断点处暂停。
在中断模式下,通过选择 "调试" > Windows > 线程打开 "线程" 窗口。 你必须在调试会话中才能打开或查看线程和其他调试窗口。
检查线程标记
"线程" 窗口中右键单击,然后选择菜单中的 "在源中 。
源代码行旁边的装订线现在显示一个线程标记图标。 线程标记指示线程在此位置停止。 如果该位置有多个已停止的线程,则会显示图标。
将指针悬停在线程标记上。 显示数据提示,并显示已停止的线程或线程的名称和线程 ID 号。 线程名称可能
<No Name>
。为了帮助识别不需要的线程,您可以在 "线程" 窗口中重命名它们。 右键单击该线程,然后选择 "重命名"。
Visual Studio调试器指南---多线程应用程序调试(二)
使用“并行堆栈”窗口:“线程”视图
在“调试”菜单上,单击“启动调试”。 等待命中第一个断点。
查看一个线程的调用堆栈
在“调试”菜单上指向“窗口”,然后单击“线程”。 将“线程”窗口停靠在 Visual Studio 底部。
在“调试”菜单上指向“窗口”,然后单击“调用堆栈”。 将“调用堆栈”窗口停靠在 Visual Studio 底部。
在“线程”窗口中双击线程,使其作为当前线程。 当前线程具有一个黄色箭头。 更改当前线程时,其调用堆栈显示在“调用堆栈”窗口中。
查看“并行堆栈”窗口
在“调试”菜单上指向“窗口”,然后单击“并行堆栈”。 确保在左上角的框中选择了“线程”。
通过使用 "并行堆栈" 窗口,您可以在一个视图中同时查看多个调用堆栈。 下图显示了 "调用堆栈" 窗口上方的 "并行堆栈" 窗口。
主线程的调用堆栈显示在一个框中,其他四个线程的调用堆栈则划分到另一个框中。 将这四个线程划分在一起是因为其堆栈帧共享相同的方法上下文;也就是说,它们处于相同的方法中:
A
、B
和C
。 若要查看共享同一框的线程的 Id 和名称,请将鼠标悬停在带有标题的框上(4 个线程)。 当前线程以粗体显示。黄色箭头指示当前线程的活动堆栈帧。
通过右键单击“调用堆栈”窗口,可以设置要显示堆栈帧的多少详细信息(“模块名称”、“参数类型”、“参数名称”、“参数值”、“行号”和“字节偏移量”)。
方框周围的蓝色突出显示指示当前线程是该框的一部分。 工具提示中的粗体堆栈帧也可指示当前线程。 如果双击“线程”窗口中的主线程,可以看到“并行堆栈”窗口中的蓝色突出显示内容相应移动。
继续执行到第二个断点
若要继续执行到命中第二个断点,请在“调试”菜单上单击“继续”。 下图所示为第二个断点处的线程树。
在第一个断点处,所有四个线程均从 S.A 执行到 S.B 再到 S.C 方法。 该信息仍会显示在“并行堆栈”窗口中,但是这四个线程已进一步执行。 其中一个线程继续执行到 S.D 再到 S.E。 另一个线程继续执行到 S.F、S.G 和 S.H。 其余两个线程继续执行到 S.I 和 S.J,其中一个线程从此方法执行到 S.K,而另一个线程继续执行到非用户外部代码。
可以将鼠标指针悬停在框标题(例如,“1 个线程”或“2 个线程”)上,以查看线程的线程 ID。 将鼠标指针悬停在堆栈帧上可以查看线程 ID 和其他帧详细信息。 蓝色突出显示指示当前线程,黄色箭头指示当前线程的活动堆栈帧。
软线程图标(interweaved lines)指示非当前线程的活动堆栈帧。 在“调用堆栈”窗口中,双击 S.B 可以切换帧。 “并行堆栈”窗口通过使用绿色的曲线箭头图标指示当前线程的当前堆栈帧。
在“线程”窗口中进行线程切换时,“并行堆栈”窗口中的视图相应更新。
可通过在“并行堆栈”窗口中使用快捷菜单来切换到其他线程或其他线程的其他帧。 例如,右键单击 S.J,指向“切换到帧”,然后单击某一命令。
右键单击 S.C,指向“切换到帧”。 其中一个带有选中标记的命令指示当前线程的堆栈帧。 您可以切换到相同线程的上述帧(将仅移动绿色箭头),也可以切换到其他线程(将同时移动蓝色突出显示)。 下图所示为子菜单。
当方法上下文只与一个堆栈帧关联时,框标题显示“1 个线程”,双击它即可切换到该线程。 如果双击关联有一个以上的帧的方法上下文,则会自动弹出该菜单。 将鼠标指针悬停在方法上下文上时,请注意右侧的黑色三角形。 单击该三角形也可以显示该快捷菜单。
对于具有多个线程的大型应用程序,您可能只希望关注某个线程子集。 “并行堆栈”窗口可仅显示已标记线程的调用堆栈。 若要标记线程,请使用快捷菜单或线程的第一个单元格。
在工具栏上,单击列表框旁边的“仅显示已标记项”按钮。
现在,只有已标记的线程才会显示在 "并行堆栈" 窗口中。
继续执行到第三个断点
若要继续执行到命中第三个断点,请在“调试”菜单上单击“继续”。
如果有多个线程位于同一方法中,但该方法不在调用堆栈开头,则会在不同框中显示该方法。 当前断点处的一个示例是 S.L,它包含三个线程并在三个框中显示这三个线程。 双击 S.L。
请注意,S.L 在其他两个框中为粗体,这样您可以看到 S.L 的其他显示位置。 如果希望查看对 S.L 进行调用的帧和 S.L 所调用的帧,请单击工具栏上的“切换方法视图”按钮。 下图显示了 "并行堆栈" 窗口的 "方法" 视图。
请注意以上关系图以所选方法为中心并将其单独放在视图中间的方框中。 被调用方和调用方分别显示在顶部和底部。 再次单击“切换方法视图”按钮可以退出该模式。
“并行堆栈”窗口的快捷菜单还包括以下其他项。
“十六进制显示”,用于在十进制和十六进制之间切换工具提示中的数字。
符号设置打开各自的对话框。
在源中显示线程在源代码中切换线程标记的显示,这些线程标记显示源代码中的线程位置。
“显示外部代码”,用于显示所有帧(即使这些帧未处于用户代码中)。 使用此项可查看展开的关系图,其中包含其他帧(这些帧可能因没有相应符号而灰显)。
在“并行堆栈”窗口中,确保已启用工具栏上的“自动滚动到当前堆栈帧”按钮。
如果关系图较大,当单步执行到下一断点时,您可能希望视图自动滚动到当前线程的活动堆栈帧;即第一个命中该断点的线程。
继续之前,请在“并行堆栈”窗口中一直滚动到左侧和底部。
继续执行到第四个断点
若要继续执行到命中第四个断点,请在“调试”菜单上单击“继续”。
请注意视图如何自动滚动到位。 在“线程”窗口中切换线程或在“调用堆栈”窗口中切换堆栈帧,并注意视图如何一直自动滚动到正确的帧。 禁用“自动滚动到当前工具帧”选项并查看不同之处。
“鸟瞰图”还有助于在“并行堆栈”窗口中处理大型关系图。 默认情况下,鸟瞰图为 on。 但是,您可以通过单击窗口右下角滚动条之间的按钮来对其进行切换,如下图所示。
在鸟瞰视图中,你可以移动矩形以便在关系图中快速平移。
朝任意方向移动此关系图的另一种方法是:单击此关系图的空白区域并将其拖动到所需位置。
若要放大和缩小此关系图,请按住 Ctrl 并移动鼠标滚轮。 或者,也可以单击工具栏上的“缩放”按钮并使用缩放工具。
通过单击“工具”菜单和“选项”,并选中或清除“调试”节点下的相应选项,还可以按从上到下(而不是从下到上)的方向查看堆栈。
继续之前,请在“调试”菜单上单击“停止调试”以结束执行。
使用“并行任务”窗口和“并行堆栈”窗口的任务视图
继续之前,建议您先完成前面的过程。
重新启动应用程序,直到命中第一个断点
在“调试”菜单上,单击“启动调试”并等待命中第一个断点。
在“调试”菜单上指向“窗口”,然后单击“线程”。 将“线程”窗口停靠在 Visual Studio 底部。
在“调试”菜单上指向“窗口”,然后单击“调用堆栈”。 将“调用堆栈”窗口停靠在 Visual Studio 底部。
在“线程”窗口中双击线程,以将其作为当前线程。 当前线程具有一个黄色箭头。 更改当前线程时,将更新其他窗口。 接着,我们将查看任务。
在 "调试" 菜单上,指向 "窗口",再单击 "任务"。 下图显示了 "任务" 窗口。
对于运行的每一项任务,您可以读取其 ID(由名称相同的属性返回)、运行该任务的线程的 ID 和名称以及任务位置(悬停以显示具有整个调用堆栈的工具提示)。 此外,“任务”列下还会显示传递到任务中的方法,即起始点。
可以对任何列进行排序。 请注意指示排序列和方向的排序标志符号。 您还可以通过向左或向右拖动列来对列重新排序。
黄色箭头指示当前任务。 通过双击某一任务或使用快捷菜单可以切换任务。 切换任务时,基础线程即成为当前线程并更新其他窗口。
在不同任务之间进行手动切换时,黄色箭头将相应移动,而白色箭头仍显示导致调试器中断的任务。
继续执行到第二个断点
若要继续执行到命中第二个断点,请在“调试”菜单上单击“继续”。
之前,"状态" 列将所有任务显示为 "活动",但现在两个任务被阻止。 任务可能因多种不同原因而被阻止。 在“状态”列中,将鼠标指针悬停在正在等待的任务上可以查看其阻止原因。 例如,在下图中,任务 3 正在等待任务 4。
任务 4 又在等待分配给任务 2 的线程所拥有的监视器。 (右键单击标题行,然后选择 "列" > 线程分配查看任务2的线程分配值 ")。
可以通过单击 "任务" 窗口第一列中的标志来标记任务。
使用标记功能可以跟踪同一调试会话中的不同断点之间的任务或筛选“并行堆栈”窗口中显示的调用堆栈所对应的任务。
在先前使用“并行堆栈”窗口时,已查看了应用程序线程。 请再次查看“并行堆栈”窗口,但这次查看应用程序任务。 请在左上方的框中选择“任务”来执行此操作。 下图所示为任务视图。
当前未在执行任务的线程不会显示在“并行堆栈”窗口的任务视图中。 此外,对于执行任务的线程,某些与任务无关的堆栈帧将从堆栈的顶部和底部筛选掉。
再次查看 "任务" 窗口。 右击任何列标题可以查看该列的快捷菜单。
您可以使用此快捷菜单添加或移除列。 例如,AppDomain 列未处于选中状态;因此,不会在列表中显示它。 单击“父级”。 此时将显示“父级”列,但不显示四个任务中任何任务的值。
继续执行到第三个断点
若要继续执行到命中第三个断点,请在“调试”菜单上单击“继续”。
此时,新任务(即任务 5)正在运行,而任务 4 则处于等待状态。 通过将鼠标指针悬停在“状态”窗口中正在等待的任务上,可以查看其原因。 请注意,父列中的任务4是任务5的父项。
若要更好地直观显示父子关系,请右键单击列标题行,然后单击 "父子视图"。 您应看到以下图示。
请注意,任务4和任务5正在同一线程上运行(如果它是隐藏的,则显示 "线程分配" 列)。 此信息不会显示在 "线程" 窗口中;这里看到的是 "任务" 窗口的另一个优点。 为确认这一点,请查看“并行堆栈”窗口。 确保你正在查看“任务”。 在任务窗口中双击任务4和5。 执行此操作时,“并行堆栈”窗口中的蓝色突出显示内容随之更新。 此外,还可以通过扫描“并行堆栈”窗口上的工具提示来查找任务 4 和任务 5。
在“并行堆栈”窗口中右键单击 S.P,然后单击“转到线程”。 此时,窗口将切换到线程视图,并显示相应帧。 你可以在同一线程上查看两个任务。
这是“并行堆栈”窗口中的任务视图相较于“线程”窗口的另一优势。
继续执行到第四个断点
若要继续执行到命中第三个断点,请在“调试”菜单上单击“继续”。 单击要按 ID 排序的“ID”列标题。 您应看到以下图示。
由于任务 5 已完成,因此不再显示该任务。 如果计算机上并非如此并且未显示死锁,请通过按“F11”单步执行一次。
任务3和任务4现在正在等待,并被阻止。 此外,还存在 5 个作为任务 2 的子级的新任务,目前已计划这些任务。 已计划任务是已在代码中启动但尚未运行的任务。 因此,其“位置”和“线程分配”列为空。
再次查看“并行堆栈”窗口。 每个框的标题都具有一个显示线程 ID 和名称的工具提示。 在“并行堆栈”窗口中切换到任务视图。 将鼠标指针悬停在标题上可以查看任务 ID 和名称以及任务状态,如下图所示。
可以按列对任务进行分组。 在 "任务" 窗口中,右键单击 "状态" 列标题,然后单击 "按状态分组"。 下图显示按状态分组的 "任务" 窗口。
此外,还可以按其他列进行分组。 通过对任务进行分组,你可以关注某个任务子集。 每个可折叠的组都包含一个分组在一起的项的计数。
要检查的任务窗口的最后一项功能是右键单击任务时显示的快捷菜单。
快捷菜单根据任务状态显示不同的命令。 这些命令可能包括“复制”、“全选”、“十六进制显示”、“切换到任务”、“冻结指定的线程”、“冻结此线程之外的全部线程”、“解冻指定的线程”和“标志”。
你可以冻结一个或多个任务的基础线程,也可以冻结除指定线程外的所有线程。 冻结的线程在 "任务" 窗口中以 "线程" 窗口中的蓝色暂停图标表示。