Windows下的确定性构build

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

其中一个应用就是将QA时间集中在发布之间实际发生变化的事情上,以及一般的更改监视。

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

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

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

我parsingPE,find所有这些东西的偏移量和大小,并比较二进制文件时忽略字节范围。 像魅力一样工作(好吧,我已经运行了几个testing)。 我可以看出,只要编译器版本和所有源代码和头文件都相同,在Win Server 2008上构build的版本为1.0.2.0的已签名可执行文件就等于未签名的版本10.6.6.6,即构build在我的Win XP开发箱上。 这似乎适用于VC 7.1 – 9.0。 (用于发布版本)

有一个警告。

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

cl.exe将相对path转换为绝对path,并将它们与编译器标志一起放入对象中等等。 这对整个二进制文件有不成比例的影响。 path中的一个字符改变将导致一个字节在这里和那里改变整个.text部分(然而,许多对象被链接,我怀疑)。 改变path的长度导致明显更多的差异。 在obj文件和链接的二进制文件中。

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

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

  • 我应该放弃整个项目,回家,因为我正在努力打破MS的物理定律和公司政策?

  • 假设我处理绝对path问题(在策略级别或通过查找一个神奇的编译器标志),还有什么其他的东西我应该注意? (像__TIME__ 这样的东西确实意味着改变了代码,所以我不介意那些不被忽略的东西)

  • 有没有办法强制编译器使用相对path,或欺骗它认为path不是它是什么?

最后一个原因是美观的Windows文件系统烦人。 你只是永远不知道什么时候删除一些价值的源和对象和svn元数据将失败,因为stream氓文件locking。 至less创造新的根总是成功,而有剩余的空间。 同时运行多个构build也是一个问题。 运行一堆虚拟机,而一个解决scheme,是一个相当沉重的。

我想知道是否有办法为一个进程和它的subprocess设置一个虚拟文件系统,以便几个进程树将同时看到不同的 “C:\ build”目录,只对它们是私有的。重量级的虚拟化…

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

Solutions Collecting From Web of "Windows下的确定性构build"

标准化构建路径

一个简单的解决方案就是在你的构建路径上进行标准化,所以它们总是这样的形式,例如:

c:\buildXXXX 

然后,比较build0434build3988 ,只需要预处理二进制文件就可以把所有的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支持工具

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

“BinDiff使用Win32可执行文件的特殊比较例程,在执行比较时可以屏蔽两个文件中的各种编译时间戳字段,当文件真正相同时,可以将两个可执行文件标记为”Near Identical“,除了他们建造的时间。“

但是,这听起来像你可能已经在做一个bindiff.exe的超集。

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

目前我们已经建立了系统,确保所有的新建都在恒定的路径上(build / 001,build / 002等),从而避免了PE布局的变化。 建立一个工具后,比较旧的和新的二进制文件忽略相关的PE领域和其他已知的表面变化的位置。 它也运行一些简单的启发式检测动态可忽略的变化。 这里是要忽略的事情的完整列表:

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

偶尔链接器会使一些PE部分更大而不会丢失任何其他的不对齐。 看起来它在填充内移动了部分边界 – 无论如何它都是零,但是因为它会得到1个字节差异的二进制文件。

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

你有没有尝试反汇编可执行文件并比较反汇编? 这应该消除你提到的许多令人分心的细节,并且使其他人更容易移除。

有没有办法强制编译器使用相对路径,或欺骗它认为路径不是它是什么?

你有两种方法来做到这一点:

  1. 使用subst.exe命令并将一个驱动器号映射到生成文件夹(这可能不可靠)。
  2. 如果subst.exe不起作用,则为每个构建文件夹创建共享并使用“net use”命令。 这一个几乎肯定应该工作。

无论哪种情况,在开始特定的构建之前,您都要为文件夹映射和重用相同的驱动器号,以便路径看起来与编译器相同。