2023年1月

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

回答是:不,生成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开关运行它以获得二进制文件的反汇编。

在调试版本中遇到的一个问题是编译本地的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++二进制与这些开关是调试目录中的输出二进制。

.cmdtree

简介

使用形式

.cmdtree cmdfile

参数

  • cmdfile
    命令文件,包含多个你需要的命令。必须是一个文本档

使用步骤

1、使用命令创建文本文件test.wl,使用以下示例作为模板。您可以按所需方式修改{}之间的节:

windbg ANSI Command Tree 1.0title {"Common Commands"}

body

{
"Common Commands"}

{
"Information"}

{
"Time of dump"} {".time"}

{
"Process being debugged"} {"|"}

{
"Dump Location"} {"||"}

{
"Create server on port 9999"} {".server tcp:port=9999"}

{
"Show remote connections"} {".clients"}

{
"Process Environment Block"} {"!peb"}

{
"Logging"}

{
"Open Log"} {".logopen /t /u /d"}

{
"Close Log"} {".logclose"}

{
"Modules"}

{
"All Modules"} {"lm D sm"}

{
"Loaded Modules"} {"lmo D sm"}

{
"Loaded Modules (verbose)"} {"lmvo D sm"}

{
"Modules w/o symbols"} {"lme D sm"}

{
"Stacks"}

{
"Set frame length to 2000"} {".kframes 2000"}

{
"Dump current stack w/ DML"} {"kpM 1000"}

{
"Dump stacks without private info"} {"knL 1000"}

{
"Dump stacks with all parameters"} {"kPn 1000"}

{
"Dump stacks (distance from last frame)"} {"kf 1000"}

{
"Dump stacks with Frame Pointer Omission"} {"kvn 1000"}

{
"Dump all stack"} {"~*kbn 1000"}

{
"Dump unique stacks"} {"!uniqstack -pn"}

{
"Thread environment block"} {"!teb"}

{
"Move to next frame"} {".f+"}

{
"Move to previous frame"} {".f-"}

{
"Memory"}

{
"Dump heaps"} {"!heap -a"}

{
"Automated Task"}

{
"!analyze"} {"!analyze -v"}

{
"Locks"} {"!ntsdexts.locks"}

{
"CPU time for User and Kernel Mode"} {"!runaway 7"}

{
"Managed"}

{
"Load sos"} {".loadby sos mscorwks"}

{
"clrstack"} {"!clrstack"}

{
"Threads"} {"!threads"}

{
"Stack Objects"} {"!dso"}

{
"Exceptions"} {"!dae"}

最近我写个例子程序研究下某个异常情况,故意制造了个崩溃。然后分析dmp文件。

当我执行!address -summary命令想观察下进程当前内存情况时,去报如下错误:

0:000> !address -summary

No symbols for ntdll. Cannot continue.

这意思是没有ntdll.dll模块的符号,不可能啊,因为有明显的证据证明该模块符号文件已经加载且匹配,如下:

1、比如我们从线程栈可以佐证

05 00affa24 7799662d 008e7000 808b6aa4 00000000 kernel32!BaseThreadInitThunk+0x19 (FPO: [Non-Fpo])
06 00affa80 779965fd ffffffff 779b51e4 00000000 ntdll!__RtlUserThreadStart+0x2f (FPO: [SEH])
07 00affa90 00000000 00da1091 008e7000 00000000ntdll!_RtlUserThreadStart+0x1b (FPO: [Non-Fpo])
只有加载了对应符号文件,才能看到这个正确的函数名。

2、执行!chksym ntdll

0:000> !chksym ntdll

ntdll.dll
    Timestamp: 6071CF9D
  SizeOfImage: 19C000
          pdb: wntdll.pdb
      pdb sig: F9EA7A7F-4120-6C21-D2ED-4E99993A9EF4
          age: 1

Loaded pdb is f:\debug_symbol\symbols32\wntdll.pdb\F9EA7A7F41206C21D2ED4E99993A9EF41\wntdll.pdb

wntdll.pdb
      pdb sig: F9EA7A7F-4120-6C21-D2ED-4E99993A9EF4
          age: 1

MATCH: wntdll.pdb and ntdll.dll
这条指令很明确的告诉我,不仅仅加载了,且是匹配的

可是为什么会报“No symbols for ntdll. Cannot continue.

在网上查了很多资料都没有什么帮助,后来在某篇文章上看到:该作者遇到了符号的其他问题,他得出结论说windbg的系统模块符号文件路径要设置为c:\symbols就解决了他面临的问题。

我想,我姑且也这样试试,于是我将我的符号路径改成如下:

然后,结果真的可以了,!address -summary命令正确执行和输出了。这是真的吗,真的是因为路径问题,真的是要c:\symbols吗?

根据我个人的经验,不太相信这个结论。我做了如下的验证步骤:

1、我注意到我原来的目录是两层的,会不会是因为这个原因呢,我将我原来的目录F:\Debug_Symbol\Symbols32调整为F:\Symbols32,执行相同命令,结果是报相同错误。

2、先删除现在c:\symbols目录,然后将我原来的符号目录考到c:盘,改名为symbols。然后执行!address -summary命令,我等待奇迹的出现,结果却是:

0:000> !address -summary

No symbols for ntdll. Cannot continue.

然后我懵逼了,但至少得出这样一个结论c:\symbols目录解决我了我的问题。然后继续折腾,结果在某次折腾里,我忘了修改windbg符号路径设置,但是目录下的相关符号被我移到了其他目录,也就是说F:\Debug_Symbol\Symbols32是空的,当调试时,ntdll.dll模块的符号文件是从微软符号服务器上拿的新的符号文件,也就是说问题的原因不在于目录而在于符号问见本身。

观察下两个符号文件:

原来的符号文件

 

这是新的,也是不出问题的符号文件

 

可以看到,这两个文件的的guid是一样的,但文件大小不一样,也就是说对应的版本还是不一样,旧的符号文件跟我系统里的ntdll其实是不匹配的。可是guid都一样,为什么不是正确的匹配版本呢。我懵逼了。

但是我们可以得出几点经验:

  • windbg告诉你符号文件匹配,但是那是不一定的
  • 以后遇到同样的问题,我们可以试着把原来所谓匹配的符号文件删除,然后执行同样的操作让windbg从新下载符号文件,可能会解决

其实,这种情况还会引发其他指令无法执行,比如:

  • !teb
    0:000> !teb
    TEB at 008ea000
    error InitTypeRead( TEB )...
  • !heap
    0:000> !heap
    Invalid type information

后续我会继续研究这个问题,同时希望看到此文的其他人,能帮我解答。

本描述了“RSDS”或“DS”类型的pdb(程序数据库)文件的格式,这些文件是由Miscrosoft的link.exe从版本7及更高版本发出的。

什么是PDB文件?

如果选择了/DEBUG选项或/DEBUG:FULL选项,则最新的Microsoft链接器将在链接时创建程序数据库(pdb)文件。pdb文件包含有关创建可执行文件的信息,还包含最新CodeView格式的符号信息。可执行文件包含本地计算机上pdb文件的路径和文件名,以及标识码,以便可以找到正确的pdb文件。pdb文件本身的格式和最新的CodeView格式都没有文档记录。据我所知,格式已经改变了两次,而且很可能会再次改变。Microsoft提供API来分析和报告其Debug Information Access(DIA)SDK中pdb文件的内容。

可执行文件中的PDB文件信息

链接器将在链接时生成的pdb文件的文件名及其在本地计算机上的路径放在可执行文件的“CODEVIEW”调试目录中。如果缺少这个,很可能是因为生成了一个dbg文件。例如,如果在链接后运行REBASE程序,则可能会发生这种情况。在这种情况下,pdb文件的路径和文件名将包含在dbg文件中。然后,dbg文件的文件名将出现在可执行文件的“MISC”调试目录中。