分类 调试 下的文章


今天我们探索一个问题: 64位的ntdll是如何被加载到WoW64下的32位进程?今天的旅程将会带领我们进入到Windows内核逻辑中的未知领域,我们将会发现32位进程的内存地址空间是如何被初始化的。

 

WoW64是什么?

 

来自MSDN:

  WOW64是允许32位Windows应用程序无缝运行在64位Windows的模拟器。 

换句话说,随着64位版本Windows的引进,Microsoft需要拿出一种允许在32位时代的Windows程序与64位Windows新的底层组件无缝交互的解决方案。特别是64位内存寻址和与内核直接交流的组件。

 

两个NT层,一个内核

 

在32位的Windows系统中,要调用Windows API的应用程序需要经过一系列的动态链接库(DLL)。然而,所有的系统调用最终会定向到ntdll.dll,它是在用户模式下将用户模式API传递给内核的最高层。以调用CreateFileW为例,这个API调用源于用户模式下的kernel32.dl,随后它以NtCreateFile传递给ntdll,随后NtCreateFile通过系统调度程序将控制权传递给内核。

 

在32位Windows下这是非常简单的,然而,在WoW64下需要额外的步骤。32位的ntdll不可以直接将控制权交给内核,因为内核是64位的,只接受遵循64位ABI的类型(译者注:ABI,Application Binary Interface,应用二进制接口)。正因为如此,一个翻译层以几个标准的命名为wow64.dll,wow64cpu.dll和wow64win.dll的DLL的形式被添加到64位Windows。这几个DLL负责将32位调用转换成64位调用。那些调用最终被定向到映射到每个32位进程中的64位ntdll。许多关于这种从32位系统调用到64位系统调用(1)的神奇转换的信息是可获得的,所以我们不会从这里进入。我们最关注的是内核何时和怎样将64位版本的ntdll映射到一个32位进程。看起来像这样:



在 Visual Studio 调试器中创建数据的自定义视图C++/C#

调试器提供了许多用于检查和修改程序状态的工具。 这些工具中的大多数仅在中断模式下有效。

在变量窗口和数据提示中创建数据的自定义视图

许多调试器窗口(如 "自动" 和 "监视" 窗口)都允许您检查变量。 您可以自定义C++类型、托管对象以及您自己的类型在调试器变量窗口和数据提示中的显示方式。

创建自定义可视化工具

可视化工具使您能够以有意义的方式查看对象或变量的内容。 在 Visual Studio 调试器中,可视化工具是指可以使用放大镜VisualizerIcon图标打开的其他窗口。 例如,HTML 可视化工具显示 HTML 字符串如何在浏览器中进行解释和显示。 你可以通过数据提示、"监视" 窗口 、"自动" 窗口和 "局部变量" 窗口访问可视化工具。 "快速监视" 对话框还提供可视化工具。 

自定义C++视图

使用 Natvis 框架在C++调试器中创建对象的自定义视图

Visual Studio Natvis 框架可以自定义本机类型在调试器变量窗口(例如局部变量、监视以及数据提示窗口)中显示的方式。 Natvis 的可视化功能可以让你创建的类型在调试期间更加直观清晰。

Natvis 替换了 Visual Studio 早期版本中的 autoexp.dat 文件,提供了 XML 语法、更好的诊断功能、版本控制功能以及多文件支持功能。

Natvis 可视化效果

你可以使用 Natvis 框架为自己创建的类型创建可视化规则,让开发人员在调试过程中更轻松地查看这些类型。

例如,下图显示的类型 Windows::UI::Xaml::Controls::TextBox 的变量在调试器窗口中未应用任何自定义可视化。

TextBox 默认可视化

突出显示的行显示 Text 类的 TextBox 属性。 由于类的层次结构很复杂,因此很难找到这个属性。 调试器不知道如何解释自定义字符串类型,所以你看不到文本框中的字符串。

如果应用了 Natvis 自定义可视化工具规则,那么在变量窗口中,同样的TextBox看起来就简单得多。 类的重要成员会显示在一起,并且调试器会显示自定义字符串类型的基础字符串值。

在 C++ 项目中使用 .natvis 文件

Natvis 使用Natvis文件来指定可视化规则。 Natvis文件是具有NATVIS扩展名的 XML 文件。 Natvis 架构在 %VSINSTALLDIR%\Xml\Schemas\natvis.xsd中定义。

.natvis 文件的基本结构由一个或多个代表可视化条目的 Type 元素构成。 每个 Type 元素的完全限定名称都在其 Name 属性中指定。

XML

仅我的代码是一种 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 文件:

XML