符号杂谈
符号服务器允许Windows上的开发人员工具自动查找符号。他们做得很好,以至于大多数开发人员都不必担心内部机制。然而,当事情出了问题时,了解它们是如何工作的是有帮助的,事实证明,这一切都非常简单。
查找PE文件
> dumpbin FractalX.exe /headers | find “date stamp”
4FFD0109 time date stamp Tue Jul 10 21:28:57 2012
> dumpbin FractalX.exe /headers | find “size of image”
147000 size of image
符号服务器共享中PE文件的路径格式为:
“%s\%s\%08X%x\%s” % (serverName, peName, timeStamp, imageSize, peName)
请注意,时间戳始终打印为8位数字(根据需要使用前导零),使用大写字母表示“A”到“F”(如果符号服务器区分大小写,则很重要),而图像大小将根据需要使用较少的数字以小写形式打印。
旁白:如果两个构建之间的差异很小,那么大小可能不会改变,而peName肯定不会改变,所以唯一阻止两个构建具有相同符号服务器地址的就是时间戳。这意味着降低时间戳的精度(比如从1秒缩短到24小时)可能会导致一些相当严重的文件名冲突。最后可能会用第二个条目覆盖符号服务器中的第一个条目。你知道,假设一下。
我的符号服务器在c:\MySyms中(通常在共享服务器上,但这是我的个人笔记本电脑),因此上面检查的文件的完整路径是:
c:\MySyms\fractalx.exe\4FFD0109147000\FractalX.exe
很简单。在我的情况下,我使用符号库.exe当我添加文件时,'s/compress选项(它节省了很多空间)。压缩文件通过用下划线替换最后一个字符来表示,因此实际路径如下:
c:\MySyms\fractalx.exe\4FFD0109147000\FractalX.ex_
这是一个很好的测试,可以确保PE文件已正确添加到符号服务器,但这不是一个非常现实的用例,因为我们使用PE文件来获取检索PE文件所需的值。更常见的情况是,您将拥有一个小型转储或xperf ETL文件,该文件将包含一系列模块名称、链接时间戳、图像大小三元组,这些将在分析时用于检索PE文件。对于小型转储文件,有一个包含相关数据的minidump_模块结构数组。请注意,符号服务器共享的布局可能要复杂得多。您应该使用api(稍后讨论)来检索PE文件–上面的技术纯粹是为了排除故障。
如果您有一个链接时间戳并希望将其转换为日期,则可以使用以下Python一行程序批处理文件:
python -c "import time; print time.ctime(int('%1',16))"
另外一个怪癖是,在微软的链接器/调试器工具链的某个地方,这些工具链用于小写PE文件的名称。这意味着,如果您为符号服务器使用区分大小写的文件系统(就像Chrome那样),那么您必须使用小写文件名来上载符号。Chrome团队很难发现这一点。PDB名称是从PE文件中提取的,大小写保持不变,因此它们只需匹配。我再也无法重现这种行为-UIforETW.exe以混合大小写方式上载到区分大小写的Google存储中,这样就可以了。
查找PDB文件
在分析客户崩溃转储以获得组装说明时,查找PE文件非常方便,但实际上比这更重要。小型转储文件不一定记录足够的信息来检索PDB文件。在这些情况下,工具检索PE文件,然后查找PE文件以获取检索PDB文件所需的信息。我们可以再次使用dumpbin从PE文件中提取此信息:
> dumpbin FractalX.exe /headers | find “Format:”
4FFD0109 cv 56 000B9308 B7B08 Format: RSDS, {6143E0D1-9975-4456-AC8E-F24C8777336D}, 1, FractalX.pdb
RSDS后面的长十六进制数是一个GUID,后面的数字(一个32位十进制数,但在本例中只有“1”)称为“age”。PDB文件名也列在这里。它们一起唯一地标识了PDB文件的特定版本。符号服务器共享中PDB文件的路径格式为:
“%s\%s\%s%x\%s” % (serverPath, pdbName, guid, age, pdbName)
有趣的是,请注意,我使用%x来打印年龄,但在上一段中,我将年龄描述为十进制数。好吧,PDB age(只是衡量同一PDB被重用了多少次)只是一个32位数字,但是dumpbin以十进制格式打印它,而符号服务器希望它是十六进制的。祝大家一致!这意味着,如果解析dumpbin输出,则需要将age转换为整数并将其打印为十六进制。如果你弄错了,那么这个bug不会出现,直到你遇到一个10岁以上的PDB。精彩的。
与PE文件一样,最后一个下划线表示文件被压缩的symstore.exe. 上面列出的PDB在符号服务器上的路径如下所示:
c:\MySyms\FractalX.pdb\6143E0D199754456AC8EF24C8777336D1\FractalX.pd_
很简单。生成GUID和age的算法是,每当您进行重建时(每当生成新的PDB时),就会创建一个新的GUID并将age设置为1。无论何时进行部分构建,PDB都会使用新的调试信息进行更新,并且age也会增加。就这样–使用PE名称、链接时间戳和图像大小来查找PE(如果尚未加载),然后使用GUID、age和PDB文件名来查找PDB文件。请注意,符号服务器共享的布局可能要复杂得多。您应该使用api(稍后讨论)来检索PDB文件–上面的技术纯粹是为了排除故障。
添加到符号服务器
如果你在Windows上发布软件,你应该有一个符号服务器。该符号服务器应该包含每种产品的PE文件和PDB文件。如果你不这样做,那么你对你自己或你的客户都是一种伤害。你还应该有一个符号服务器,用于公司里任何人都可能最终运行的所有内部构建。如果程序可能崩溃,并且您希望能够调查崩溃,那么将符号放在符号服务器上。如果您担心内部版本会占用太多空间,请将它们放在单独的符号服务器上,并偶尔清除旧文件。您还应该确保您的生成计算机正在运行源索引,这样当您在旧版本的软件中调试崩溃时,您将自动获得正确的源文件。幸运的是我已经写过了。向符号服务器添加文件是最简单的。将sourcedir设置为指向包含要添加的文件的目录,并将dest设置为符号服务器目录,所有需要符号的人都可以访问该目录。然后运行以下命令:
symstore add /f %sourcedir%\*.dll /s %dest% /t prodname /compress
symstore add /f %sourcedir%\*.exe /s %dest% /t prodname /compress
symstore add /f %sourcedir%\*.pdb /s %dest% /t prodname /compress
如果您运行的是区分大小写的文件系统,那么之后您需要将PE文件名的大小写改为小写—抱歉。
就这样。如果希望递归地添加文件,请使用/r,有关详细信息,请参阅帮助。如果现有的符号服务器未被压缩,或者希望在添加到符号服务器时单独执行压缩步骤,则makecab命令(随Windows一起提供)就可以实现此目的。这是Chrome过去使用的命令行类型:
makecab /D CompressionType=LZX /D CompressionMemory=21 chrome.dll.pdb
这将生成一个压缩的chrome.dll.pd_下载到本地符号缓存时将自动解压缩的文件。
另一个选择是使用压缩.exe从Windows Server 2003资源工具包工具。但是要小心。compress/help说LZX是默认值,但不是这样。所以一定要使用compress-ZX,否则将得到SymSrv无法解压缩的压缩文件。
显然是makecab,压缩.exe,和symstore都有其输入文件必须小于2 GiB的限制。这是Chrome统一版的一个问题chrome.dll.pdb目前(2019年10月)约为2.5GB,因此无法压缩。哎呀。pigz可以处理4gib的输入文件,但是生成CAB文件的版本还没有开源。我们最终修复了这个问题,通过前面的微软压缩,而不是使用HTTP压缩——我们在gsutil命令行中添加了-Z,这样pdb在上传时将被gzip压缩。
如果您通过HTTP使符号服务器可用,请确保使用https,以确保下载的完整性。
或者,如果您试图创建一个可公开访问的符号服务器,只需遵循以下简单的说明。
使用符号服务器
如何让开发工具使用symbol server的具体细节各不相同,但一种几乎通用的方法是将_NT_symbol_PATH环境变量(此处和此处的高级用法)设置为如下所示:
_NT_SYMBOL_PATH=SRV*c:\symbols*c:\MySyms;SRV*c:\symbols*https://msdl.microsoft.com/download/symbols;SRV*c:\symbols*https://chromium-browser-symsrv.commondatastorage.googleapis.com
这告诉工具首先在本地缓存(c:\symbols)中查找,然后在符号服务器c:\MySyms中查找。如果在c:\MySyms中找到符号,则将它们复制(并解压缩)到c:\symbols。如果这些都不起作用,那么微软基于web的符号缓存和Chrome的符号缓存将遵循相同的过程(包括相同的缓存目录)。请注意,在处理压缩符号时需要本地符号缓存。请注意,一些符号服务器,如Chrome和Microsofts,可以通过https和http访问。当https作为选项可用时,您应该始终使用它,否则中间人攻击可能会在下载和使用这些pdb时使用格式错误的pdb或源索引命令来执行任意代码。
微软在某些地方仍然使用http列出了他们的符号服务器,但是https是有效的,应该是首选。
_NT_SYMBOL_PATH的SRV*部分很重要,文档记录不清楚,而且很难理解。我的理解是,通过对stackoverflow的讨论,SRV*告诉我们symsrv.dll将以下路径或url视为符号服务器,而不仅仅是松散文件的集合。因此,如果_NT_SYMBOL_PATH是c:\symbols,那么dbghelp或symsrv可能会递归地搜索目录结构中的符号,但是如果_NT_SYMBOL_PATH是SRV*c:\symbols,那么它将以非常结构化和高效的方式进行搜索。如果符号路径是
SRV*c:\symbols*https://msdl.microsoft.com/download/symbols
然后symsrv将首先使用快速高效的符号服务器算法在c:\symbols中查找,然后(如果没有找到符号)在微软的symbol server中进行同样有效的搜索。您应该更喜欢使用SRV*和符号服务器布局,而不是非结构化符号。
以编程方式检索符号
通常,您使用的调试器和探查器将知道如何使用符号服务器,但有时您可能需要编写代码来下载符号—可能您正在编写调试器或探查器。在我的例子中,我有一个网页,列出了几十个微软dll的guid、age和PDB名称,这些dll来自于我们需要符号的几十个Windows版本。编写代码来下载所有这些符号是很简单的—比为其他版本的Linux获取符号要容易几个数量级。我需要做的简短解释是“调用symfindfeinpath”。为了证明这是多么容易,我决定给出一个稍微长一点的解释。示例代码作为UIforETW的一部分在github上提供,它接受GUID、age和pdb名称,或者日期戳、大小和pename,并从符号服务器下载PE或pdb文件。最大的代码块是用来解析GUID的——实际下载很简单。
测试定义使用已知良好的GUID、age、name和symbol server。注释掉定义使用此命令从指定的符号服务器下载任意符号。如果遇到任何困难,请在调试器中运行此程序–dbghelp将在调试器输出窗口中打印诊断结果。唯一的问题是dbghelp.dll以及symsrv.dll必须和你的工具在同一个目录中-把它们放在你的路径中是不可靠的。
在前面提到的最新版本的源代码可以在这里找到。symchk工具(在Windows调试器工具包中提供)在传递PE文件时也会下载PDB文件–使用/v选项获取有关PDB文件下载位置的信息以及其他信息。如果有需要符号的.dmp文件或PE文件,symchk更方便;而如果有GUID、age和PDB文件名,则RetrieveSymbols更方便。
用windbg诊断符号问题
如果您有一个小型转储,但它的符号没有加载,那么我建议将该小型转储加载到windbg并使用其诊断:
!sym noise–打印有关尝试获取符号的详细信息
lmv m MyModule–从崩溃转储的模块列表中打印一条记录,包括其名称、时间戳、图像大小以及PDB所在的位置(如果找到)
!lmi MyModule–打印模块的头信息–仅当PE文件已加载时才有效,这是加载符号的先决条件
dumpbin摘要
“%VS120COMNTOOLS%..\..\VC\vcvarsall.bat”–这会将dumpbin的目录添加到路径中
dumpbin FX.exe /headers | find “date stamp”–查找PE文件的链接时间戳
dumpbin FX.exe /headers | find “size of image”–查找PE文件的图像大小
dumpbin FractalX.exe /headers | find “Format:”–查找PE文件的PDB文件的GUID、age和文件名