Windows 下的确定性构建

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

The ultimate goal is comparing 2 binaries built from exact same source in exact same environment and being able to tell that they indeed are functionally equivalent.

为此的一个应用程序是将 QA 时间集中在发布之间实际更改的事情上,以及一般的更改监控.

One application for this would be focusing QA time on things that were actually changed between releases, as well as change monitoring in general.

MSVC 与 PE 格式的结合自然使这很难做到.

MSVC in tandem with PE format naturally makes this very hard to do.

到目前为止,我发现并消除了这些东西:

So far I found and neutralized those things:

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

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

I parse PE, find offsets and sizes for all those things and ignore byte ranges when comparing binaries. Works like charm (well, for the few tests I've run it). I can tell that signed executable with version 1.0.2.0 built on Win Server 2008 is equal to unsigned one, of version 10.6.6.6, build on my Win XP dev box, as long as compiler version and all sources and headers are the same. This seems to work for VC 7.1 -- 9.0. (For release builds)

有一个警告.

两个构建的绝对路径 必须相同 必须具有相同的长度.

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

cl.exe converts relative paths to absolute ones, and puts them right into objects along with compiler flags and so on. This has unproportional effects on whole binary. One character change in path will result in one byte changed here and there several times over whole .text section (however many objects were linked I suspect). Changing length of the path results in significantly more differences. Both in obj files and in linked binary.

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

Feels like file path with compile flags is used as some kind of hash, which makes it into linked binary or even affects placement order of unrelated pieces of compiled code.

所以这是由 3 部分组成的问题(总结为现在怎么办?"):

So here is the 3-part question (summarized as "what now?"):

  • 我是否应该放弃整个项目并回家,因为我正在尝试做的事情违反物理定律和 MS 的公司政策?

  • Should I abandon the whole project and go home because what I am trying to do breaks laws of physics and corporate policy of MS?

假设我处理了绝对路径问题(在策略级别或通过找到一个神奇的编译器标志),还有什么我应该注意的事情吗?(像 __TIME__ do 意味着更改的代码,所以我不介意那些不被忽略的)

Assuming I handle absolute path issue (on policy level or by finding a magical compiler flag), are there any other things I should look out for? (things like __TIME__ do mean changed code, so I don't mind those not being ignored)

有没有办法强制编译器使用相对路径,或者欺骗它认为路径不是它本来的样子?

Is there a way to either force compiler to use relative paths, or to fool it into thinking the path is not what it is?

最后一个的原因是非常烦人的 Windows 文件系统.你永远不知道什么时候删除几场演出价值的源和对象,svn 元数据会因为流氓文件锁定而失败.至少在剩余空间时创建新根总是成功的.同时运行多个构建也是一个问题.运行一堆虚拟机虽然是一种解决方案,但却是一项相当繁重的工作.

Reason for the last one is beautifully annoying Windows file system. You just never know when deleting several gigs worth of sources and objects and svn metadata will fail because of a rogue file lock. At least creating new root always succeeds while there is space left. Running multiple builds at once is an issue too. Running bunch of VMs, while a solution, is a rather heavy one.

我想知道是否有办法为进程及其子进程设置虚拟文件系统,以便多个进程树将看到不同C:uild"目录,仅对它们私有,所有同时......各种轻量级虚拟化......

I wonder if there is a way to setup a virtual file system for a process and its children so that several process trees will see different "C:uild" dirs, private to them only, all at the same time... A light-weight virtualization of sorts...

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

UPDATE: we recently opensourced the tool on GitHub. See Compare section in documentation.

推荐答案

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

I solved this to an extent.

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

Currently we have build system that makes sure all new builds are on the path of constant length (builds/001, builds/002, etc), thus avoiding shifts in the PE layout. After build a tool compares old and new binaries ignoring relevant PE fields and other locations with known superficial changes. It also runs some simple heuristics to detect dynamic ignorable changes. Here is full list of things to ignore:

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

偶尔链接器会使一些 PE 部分变大,而不会抛出任何其他不对齐的内容.看起来它在填充内移动了部分边界――无论如何它都是零,但因此我会得到具有 1 个字节差异的二进制文件.

Once in a while linker would make some PE sections bigger without throwing anything else out of alignment. Looks like it moves section boundary inside the padding -- it is zeros all around anyway, but because of it I'll get binaries with 1 byte difference.

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

UPDATE: we recently opensourced the tool on GitHub. See Compare section in documentation.

相关文章