分类 调试 下的文章

在调试版本中遇到的一个问题是编译本地的C++应用程序。例如,许多局部变量消失了,因为代码生成器没有将它们放在堆栈上,而是将它们放在寄存器中,就像在调试生成中发生的那样。此外,release积极地构建对函数的内联调用,因此代码生成器将函数体直接放入调用方法中。一旦您习惯了编译器的模式,并了解了一点汇编语言,就不难理解在调试发行版生成代码时会发生什么。

我想在本文中介绍的是在不加应用程序的情况下正确创建本地C++发布PDB所必需的精确开关,这样我就可以回答隐形的问题了。我将向您展示的开关与优化无关,PDB文件的创建不会影响优化。
要设置的第一个开关是在编译器CL.EXE上,它是/Zi。此开关将调试信息放入.OBJ文件中,以便链接器将其放入最终PDB。您将在项目的C/C++属性页中设置这个开关。如下图

接下来的三个开关应用于链接器LINK.EXE。第一个命令/DEBUG告诉链接器您要为该生成创建PDB文件。如下:

/DEBUG开关有个小问题。打开它会告诉链接器您正在执行未优化的生成,所以/DEBUG隐式地打开/INCREMENTAL并实质上创建一个调试生成,尽管编译器优化会应用(但不是链接时代码生成优化)。这对您意味着链接器以“快速模式”链接,因此如果您的OBJ中有300个函数,但您只引用(即,使用)其中一个函数,那么链接器会将所有300个函数放入输出二进制文件中。是的,这意味着299个死函数和一个非常臃肿的二进制文件。这种“将所有内容都放入输出二进制文件”是调试版本比发布版本大得多的原因之一。微软的优化技术非常好,但不是那么好!
因为您只希望在输出二进制文件中实际引用那些函数,所以需要使用/OPT:REF开关将其告知链接器,该开关在链接器的优化部分中设置如下:

您需要设置的最后一个链接器开关是一个有趣的小gem:/OPT:ICF。这将打开COMDAT Folding。真 的!有一个术语你不是每天都听到。这是一个很好的小编译器优化,链接器将查找具有相同汇编语言代码的函数,并且只生成其中一个。这里的大多数人第一次谈到COMDAT的folding时,它会把它们扔出去循环。然而,当您考虑有多少函数,特别是简单且相同的STL模板时,这个COMDAT Folding可以帮助您精简二进制文件。如果仔细查看上面屏幕快照的优化部分,您将看到/OPT:ICF选项就在/OPT:REF选项的正下方。

发布构建二进制文件之所以增长,是因为没有设置/OPT:REF和/OPT:ICF开关。唯一的信息添加到一个本地C++二进制与这些开关是调试目录中的输出二进制。

.f+, .f- (Shift Local Context)

.f+ 命令将帧序号移动到当前堆栈中的下一帧。.f- 命令将帧序号移动到当前堆栈中的上一帧。

语法

.f+  
.f-  

环境

模式 用户模式、内核模式
目标 活动目标、崩溃转储
平台 所有

 

注释

(frame)用来指定调试器用来解析局部变量的局部上下文(作用域)。

.f+.f-
命令是用来在当前调用堆栈中移动到下一帧或者前一帧的快捷方式。这些命令和下面的.frame
命令作用相同,但是.f 要更短更方便:

  • .f+.frame @$frame + 1一样。
  • .f-.frame @$frame - 1一样。

美元符号($)表示帧的值是一个伪寄存器。At符号(@)使得调试器访问该值更快,因为它告诉调试器后面的字符串是一个寄存器或者伪寄存器。

程序运行时,局部变量的意义由程序计数器的位置决定,因为这些变量的作用域仅在定义它们的函数内部。如果没有使用.f+.f-
命令(或者 .frame 命令),调试器会使用当前函数(调用堆栈中的当前帧)的作用域作为局部上下文。

帧序号(frame number)是堆栈帧在堆栈回溯中的位置。可以使用k, kb, kd, kp, kP, kv (Display Stack Backtrace)命令或者Calls
窗口查看堆栈回溯。第一行 (当前帧) 的帧序号是0。后面的行分别是1、2、3等等。

可以将局部上下文设置到另一个堆栈帧来查看新的局部变量信息。但是,实际可用的变量由执行的代码决定。

如果又对程序进行执行,调试器会将局部上下文重置为程序计数器的作用域。如果寄存器上下文改变,局部上下文也重置到调用堆栈顶部的帧。

有人问了这样的问题:"我工作的公司正极力反对用生成的调试信息构建发布模式二进制文件,这也是我注册该类的原因之一。他们担心演示会受到影响。我的问题是,在发布模式下生成符号的最佳命令行参数是什么?还有什么地方我可以参考,以表明不应该有性能问题。“

回答是:不,生成PDB文件对性能没有任何影响。至于我也可以给你指出的参考资料,我在网上还没有找到任何能回答确切问题的资料,所以让我依次介绍.NET和本机开发。

Eric Lippert写了一篇很棒的文章,优化开关是做什么的?他讨论了编译器和即时(JIT)编译器所做的优化。(基本上,您可以将其总结为JITter完成所有实际的优化工作。)C#和VB.NET编译器之间的切换有些混乱,因为有四个不同的/debug开关,/debug,/debug+,/debug:full和/debug:pdb。我之所以造成这种混乱,是因为我认为/debug:pdb只做了一些与其他三个/debug开关不同的事情,它们对发布版本的构建更好。

所有四个开关都做相同的事情,因为它们会生成一个PDB文件,但为什么有四个开关做相同的事情?微软开发人员真的喜欢解析稍微不同的命令行选项吗?真正的原因是:历史。在.NET1.0中有差异,但在.NET2.0中没有。看起来.NET4.0将遵循相同的模式。在与CLR调试团队进行了反复检查之后,没有任何区别。

控制抖动是否执行调试生成的是/optimize开关。使用/optimize构建-将在程序集中添加一个属性DebuggableAttribute,并将DebuggingMode参数设置为DisableOptimizations。不需要罗兹学者就能弄明白,禁用优化正是按照它所说的做的。

归根结底,您希望使用/optimize+和任何/debug开关来构建发布版本,以便可以使用源代码进行调试。阅读Visual Studio文档,了解如何在不同类型的项目中设置这些开关。

很容易证明这些是最佳开关。使用我的示例程序,我用/optimize+和/debug编译了一个构建,用just/optimize+编译了另一个构建。这与/debug+和/debug相同,而另一个与/optimize+/debug:pdbonly相同,显示了不同之处,这是我们错误的根源。编译之后,我使用ILDASM和以下命令行从二进制文件中获取原始信息

ILDASM /out=Paraffin.IL Paraffin.exe

使用diff工具,您将看到两个构建之间的IL本身是相同的。主要区别在于程序集的DebuggableAttribute声明。在build/optimize+和a/debug开关时,一个DebuggingMode.IgnoreSequencePoints被传递给DebuggableAttribute,告诉JIT编译器不需要加载PDB文件就可以正确地对IL进行JIT。DebuggingMode的值。默认值也为或,但该值被忽略。

与.NET一样,构建PDB文件与优化无关,因此对应用程序的性能没有影响。如果你的经理用说是“害怕业绩会受到影响”,我就告诉他们。(不幸的是,我遇到的经理中,说这话的人比我想象的还多)。

NET非常简单,因为实际上只有两个开关,所以适当的优化开关取决于许多单独的应用程序因素。我能告诉你的是在发布版本中正确生成PDB文件需要设置哪些开关。

对于编译器CL.EXE,您需要添加/Zi以使其将调试符号放入.OBJ文件。对于链接器LINK.EXE,需要指定三个选项。第一个是/DEBUG,它告诉链接器生成一个PDB文件。但是,该开关还告诉链接器这是调试生成。这不太好,因为这会影响二进制文件的性能。基本上,使用/调试时发生的情况是链接器链接速度更快,因为它不再查找单个引用。如果使用OBJ中的一个函数,链接器会将整个OBJ抛出到输出二进制文件中,因此现在有一堆死函数。

要告诉链接器只需要引用的函数,需要添加/OPT:REF作为第二个开关。第三个开关是/OPT:ICF,它启用了COMDAT折叠。有一个术语你不是每天都听到。基本上,这意味着在生成二进制文件时,链接器将查找具有相同代码的函数,并且只生成一个函数,但使多个符号指向一个函数。

如果您想自己在本机二进制文件上测试差异,以查看对生成PDB文件有什么影响,这几乎和.NET二进制文件一样简单。Visual Studio附带了一个很好的小程序DUMPBIN,它可以告诉您关于可移植可执行文件的更多信息使用/DISASM开关运行它以获得二进制文件的反汇编。

.dumpcab (Create Dump File CAB)

.dumpcab命令创建一个包含当前dump文件的CAB文件。

语法

.dumpcab [-aCabName 

参数

-a
使得当前加载的符号也包含在CAB文件中。对于minidump,所有以加载的映像也会包含进去。使用lml来查看加载了哪些符号和映像。
CabName
包含扩展名的CAB文件名。CabName 可以包含绝对或者相对路径;相对路径是相对于调试器启动的目录的。建议使用.cab扩展名。

环境

模式 用户模式、内核模式
目标 活动目标、崩溃转储
平台 所有

注释

该命令只有在调试dump文件时可以使用。(这句话似乎和前面的表格有冲突。—译者注)

如果在调试活动目标时想创建CAB中的dump文件,需要使用.dump (Create Dump File)命令。然后,打开一个使用该dump文件作为目标的新的调试会话,然后再使用.dumpcab

.dumpcab命令不能将多个dump文件放入一个CAB文件中。

大多数开发人员都意识到PDB文件有助于您进行调试,但仅此而已。如果你不知道PDB文件是怎么回事,不要觉得很糟糕,因为虽然有文档在那里,但它分散在周围,而且大部分是为编译器和调试器编写器准备的。虽然编写编译器和调试器非常酷和有趣,但这可能不是你的工作。
我想做的是把每个在微软操作系统上进行开发的人都必须知道的PDB文件放在一个地方。这些信息也适用于本机开发人员和托管开发人员,不过我将提到一个特定于托管开发人员的技巧。我将从讨论PDB文件存储和内容开始。由于调试器使用PDB文件,我将详细讨论调试器如何为二进制文件找到正确的PDB文件。最后,我将讨论调试器在调试时如何查找源文件,并向您展示一个与调试器如何查找源代码相关的常用技巧。
在我们开始之前,我需要定义两个重要的术语。在开发计算机上执行的生成是私有生成。在生成计算机上完成的生成是公共生成。这是一个重要的区别,因为调试在本地生成的二进制文件很容易,总是公共生成导致问题。
所有开发人员需要知道的最重要的事情是:PDB文件和源代码一样重要!很多公司没有人能找到在生产服务器上运行的构建的PDB文件。如果没有匹配的PDB文件,您的调试挑战几乎是不可能的。通过大量的努力,可以在没有正确的PDB文件的情况下找到问题,但是如果您首先拥有正确的PDB文件,它将为您节省大量的资金。

正如Visual Studio的开发经理约翰坎宁安(John Cunningham)在2008年的PDC会议上所说的,“热爱、保持和保护您的pdb”。至少,每个开发商店都必须设置一个符号服务器。您还可以在Windows调试工具帮助文件中阅读Symbol Server文档本身。请查看这些资源以了解有关详细信息的更多信息。简而言之,符号服务器存储所有公共构建的pdb和二进制文件。这样,无论哪个构建有人报告崩溃或问题,您都有与调试器可以访问的公共构建完全匹配的PDB文件。Visual Studio和WinDBG都知道如何访问符号服务器,如果二进制文件来自公共构建,调试器将自动获取匹配的PDB文件。
大多数人在将PDB文件放入Symbol服务器之前还需要做一个准备步骤。这一步是在您的公共PDB文件上运行源服务器工具,这被称为源索引。索引嵌入了版本控制命令,以拉动在特定公共构建中使用的确切源文件。因此,在调试公共生成时,您永远不必担心找到该生成的源文件。如果您是一个一人或两人的团队,则有时可以不使用源服务器步骤。
我听到过一些团队对设置符号服务器的抱怨,他们的软件太大太复杂。我不得不承认,当我听到别人说“我的团队功能失调”时,你的软件绝不可能比微软做的任何事情都更大、更复杂。它们将所有产品的每个版本的索引和存储到一个符号服务器中。这意味着从Windows到Office,再到SQL,再到游戏,所有东西都存储在一个中心位置。我的猜测是,在雷德蒙德的34号楼只不过是存储所有这些文件的SAN驱动器,而大楼里的每个人都支持这些SAN。能够在微软内部调试任何东西都是非常令人惊奇的,而且您不必担心符号或源代码(前提是您对源代码树拥有适当的权限)。

在关键基础设施讨论结束后,让我来看看PDB中的内容以及调试器如何找到它们。PDB文件的实际文件格式是一个严格保密的秘密,但是微软提供了api来为调试器返回数据。本机C++ PDB文件包含相当多的信息:

  • 公共、私有和静态函数地址
  • 全局变量名称和地址
  • 参数和局部变量的名称和偏移在堆栈上的位置
  • 由类、结构和数据定义组成的类型数据
  • 帧指针省略(FPO)数据,这是在x86上进行本机堆栈遍历的关键
  • 源文件名及其行

一个.NET PDB只包含两部分信息:源文件名及其行和本地变量名。所有其他信息都已在.NET元数据中,因此无需在PDB文件中重复相同的信息。
将模块加载到进程地址空间时,调试器使用两条信息来查找匹配的PDB文件。第一个显然是文件名。如果加载ZZZ.DLL,调试器将查找ZZZ.PDB。最重要的部分是调试器如何知道这是与此二进制文件完全匹配的PDB文件。这是通过嵌入在PDB文件和二进制文件中的GUID完成的。如果GUID不匹配,您肯定不会在源代码级别调试模块。
.NET编译器和本机的链接器将此GUID放入二进制和PDB中。既然编译的行为创建了这个GUID,那么请停下来考虑一下。如果您有昨天的版本,但没有保存PDB文件,您是否可以再次调试二进制文件?不!这就是为什么保存每次生成的PDB文件如此重要的原因。因为我知道你在想,所以我会继续回答你脑海中已经形成的问题:不,没有办法更改GUID。
但是,可以查看二进制文件中的GUID值。使用Visual Studio附带的命令行工具DUMPBIN,可以列出可移植可执行文件(PE)的所有部分。要运行DUMPBIN,请从程序菜单中打开Visual Studio 2008命令提示符,因为需要设置PATH环境变量才能找到DUMPBIN EXE。顺便说一下,如果你对DUMPBIN向你展示的信息感兴趣的话,我强烈推荐Matt Pietrek在2002年2月和2002年3月的MSDN杂志上关于PE文件的权威文章。

DUMPBIN有很多命令行选项,但是显示构建GUID的选项是/HEADERS。Pietrek文章将解释输出,但对我们来说重要的一点是调试目录输出:

Debug Directories
Time Type Size RVA Pointer
——– —— ——– ——– ——–
4A03CA66 cv 4A 000025C4 7C4 Format: RSDS,
  {4B46C704-B6DE-44B2-B8F5-A200A7E541B0}, 1,
C:junkstuffHelloWorldobjDebugHelloWorld.pdb

根据调试器如何确定正确匹配的PDB文件的知识,我想讨论调试器在哪里查找PDB文件。调试时,通过查看Visual Studio模块窗口的符号文件列,可以看到所有这些命令都是自己加载的。首先搜索的是加载二进制文件的目录。如果PDB文件不存在,调试器将查找嵌入PE文件中的调试目录中的硬编码生成目录。如果查看上面的输出,就会看到完整的路径C:JUNKSTUFFHELLOWORLDOBJDEBUGHELLOWORD.PDB。(用于生成.NET应用程序的MSBUILD任务实际上生成到OBJ<CONFIG>目录,并仅在成功生成时将输出复制到调试或发布目录。)如果PDB文件不在前两个位置,并且在计算机上为设置了符号服务器,则调试器将在符号服务器缓存目录中查找。最后,如果调试器在Symbol Server缓存目录中找不到PDB文件,它将在Symbol Server本身中查找。此搜索顺序就是为什么本地生成和公共生成部件从不冲突的原因。
调试器搜索PDB文件的方式对几乎所有要开发的应用程序都很好。PDB文件加载更有趣的地方是那些需要您将程序集放入全局程序集缓存(GAC)中的.NET应用程序。我特别关注的是你的SharePoint和你对web部件的残忍,但是还有其他的。对于本地计算机上的私有构建,使用起来很容易,因为调试器将在构建目录中找到PDB文件,如我上面所述。当您需要在另一台计算机上调试或测试私有构建时,就会出现问题。

在另一台机器上,我看到许多开发人员在使用GACUTIL将程序集放入GAC后所做的工作是打开一个命令窗口,在C:windowssembly中搜索程序集在磁盘上的物理位置。尽管将来可能会发生更改,但为任何CPU编译的程序集实际上都位于如下目录中:C:WindowsassemblyGAC_MSILExample1.0.0.0__682bc775ff82796a

示例是程序集的名称,1.0.0.0是版本号,682bc775ff82796a是公钥标记值。推导出实际目录后,可以将PDB文件复制到该目录,调试器将加载该目录。
如果你现在对像这样挖掘GAC感到有点不安,你应该,因为它是不受支持和脆弱的。有一个更好的方法,似乎几乎没人知道DEVPATH。其思想是,您可以在.NET中设置一些设置,它会将您指定的目录添加到GAC中,因此您只需将程序集和它的PDB文件放入该目录,这样调试就容易得多。仅在开发计算机上设置DEVPATH,因为存储在指定目录中的任何文件都不会像在真正的GAC中那样进行版本检查。
顺便说一下,如果你在任何一个互联网搜索引擎中搜索DEVPATH,其中一个最热门的条目是Suzanne Cook的一篇过时的博客文章,她说微软正在摆脱DEVPATH。这不再是事实。和其他博客一样,看看苏珊博客上的日期:2003年。这相当于互联网时代的1670年。
要使用DEVPATH,首先要创建一个目录,该目录对所有帐户都具有读访问权限,至少对开发帐户具有写访问权限。此目录可以位于计算机上的任何位置。第二步是设置一个系统范围的环境变量DEVPATH,其值是您创建的目录。有关DEVPATH的文档并没有说明这一点,但是在执行下一步之前,请先设置DEVPATH环境变量。

要告诉.NET运行时已设置DEVPATH,需要根据应用程序的需要将以下内容添加到APP.CONFIG、WEB.CONFIG或MACHINE.CONFIG中:

<configuration>
   <runtime>
      <developmentMode developerInstallation=”true”/>
   </runtime>
</configuration>

一旦打开开发模式,您就会知道在进程中缺少DEVPATH环境变量或者您设置的路径不存在,如果应用程序在启动时死机,错误消息表示完全非直观的:“注册表的无效值”。如果确实要在MACHINE.CONFIG中使用DEVPATH,请格外小心,因为计算机上的每个进程都会受到影响。在一台机器上导致所有.NET应用程序失败不会赢得办公室里的许多朋友。
每个开发人员需要知道的关于PDB文件的最后一项是源文件信息如何存储在PDB文件中。对于已在其上运行源索引工具的公共构建,存储是将源文件获取到所设置的源缓存中的版本控制命令。对于私有构建,存储的是编译器用来生成二进制文件的源文件的完整路径。换句话说,如果在C:FOO中使用源文件MYCODE.CPP,那么PDB文件中嵌入的是C:FOOMYCODE.CPP。这可能是你已经怀疑过的,但我只是想说清楚。
理想情况下,所有的公共构建都会立即自动被源代码索引并存储在符号服务器中,因此如果您甚至不必再考虑源代码在哪里的话。然而,一些团队在PDB文件中不做源索引,直到他们做了冒烟测试或其他祝福,看看该构建是否足够好让其他人使用。这是一个非常合理的方法,但是如果必须在生成的源代码被索引之前调试生成,最好将该源代码拉到与生成计算机使用的驱动器和目录结构完全相同的位置,否则在源代码级别调试时可能会遇到一些问题。虽然Visual Studio调试器和WinDBG都有设置源搜索目录的选项,但我发现很难找到正确的方法。
对于较小的项目,这没有问题,因为您的源代码总是有足够的空间。生活更困难的地方是在更大的项目上。如果你有30MB的源代码,而你的C:驱动器上只剩下20MB的磁盘空间,你该怎么办?有办法控制PDB文件中存储的路径不是很好吗?
虽然我们不能编辑PDB文件,但是有一个简单的技巧可以控制PDB文件中的路径:SUBST.EXE。subt所做的是将路径与驱动器号相关联。如果您将源代码下拉到C:DEV并执行“SUBST R:C:DEV”,那么如果您键入“DIR C:DEV”,R:drive现在将在其顶层显示相同的文件和目录。您还将在资源管理器中看到R:drive作为新驱动器。也可以通过将驱动器映射到资源管理器中的共享目录来实现驱动器到路径的影响我个人更喜欢subt方法,因为它不需要机器上的任何共享。虽然有些人认为可以通过<DRIVE>$共享,但有些组织禁用了该功能。
在生成计算机上要做的是设置一个启动项,该启动项执行特定的SUBST命令。当生成系统帐户登录时,它将提供新的驱动器号,这是您进行生成的位置。通过对PDB文件中嵌入的驱动器和根目录的完全控制,在测试机上设置源代码所需做的全部工作就是在任何需要的地方将其拉下来,并使用与生成机使用的驱动器号相同的驱动器号执行子命令。现在调试器中不再考虑源匹配了。