Windows下的确定性构建

32

最终目标是比较两个从完全相同的源代码在完全相同环境下编译出的二进制文件,并且能够确认它们功能上确实等价。

其中一个应用就是能够将QA时间集中于实际变更的内容,以及对变更的监控。

MSVC与PE格式在这方面自然使情况更加复杂。

到目前为止,我已经找到并消除了以下问题:

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

我解析PE文件,找到了所有这些内容的偏移量和大小,并且在比较二进制文件时忽略了这些字节范围。工作得很好(至少我运行了几个测试)。只要编译器版本、全部源代码和头文件都相同,我可以确定使用Win Server 2008构建的带有1.0.2.0版本的签名可执行文件与Win XP开发框架上构建的10.6.6.6版本的未签名可执行文件是相等的。这似乎适用于VC 7.1-9.0。 (适用于发布版本)

但是有一个警告。

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

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

感觉像是将带有编译标志的文件路径用作某种哈希值,使其成为链接二进制文件甚至影响编译代码片段的放置顺序。

因此,这里有一个分为三个部分的问题(概括为“现在怎么办?”):

  • 如果我试图做的事情违背了物理定律和微软公司政策,那我应该放弃整个项目回家吗?

  • 假设我解决了绝对路径问题(在政策层面或通过找到一个神奇的编译器标志),还有其他需要注意的事项吗?(例如__TIME__确实表示代码已更改,所以不介意这些内容没有被忽略)

  • 有没有一种方法可以强制编译器使用相对路径,或者欺骗编译器认为路径不是它所看到的那样?

  • 最后一项的原因是Windows文件系统非常烦人。你永远不知道删除数GB的源文件、目标文件和SVN元数据是否会因为某个流浪文件锁而失败。至少,在还有剩余空间的情况下,创建新根总是成功的。同时运行多个构建也是一个问题。虽然运行一堆虚拟机是一个解决方案,但它比较重量级。

    我想知道是否有一种方法可以为进程及其子进程设置虚拟文件系统,以便几个进程树同时看到仅对它们私有的不同的"C:\build"目录...一种轻量级的虚拟化解决方案...

    更新:我们最近在GitHub上开源了这个工具。请查看文档中的比较部分。


感谢peparser的“--compare”选项。但是,这部分“PDB ...文件路径”似乎并不适用于所有情况。如果我在链接器命令行中添加/PDBALTPATH:%_PDB%(这会导致实际路径从二进制映像中删除)后,重新构建VC++ 2015项目,则peparse将其报告为与原始构建“不等效”。 - dxiv
@dxiv 能否在 GitHub 上提交一个带有二进制文件的 bug 报告? - Eugene
已完成(https://github.com/smarttechnologies/peparser/issues/2),感谢您的关注。 - dxiv
5个回答

13

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

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

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

偶尔链接器可能会使一些PE节变得更大,而不会将其他内容排除在对齐之外。看起来它会在填充区内移动节边界--周围都是零,但因此我会得到具有1个字节差异的二进制文件。

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


1
以下是TLB时间戳的简单解决方法(仅在msvs_2015 + MIDL版本7.00.0555上进行了测试): peparser_with_tlb - Smalti

8

标准化构建路径

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

c:\buildXXXX

然后,当您比较例如 build0434build0398 时,只需预处理二进制文件,将所有出现的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实用程序

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

Windows XP Service Pack 2 Support Tools

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

"BinDiff使用一种特殊的比较例程 用于Win32可执行文件,掩盖 在执行比较时的各种构建时间戳字段。这允许将两个可执行文件标记为“近似相同”, 当文件真正相同时, 除了它们被构建的时间之外。”

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


不幸的是,源路径并没有以明文形式保存,我找不到任何关于它实际受到影响的信息,也无法确定是否可以安全地忽略它(毕竟错误否定比肯定更糟糕)。 - Eugene

3

你尝试过对可执行文件进行反汇编并比较吗?这样可以消除您提到的许多干扰细节,并使去除其他细节变得更加容易。


没有尝试过那个,即使它能够工作,也不能真正地可靠自动化...虽然这可能会带来一些启示,究竟有什么不同。我会尝试的,谢谢。 - Eugene
我相信你可以自动化软件反汇编。从命令行运行...这可能是一个不错的解决方案,具体取决于你在反汇编器输出方面遇到了哪些问题 ;) - Kieveli

3
有两种方法可以实现这一点:
  1. 使用subst.exe命令将一个驱动器映射到构建文件夹(这可能不可靠)。
  2. 如果subst.exe不起作用,则为每个构建文件夹创建共享并使用"net use"命令。这个方法几乎肯定会起作用。
在任一情况下,在开始特定的构建之前,您都需要为文件夹映射并重复使用相同的驱动器号,以便路径对编译器显示为相同。

我建议相同的做法,但是使用共同目录下的符号链接,例如C:\BUILD\XXX。 - Preet Sangha
NTFS 支持联接点。但您需要下载实用程序或者使用 Vista 以上版本。Windows 在技术上处理联接点的方式不同,所以就像 subst.exe 一样,这可能会有或没有效果。 - hythlodayr
交叉点会起作用,除非两个进程在同一时间运行时需要指向不同位置的相同路径。我猜它们会简化清理工作... - Eugene
我没有看到你在最后提到的“同时”的要求。为什么不通过顺序构建来简化问题呢? - hythlodayr
每晚都有很多东西要构建,每个构建可能需要几个小时(即使在一台相当不错的机器上)。白天也有很多构建。(而且这些都是干净的发布构建,而不是CC) - Eugene
显示剩余3条评论

1
我发现了一个额外的工具,可以帮助解决这个问题: Ducible on GitHub "这是一个制作可重现性便携式可执行文件(PE)和PDB的工具。"
它会直接修改提供的*.exe、*.dll和*.pdb文件,用确定性数据替换非确定性数据。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接