为什么在gcc中只改变注释的两个程序二进制文件不完全匹配?

112

我创建了两个C程序

  1. 程序1

    int main()
    {
    }
    
  2. 程序 2

  3. int main()
    {
    //Some Harmless comments
    }
    
    AFAIK,编译时编译器(gcc)应忽略注释和冗余空格,因此输出应该是相似的。但是当我检查输出二进制文件的md5值时,它们不匹配。我还尝试了使用优化标志 -O3 和 -Ofast 进行编译,但仍然无法匹配。请问这里发生了什么?编辑:精确命令及其md5sums如下(t1.c 是程序 1,t2.c 是程序 2)。
    gcc ./t1.c -o aaa
    gcc ./t2.c -o bbb
    98c1a86e593fd0181383662e68bac22f  aaa
    c10293cbe6031b13dc6244d01b4d2793  bbb
    
    gcc ./t2.c -Ofast -o bbb
    gcc ./t1.c -Ofast -o aaa
    2f65a6d5bc9bf1351bdd6919a766fa10  aaa
    c0bee139c47183ce62e10c3dbc13c614  bbb
    
    
    gcc ./t1.c -O3 -o aaa
    gcc ./t2.c -O3 -o bbb
    564a39d982710b0070bb9349bfc0e2cd  aaa
    ad89b15e73b26e32026fd0f1dc152cd2  bbb
    

    是的,在使用相同标志编译的多个版本之间,md5sums匹配。

    顺便说一下,我的系统是gcc(GCC)5.2.0Linux 4.2.0-1-MANJARO#1 SMP PREEMPT x86_64 GNU / Linux


17
请提供您使用的确切命令行标志。例如,二进制文件中是否包含调试信息?如果有,显然行号的变化会对其产生影响... - Jon Skeet
4
相同代码的多次构建是否会产生相同的MD5校验和? - unenthusiasticuser
3
我无法重现这个问题。我猜测这可能是因为GCC在编译时将大量元数据嵌入二进制文件中(包括时间戳)所导致的。如果你能提供你使用的精确命令行标志,那将很有用。 - cyphar
12
尽管回答“两个编译器输出有什么不同?”的问题很有趣,但我注意到这个问题有一个不必要的假设:即两个输出应该相同,并且我们需要一些解释为什么它们不同。编译器承诺的只是当你给它一个合法的C程序时,输出是一个实现该程序的合法可执行文件。任何两次编译器的执行产生相同的二进制结果并不是C标准的保证。 - Eric Lippert
2
@EricLippert: 我一般知道你的意思,但在这种情况下,我认为这个问题确实值得回答。从天真的角度来看(“为什么编译器在给定完全相同的输入时不会产生相同的输出?”),但也因为有令人信服的现实动机使其如此——既有实际的(难以跟踪依赖于可执行文件中精确字节的行为的错误)又有程序性的(实施ISO9000或类似标准可能需要能够完全复制构建)。一个可以帮助解决这些问题的编译器是一个好东西 :) - psmears
显示剩余9条评论
3个回答

160

这是因为文件名不同(尽管字符串输出相同)。如果您尝试修改文件本身(而不是有两个文件),您会注意到输出二进制文件不再不同。正如Jens和我所说,这是因为GCC在它构建的二进制文件中转储了大量元数据,包括确切的源文件名(据我所知,clang也是如此)。

尝试这个:

$ cp code.c code2.c subdir/code.c
$ gcc code.c -o a
$ gcc code2.c -o b
$ gcc subdir/code.c -o a2
$ diff a b
Binary files a and b differ
$ diff a2 b
Binary files a2 and b differ
$ diff -s a a2
Files a and a2 are identical

这解释了为什么构建之间您的md5sums不会改变,但在不同文件之间是不同的。如果想要,可以像 Jens 建议的那样比较每个二进制文件的 strings 输出,将会注意到文件名嵌入二进制文件中。如果想要“修复”这个问题,可以对二进制文件进行 strip 处理,元数据将被删除:

$ strip a a2 b
$ diff -s a b
Files a and b are identical
$ diff -s a2 b
Files a2 and b are identical
$ diff -s a a2
Files a and a2 are identical

编辑:更新为您可以剥离二进制文件以“修复”问题。 - cyphar
30
应该比较汇编输出而非 MD5 校验和,原因就在于此。 - Lightness Races in Orbit
1
我在这里提出了一个后续问题 here - Federico Poloni
4
根据目标文件格式,编译时间也会存储在目标文件中。因此,如果使用 COFF 格式的文件,例如文件 a 和 a2 将不相同。 - Martin Rosenau

28

最常见的原因是编译器添加的文件名和时间戳(通常在ELF部分的调试信息中)。

尝试运行

 $ strings -a program > x
 ...recompile program...
 $ strings -a program > y
 $ diff x y

你可能会看到原因。我曾经使用它来查找为什么在不同的目录中编译相同的源代码会导致不同的代码。发现是__FILE__宏展开为一个绝对文件名,在两个目录中不同。


1
根据 https://gcc.gnu.org/ml/gcc-help/2007-05/msg00138.html(已过时,我知道),他们不保存时间戳,这可能是链接器问题。尽管如此,我最近记得读到一个故事,讲述了一家安全公司如何利用GCC时间戳信息来分析黑客团队的工作习惯。 - cyphar
3
更不用说,OP指出“在使用相同标志进行多次编译时md5sums匹配”,这表明时间戳可能不是引起问题的原因。很可能是由于文件名不同引起的。 - cyphar
1
@cyphar 不同的文件名也应该被字符串/差异方法捕获。 - Jens

16

注意: 记住源文件名会被编译到未剥离二进制文件中,因此来自不同命名源文件的两个程序将具有不同的哈希值。

在类似情况下,如果上述方法不适用,您可以尝试以下操作:

  • 运行strip命令从二进制文件中删除一些元数据。如果剥离后的二进制文件相同,则差异可能在于非程序操作所需的一些元数据。
  • 生成汇编中间输出以验证差异是否真的在CPU指令中(或者更好地确定差异实际上出现在哪里)
  • 使用strings命令,或者将两个程序转储为十六进制,然后对这两个十六进制进行比较。一旦找到差异,您可以尝试查看它们是否有某种规律(如PID、时间戳、源文件时间戳等)。例如,您可能有一个存储编译时时间戳以供诊断目的的例程。

我的系统是 gcc (GCC) 5.2.0Linux 4.2.0-1-MANJARO #1 SMP PREEMPT x86_64 GNU/Linux - Registered User
2
你应该尝试实际上创建两个单独的文件。我也无法通过修改单个文件来重现它。 - cyphar
是的,文件名是罪魁祸首。如果我使用相同的名称编译程序,我可以得到相同的md5sums。 - Registered User

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