2023年1月

在《Windbg里识别CRT堆》一文里,介绍了可以通过msvcrXX.dll!_crtheap来获取crt堆的句柄。但这种方法只针对VS2015前的crt版本有效。

这是因为从 Visual Studio .NET 到 Visual Studio 2013,C++ 编译器和工具的每个主版本都包含一个新的独立版本的 Microsoft C 运行 (CRT) 库。 CRT 的这些独立版本彼此独立,并在不同程度上彼此不兼容。 例如,Visual Studio 2012 使用的 CRT 库是第 11 版,名为 msvcr110.dll,而 Visual Studio 2013 使用的 CRT 是第 12 版,名为 msvcr120.dll。 从 Visual Studio 2015 开始,不再是这样。 Visual Studio 2015 及更高版本的 Visual Studio 都使用一个通用 CRT(Universal CRT,UCRT),是 Microsoft Windows 操作系统组件。

当vs2015还在CTP阶段时,微软的设想是将VC运行时库拆分成三部分:

  • vcruntime140.dll 包含运行期需要处理的功能,如:进程启动、异常处理、以及耦合到相关编译器的功能。
  • appcrt140.dll包含所有平台上都可用的所有功能,且以后保持这部分CRT的向后兼容性。包括:堆、数学库、stdio库、locale库、大多数字符串操作函数、时间库和一些其他功能等。
  • desktopcrt140.dll包含所有只能由桌面应用程序使用的功能,且以后保持这部分CRT的向后兼容性。包括:处理多字节字符串、exec和spawn进程管理函数、direct-to-console I/O函数的功能等等。

在最终发布正式版的时候,微软将appcrt140.dll和desktopcrt140.dll合并为一个不带版本号的程序库:ucrtbase.dll。它对应的Debug版本的命名是ucrtbased.dll。这个后来被正式命名为“The Universal CRT”。

那么ucrtbase/ucrtbased自然是我们关注的焦点,crt堆句柄自然就在这里面,有一个导出的符号__acrt_heap,就是它

看debug版本,如下:

看Release版本,如下:

来了个这个,难道Release版没有这个符号,输入x ucrtbase!*看看

输入

dd 76ff12dc

.NET引入了一种新的符号文件(PDB)格式——Portable PDB。与仅限Windows的传统PDB不同,Portable PDB可以在所有平台上创建和读取。

对于任何不熟悉PDB文件的人来说,PDB文件是由编译器生成的辅助文件,用于提供其他工具,尤其是调试器,有关可执行文件中的内容以及如何生成的信息。Windows PDB格式已经存在了很长一段时间(约25年),它是从其他更古老的本机调试符号格式演变而来的。它最初是作为原生(C/C++)程序的一种格式出现的。第一次发布的.NET框架,Windows PDB格式被扩展以支持.NET。

虽然Windows PDB格式多年来一直运行良好,但仍存在一些问题。NET Roslyn团队决定是时候想出一种新的格式了。原因如下:

  • Windows PDB格式很复杂,没有很好的文档记录。这种复杂性对于该格式设计的某些本机代码场景很重要,但对于其他应用程序来说是不必要的。.NET可移植格式是开源的,并有文档记录。
  • 在这项工作开始时,还没有一个跨平台的库可以读取或写入原始的windows PDB格式。
  • Windows PDB格式不是托管代码的紧凑表示形式。使用新格式可以在不丢失任何信息的情况下大幅缩小尺寸。

如今,Portable PDB和Windows PDB不是任何地方都支持,因此您需要考虑在哪里使用(,以决定使用哪种格式。如果您有一个项目希望能够以两种格式使用和调试,那么可以使用不同的生成配置并生成两次该项目,以支持这两种类型的使用者。

Windows PDB只能在Windows上写入或读取。除了Visual Studio代码(因为Visual Studio代码努力在所有平台上实现一致的行为)和Visual Studio正在调试远程Linux/OSX计算机的场景(因为必须在远程计算机上读取PDB)之外,所有Windows工具都支持这些功能。PortablePDB可以在任何操作系统上读取,但仍有许多地方不支持它们。比如以下情况:


  1. Visual Studio调试器的旧版本(VS 2015更新2之前的版本)。
  2. 应用程序目标.NET Framework 4.7.1或早期版本:打印堆栈跟踪,并将其映射回行号(例如在ASP.NET错误页中)。方法的名称不受影响,只支持源文件名和行号。
  3. C#代码分析(又名FxCop),请注意,这不适用于Roslyn Analyzer。
  4. 一些符号服务器。
  5. 运行编译后构建步骤,使用旧版本的工具(如CCI、CodeContracts)使用或修改PDB。
  6. 使用.NET反编译程序,如ILDASM或\NET Reflector,并希望看到源行映射或本地参数名称。
  7. 基于MS DIA的工具,如WinDBG。

当我们的程序出了问题,想要观察程序的执行过程,这时有两种进入调试的方式:

  • 附加到进程
  • 调试器启动进程

这是我们常用的两种方式,那么有什么差别呢?有差别,今天我就谈一点:那就是堆内存的不同。通过附加到进程的方式(排除用其他工具做了设置)内存是标准堆;而直接用调试器启动的方式内存是系统调试堆。

我们用Windbg来观察一下

直接启动进程

看到,直接启动进程进行调试,堆内存被做了很多设置:htc、hfc、hpc等

而附加的方式如下

可见少了很多设置项。

通过上面的差别,我们就明白了两种方式的同步。由于调试堆或多或少会影响性能,如果我们想不管哪种方式都想用标准堆,可以做如下设置:

  • 在系统环境变量里设置_NO_DEBUG_HEAP=1(这个是系统全局性,对任何调试器都可以禁止)
  • 如果是Windbg,可以在命令行使用 -hd选项
  • 如果是VS IDE,可以在项目属性里设置环境变量_NO_DEBUG_HEAP=1
    这个在VS2015以前的版本是管用的,2015及其以后的版本不需要了,已经自动禁止了

     

 

系统API或COM调用,会产生错误。设置一个错误代码,可以由GetLastError检索。有几种方法可以破译此代码:

  • 使用命令行 net helpmsg
  • 从VS菜单中,选择工具/错误查找并粘贴代码
  • 在VS的监视窗口上输入“$err,hr”!
  • 在VS的监视窗口上输入 *(int*)(@tib+0x34),hr 

其实最后两种方法是一样的。