关于C#:Windows下的确定性构建

Deterministic builds under Windows

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

为此,一个应用程序将把质量检查时间集中在版本之间实际发生的更改以及总体上的更改监视上。

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文件和链接的二进制文件中均如此。

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

这是一个三部分的问题(概括为"现在如何?"):

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

  • 假设我处理绝对路径问题(在策略级别或通过找到一个神奇的编译器标志),还有其他需要注意的事情吗? (诸如__TIME__之类的东西确实意味着代码已更改,因此我不介意那些未被忽略的内容)

  • 是否有一种方法可以强制编译器使用相对路径,或者让编译器愚弄它以为该路径不是它的真实路径?

最后一个原因是令人讨厌的Windows文件系统。您只是永远不知道何时删除价值几千美元的源和对象,并且由于流氓文件锁定,svn元数据将失败。至少有剩余空间时,创建新的根总是总是成功的。一次运行多个构建也是一个问题。运行一堆虚拟机虽然是一种解决方案,但却是一个沉重的负担。

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

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


我在某种程度上解决了这个问题。

当前,我们拥有一个构建系统,该系统可确保所有新构建都位于固定长度的路径上(builds / 001,builds / 002等),从而避免了PE布局的变化。构建完成后,工具会比较旧的和新的二进制文件,而忽略相关的PE字段和其他具有已知表面变化的位置。它还运行一些简单的试探法来检测动态的可忽略的变化。这是要忽略的事情的完整列表:

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

偶尔,链接器将使某些PE部分变大,而不会导致其他任何对齐问题。看起来它在填充内移动了节边界-无论如何它始终为零,但是由于这个原因,我将获得具有1个字节差的二进制文件。

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


标准化构建路径

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

1
c:\\buildXXXX

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

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

垃圾桶实用程序

另一个提示是使用dumpbin.exe(MSVC附带)。使用dumpbin / all将二进制文件的所有详细信息转储到text / hex转储中。这样可以更清楚地看到正在发生什么/在哪里更改。

例如:

1
2
3
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 uses a special compare routine
for Win32 executable files that masks
out various build time stamp fields in
both files when performing the
compare. This allows two executable
files to be marked as"Near Identical"
when the files are truely identical,
except for the time they were built."

但是,听起来您可能已经在做bindiff.exe的超集。


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?

您可以通过两种方式执行此操作:

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


    您是否尝试过反汇编可执行文件并比较反汇编?这应该删除了您提到的许多分散注意力的细节,并使删除其他细节变得更加容易。


    我遇到了一个额外的工具来帮助解决此问题:
    在GitHub上可疑

    "这是可复制可移植可执行文件(PE)和PDB的工具。"

    它就地修改提供的* .exe,*。dll和* .pdb文件,用确定性数据替换非确定性数据。