同一源代码构建的可执行文件是否可能功能上存在差异?

12

最近,我的一位同事说了这样一句话:“由构建服务器从相同源代码生成的连续APK(可执行文件)可能不是相同的”。讨论的背景是是否对X版进行的QA测试也适用于由相同的构建服务器(以相同方式配置)从相同的源代码执行的Y版。

我认为生成的可执行文件可能由于各种因素(例如时间戳等)而不同,但问题在于它们是否可以在功能上有所不同。

我唯一能想到的相同源代码可能产生不同功能的情况是多线程问题:如果多线程代码同步不正确,编译时执行的不同重新排序/优化操作可能会影响这个同步不良的代码并更改其功能行为。

我的问题如下:

  1. 是否真的存在从相同源代码由同一构建服务器执行的连续构建可能在功能上存在差异?
  2. 如果问题1是真的,这些差异是否仅限于未正确同步的多线程代码?
  3. 如果问题2是错误的,还有哪些部分会发生变化?

欢迎提供任何相关资料链接。


1
通常情况下,如果使用相同版本的构建工具构建相同的源代码,则生成的字节码是相同的。除非...嗯,除非您进行一些诡计,例如将某些构建时间戳放入生成的元文件中,并将其打包到最终的JAR文件中。您还可以读取该元文件,并在特定时间戳上使用其他代码执行其他操作。 - Seelenvirtuose
我必须承认在这个问题上我是个门外汉,因此可能会严重出错和误解。但我的简单逻辑思维是:如果一个源代码能够在构建时处理一个时间戳,那为什么不能在代码的某些分支中检查时间是否早于或晚于预定时间,并根据结果让构建者执行不同的操作呢? - Mok-Kong Shen
6个回答

5

我认为不同的功能可能仅由环境差异引起,或者您正在使用某些第三方库的快照版本,因此在一段时间后进行了更新。

一些建议: 如果可以重建它,请使用构建工具的详细模式(例如Maven中的-X),并将输出逐行与某些diff程序进行比较。


5
在某些情况下是有可能的。我会假设您正在使用Gradle来构建Android应用程序。
案例1:您正在使用包括版本通配符的第三方依赖项,例如:
compile somelib.1+
在这种情况下,依赖关系有可能发生变化,因此强烈建议使用明确的依赖项版本。
案例2:您正在使用Gradle的buildConfigFields向您的应用程序注入环境信息。 这些值将被注入到您的应用程序的BuildConfig类中。 根据您如何使用这些值,应用程序行为可能会因连续构建而有所不同。
案例3:在连续构建之间更新CI上的JDK。虽然可能性极小,但有可能应用程序行为会因编译方式而有所不同。例如,您可能遇到了JDK中的边缘情况,在后续版本中得到解决,导致先前可以正常工作的代码表现出不同的行为。
我想这回答了您的第一个问题和第二个问题。
编辑:对不起,我认为我错过了您的OP中的一些重要信息。我的第二个案例是您的“例如不同的时间戳”的示例,而案例3则违反了您的“以相同的方式配置”。尽管如此,我还是会把答案留在这里。

4
如果同一台机器/配置上的相同源代码可能产生不同的结果,那么我们所知道的编程可能就不可能存在。
当语言级别、操作系统或其他依赖项发生变化时,总有一种选择会导致事情崩溃。如果所有这些都改变了构建的时间,你必须做一些根本性的错误。
在使用android / gradle时,导致不同行为或错误的一个可能原因是在您的build.gradle文件中使用+来表示库版本。这就是为什么应该避免这样做的原因,因为连续的构建可能会获取更新/不同的版本,因此您将拥有不同的源代码,从而可以创建一个功能不同的可执行文件。
良好的构建应始终是可重复的。这意味着在相同的配置下它应该具有相同的结果。如果不是这样,你永远不能依赖任何东西,并且必须对所有内容进行完全回归测试。
[...]由同一构建服务器从相同的源代码执行的连续构建可能具有不同的功能
不。如上所述,如果您使用相同的版本、相同的源代码,它应该产生相同的行为。除非你做了什么非常错误的事情。
[...]这些差异是否仅限于未正确同步的多线程代码?
这将意味着您的编译器存在错误。虽然这是可能的,但极不可能。
[...]还有哪些部分可以改变?
除了时间戳和构建号之外,在给定相同的源代码和配置的情况下,没有其他部分应该发生变化。
在构建中包含单元测试(和其他测试)总是一个好主意。这样你就可以测试特定的行为是否与每个构建相同。

2
除了时间戳和版本号,不应该有任何其他更改。我不知道这是否适用于您正在使用的编译器,但对于其他编译器,它肯定不适用。例如,C#编译器设计上在每个新二进制文件中包含一个GUID,使它们不同;GCC可以(可能)在不同的运行中选择(随机)不同的优化。请参见https://superuser.com/questions/639351/does-recompiling-a-program-produce-a-bit-for-bit-identical-binary。 - hmijail
@hmijail 这个问题涉及到Java和Android,虽然我不确定是否有这样的工具。另外,我认为可以合理地假设GUID和构建号非常相似,因为两者都会随着每次构建而改变。 - David Medenjak

1
除了以下几点,它们应该是相同的:
- 构建系统中存在线程/优化问题。 - 构建环境中的硬件故障 CPU/RAM/HDD 问题。 - 构建系统本身或构建脚本中的时间/平台相关代码。
因此,如果您在完全相同的硬件上使用完全相同的构建系统版本和相同的操作系统版本构建完全相同的代码,并且您的代码不特别依赖于构建时间结果,则结果应该是相同的。它们甚至应该具有完全相同的校验和和大小。
此外,仅当您的代码不依赖于在构建时从互联网下载的外部模块时,结果才相同,例如 Gradle/Maven - 您无法保证这些库是相同的,因为它们不在版本控制中。而且,可能存在模块版本未精确指定(如 2.0.+)的依赖关系,因此如果维护者更新了此模块,则您的构建系统将使用更新后的模块 - 因此基本上您的构建是由不同的源代码生成的。
正如某些人提到的那样,在构建服务器上使用单元测试是一个好习惯,以确保您的构建稳定且不包含明显的错误。

0

虽然这个问题涉及Java/Android,但Jon Skeet在博客中提到了不同的C#解析器对一些Unicode字符的处理方式不同,主要是由于Unicode字符数据库的更改。

在他的例子中,蒙古语元音分隔符(U+180E)被认为是空格字符或允许在标识符中使用的字符,从而导致变量赋值产生不同的结果。


-1

这是完全可能的。您可以构建一个示例程序,每次启动时都会以不同的功能方式运行。

想象一下策略设计模式,它允许您在运行时选择算法,并根据 RNG 加载一个算法。


这与生成不同的代码没有区别。在您的示例中,代码旨在以不同的方式行为,无论构建如何启动。 - Daniele Segato

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