Windows下的确定性构建

时间:2009-07-25 01:01:32

标签: c++ windows visual-c++ portable-executable

最终目标是在完全相同的环境中比较从完全相同的源构建的2个二进制文件,并且能够告诉它们确实在功能上是等效的。

这方面的一个应用是将QA时间集中在发布之间实际发生变化的事情上,以及一般的变更监控。

与MS格式串联的MSVC自然会使这很难做到。

到目前为止,我发现并中和了这些事情:

  • PE时间戳和校验和
  • 数字签名目录条目
  • 调试器部分时间戳
  • PDB签名,年龄和文件路径
  • 资源时间戳
  • VS_VERSION_INFO资源中的所有文件/产品版本
  • 数字签名部分

我解析PE,查找所有这些内容的偏移量和大小,并在比较二进制文件时忽略字节范围。像魅力一样工作(好吧,我运行它的少数测试)。我可以说,只要编译器版本和所有源和头都相同,在Win Server 2008上构建的版本1.0.2.0的已签名可执行文件等于版本10.6.6.6的无符号可执行文件,构建在我的Win XP开发框中。这似乎适用于VC 7.1 - 9.0。 (对于发布版本)

有一点需要注意。

两个版本的绝对路径 必须相同 必须具有相同的长度。

cl.exe将相对路径转换为绝对路径,并将它们与编译器标志一起放入对象中,依此类推。这对整个二进制文件具有不成比例的影响。路径中的一个字符更改将导致在此处更改一个字节,并且在整个.text部分上有几次(但是我怀疑链接了很多对象)。改变路径的长度会导致明显更多的差异。 obj文件和链接二进制文件。

感觉像带有编译标志的文件路径被用作某种哈希,这使得它成为链接二进制文件甚至影响不相关的编译代码片段的放置顺序。

所以这是三部分的问题(总结为“现在是什么?”):

  • 我是否应该放弃整个项目并回家,因为我想要做的事情违反了MS的物理法则和公司政策?

  • 假设我处理绝对路径问题(在策略级别或找到一个神奇的编译器标志),还有其他我需要注意的事项吗? (例如__TIME__ 意味着改变了代码,所以我不介意那些不被忽略的代码)

  • 有没有办法强制编译器使用相对路径,或者欺骗它认为路径不是它的原因?

最后一个原因是令人讨厌的Windows文件系统。你永远都不知道什么时候删除几个有价值的源和对象,并且svn元数据会因为流氓文件锁而失败。至少创建新根总是成功,而剩下空间。一次运行多个构建也是一个问题。运行一堆虚拟机虽然是一个解决方案,但却是一个相当繁重的虚拟机。

我想知道是否有办法为进程及其子进程设置虚拟文件系统,以便多个进程树将看到不同的“C:\ build”dirs,仅对它们是私有的,同时...轻量级的虚拟化......

更新:我们最近在GitHub上开源了该工具。请参阅文档中的比较部分。

5 个答案:

答案 0 :(得分:10)

我在一定程度上解决了这个问题。

目前我们已经构建了一个系统,可以确保所有新构建都在常量长度的路径上(builds / 001,builds / 002等),从而避免PE布局的变化。构建之后,工具会比较旧的和新的二进制文件,忽略相关的PE字段和其他具有已知表面变化的位置。它还运行一些简单的启发式方法来检测动态可忽略的变化。以下是要忽略的完整列表:

  • PE时间戳和校验和
  • 数字签名目录条目
  • 导出表格时间戳
  • 调试器部分时间戳
  • PDB签名,年龄和文件路径
  • 资源时间戳
  • VS_VERSION_INFO资源中的所有文件/产品版本
  • 数字签名部分
  • 嵌入式类库的MIDL虚荣存根(包含时间戳字符串)
  • __ FILE __,_ _ D ____和__TIME__宏用作文字字符串时(可以是宽字符或窄字符)

偶尔链接器会使一些PE部分更大而不会抛出任何其他不对齐的东西。看起来它在填充内部移动了部分边界 - 无论如何它都是零,但由于它,我将获得1字节差异的二进制文件。

更新:我们最近在GitHub上开源了该工具。请参阅文档中的比较部分。

答案 1 :(得分:8)

标准化构建路径

一个简单的解决方案是标准化您的构建路径,因此它们始终是形式,例如:

c:\buildXXXX

然后,当您将 build0434 build0398 进行比较时,只需预处理二进制文件即可将 build0434 的所有内容更改为 build0398 即可。选择一种您不太可能在实际源/数据中显示的模式,但编译器/链接器嵌入到PE中的那些字符串除外。

然后你就可以进行常规差异分析了。通过使用相同长度的路径名,您不会移动任何数据并导致误报。

Dumpbin实用程序

另一个提示是使用 dumpbin.exe (随MSVC一起提供)。使用 dumpbin / all 将二进制文件的所有详细信息转储到文本/十六进制转储。这可以让人们更清楚地看到正在发生变化的地方。

例如:

dumpbin /all program1.exe > program1.txt
dumpbin /all program2.exe > program2.txt
windiff program1.txt program2.txt

或使用您喜欢的文字差异工具,而不是Windiff。

Bindiff实用程序

您可能会发现Microsoft的 bindiff.exe 工具很有用,可以在此处获取:

Windows XP Service Pack 2 Support Tools

它有一个/ v选项,用于指示它忽略某些二进制字段,例如时间戳,校验和等:

  

“BinDiff使用特殊的比较例程   用于掩盖的Win32可执行文件   出来的各种构建时间戳字段   执行时的两个文件   相比。这允许两个可执行文件   要标记为“近乎相同”的文件   当文件完全相同时,   除了他们建造的时间。“

然而,听起来像你可能已经在做 bindiff.exe所做的超集。

答案 2 :(得分:3)

  

有没有办法要么强迫   编译器使用相对路径,或   愚弄它认为道路不是   它是什么?

您有两种方法可以做到这一点:

  1. 使用subst.exe命令并将驱动器号映射到构建文件夹(这可能不可靠)。
  2. 如果subst.exe不起作用,则为每个构建文件夹创建共享并使用“net use”命令。这个几乎肯定应该有效。
  3. 在任何一种情况下,您都要在开始特定构建之前映射并重用文件夹的相同驱动器号,以使路径看起来与编译器完全相同。

答案 3 :(得分:2)

您是否尝试过反汇编可执行文件并比较反汇编?这应该会删除你提到的许多令人分心的细节,并使删除其他更容易。

答案 4 :(得分:1)

我遇到了一个额外的工具来帮助解决这个问题: Ducible on GitHub

“这是一个使可移植可执行文件 (PE) 和 PDB 的构建可重现的工具。”

它修改了提供的 *.exe、*.dll 和 *.pdb 文件,将非确定性数据替换为确定性数据。