C / C ++中的确定性程序集简介。 第一部分

本文的翻译是专门为“ C ++开发人员”课程的学生准备的。




什么是确定性程序集?


确定性汇编是在相同环境和汇编指令下构建相同源代码的过程,在任何情况下都将创建相同的二进制文件,即使它们是在不同的计算机上,在不同的目录中以及以不同的名称创建的也是如此。 。 如果可以保证即使从不同的文件夹进行编译,此类程序集也将创建相同的二进制文件,因此有时也称为可播放或密封程序集。

确定性程序集并不是自己发生的事情。 它们不是在普通项目中创建的,对于每个操作系统或编译器,未发生这种情况的原因可能有所不同。

对于给定的组装环境,必须保证确定性的组装。 这意味着某些变量,例如操作系统,构建系统版本和目标体系结构 ,在不同的构建中可能保持不变。

近年来,诸如ChromiumReproducible buildsYocto之类的各种组织为实现确定性组装付出了巨大的努力。

确定性程序集的重要性


确定性组装如此重要的主要原因有两个:

  • 安全性 更改二进制文件而不是源代码会使原始作者看不见更改。 在对医学,航空和太空等至关重要的安全环境中,这可能是致命的。 这些材料的可能相同的结果使第三方可以就正确的结果达成共识。
  • 可追溯性和二进制控制 。 如果您想要一个用于存储二进制文件的存储库,那么很可能您不想从同一版本的源中创建带有随机校验和的二进制文件。 当二进制文件应该相同时,这可能导致存储库系统将不同的二进制文件存储为不同的版本。 例如,如果您在Windows或MacOS上工作,则库中包含带有创建/修改对象文件的时间的字段,这将导致二进制文件之间的差异。

C / C ++中的生成过程中涉及的二进制文件


根据操作系统的不同,在C / C ++的构建过程中会创建各种类型的二进制文件。

微软视窗 最重要的是扩展名为.obj.lib等的文件.lib dll.exe 。 它们都符合可移植可执行(PE)格式规范。 可以使用dumpbin之类的工具来分析这些文件。
的Linux 扩展名为.o.a.so且不带扩展名的文件(对于可执行二进制文件)对应于可执行文件和可链接文件(ELF)的格式。 ELF文件的内容可以使用readelf进行分析。
Mac OS 扩展名为.o.a.dylib且不带有扩展名的文件(对于可执行二进制文件)符合Mach-O格式规范。 可以使用otool应用程序验证这些文件,该应用程序是MacOS上Xcode工具包的一部分。

变异来源


许多不同的因素会使您的程序集不确定 。 不同的操作系统和编译器,因素会有所不同。 每个编译器都有某些参数来更正变化的来源。 迄今为止, gccclang是包含更多修复选项的编译器。 您可以尝试使用msvc一些未msvc说明的选项,但是最后,您可能必须修复二进制文件才能获得确定性程序集。

编译器/链接器添加的时间戳


我们的二进制文件可能包含使它们无法播放的时间信息的主要原因有两个:

  • 在源中使用__DATE____TIME__
  • 当文件格式强制您将时间信息存储在目标文件中时。 这是Windows上的可移植可执行格式和MacOS上的Mach-O的情况。 在Linux上,ELF文件不编码任何时间戳。

让我们看一个示例,其中此信息以在MacOS上编译hello世界基础项目的静态库结束。

 . ├── CMakeLists.txt ├── hello_world.cpp ├── hello_world.hpp ├── main.cpp └── run_build.sh 

该库在终端中显示一条消息:

 #include "hello_world.hpp" #include <iostream> void HelloWorld::PrintMessage(const std::string & message) { std::cout << message << std::endl; } 

应用程序将使用它来显示消息“ Hello World!”:

 #include <iostream> #include "hello_world.hpp" int main(int argc, char** argv) { HelloWorld hello; hello.PrintMessage("Hello World!"); return 0; } 

我们将使用CMake构建项目:

 cmake_minimum_required(VERSION 3.0) project(HelloWorld) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) add_library(HelloLibA hello_world.cpp) add_library(HelloLibB hello_world.cpp) add_executable(helloA main.cpp) add_executable(helloB main.cpp) target_link_libraries(helloA HelloLibA) target_link_libraries(helloB HelloLibB) 

我们将使用相同的源代码创建两个不同的库,并使用相同的源创建两个二进制文件。 生成项目并运行md5sum以查看所有二进制文件的校验和:

 mkdir build && cd build cmake .. make md5sum helloA md5sum helloB md5sum CMakeFiles/HelloLibA.dir/hello_world.cpp.o md5sum CMakeFiles/HelloLibB.dir/hello_world.cpp.o md5sum libHelloLibA.a md5sum libHelloLibB.a 

我们得出这样的结论:

 b5dce09c593658ee348fd0f7fae22c94 helloA b5dce09c593658ee348fd0f7fae22c94 helloB 0a4a0de3df8cc7f053f2fcb6d8b75e6d CMakeFiles/HelloLibA.dir/hello_world.cpp.o 0a4a0de3df8cc7f053f2fcb6d8b75e6d CMakeFiles/HelloLibB.dir/hello_world.cpp.o adb80234a61bb66bdc5a3b4b7191eac7 libHelloLibA.a 5ac3c70d28d9fdd9c6571e077131545e libHelloLibB.a 

这很有趣,因为helloAhelloB以及Mach-O中间对象文件hello_world.cpp.o具有相同的校验和,但是对于扩展名为.a文件则不能这么说。 这是因为它们以存档格式存储有关中间目标文件的信息。 此格式的标头包含一个由stat系统调用设置的名为st_time的字段。 使用otool检查libHelloLibA.alibHelloLibB.a以显示标题:

 > otool -a libHelloLibA.a Archive : libHelloLibA.a 0100644 503/20 612 1566927276 #1/20 0100644 503/20 13036 1566927271 #1/28 > otool -a libHelloLibB.a Archive : libHelloLibB.a 0100644 503/20 612 1566927277 #1/20 0100644 503/20 13036 1566927272 #1/28 

我们看到该文件包含几个临时字段,这些临时字段使我们的程序集不确定。 请注意,这些字段不适用于最终的可执行文件,因为它们具有相同的校验和。 当使用Visual Studio,但使用PE文件而不是Mach-O在Windows上构建时,也会发生此问题。

在这一点上,我们可以尝试使情况变得更糟,并使我们的二进制文件也不确定。 更改main.cpp文件,使其包含__TIME__宏:

 #include <iostream> #include "hello_world.hpp" int main(int argc, char** argv) { HelloWorld hello; hello.PrintMessage("Hello World!"); std::cout << "At time: " << __TIME__ << std::endl; return 0; } 

再次检查文件的校验和:

 625ecc7296e15d41e292f67b57b04f15 helloA 20f92d2771a7d2f9866c002de918c4da helloB 0a4a0de3df8cc7f053f2fcb6d8b75e6d CMakeFiles/HelloLibA.dir/hello_world.cpp.o 0a4a0de3df8cc7f053f2fcb6d8b75e6d CMakeFiles/HelloLibB.dir/hello_world.cpp.o b7801c60d3bc4f83640cadc1183f43b3 libHelloLibA.a 4ef6cae3657f2a13ed77830953b0aee8 libHelloLibB.a 

我们看到现在我们有不同的二进制文件。 我们可以使用diffoscope之类的工具来分析可执行文件,该工具显示了两个二进制文件之间的差异:

 > diffoscope helloA helloB --- helloA +++ helloB ├── otool -arch x86_64 -tdvV {} │┄ Code for architecture x86_64 │ @@ -16,15 +16,15 @@ │ 00000001000018da jmp 0x1000018df00000001000018df leaq -0x30(%rbp), %rdi │ 00000001000018e3 callq 0x100002d54 ## symbol stub for: __ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEED1Ev │ 00000001000018e8 movq 0x1721(%rip), %rdi ## literal pool symbol address: __ZNSt3__14coutE │ 00000001000018ef leaq 0x162f(%rip), %rsi ## literal pool for: "At time: " │ 00000001000018f6 callq 0x100002d8a ## symbol stub for: __ZNSt3__1lsINS_11char_traitsIcEEEERNS_13basic_ostreamIcT_EES6_PKc │ 00000001000018fb movq %rax, %rdi │ -00000001000018fe leaq 0x162a(%rip), %rsi ## literal pool for: "19:40:47" │ +00000001000018fe leaq 0x162a(%rip), %rsi ## literal pool for: "19:40:48" │ 0000000100001905 callq 0x100002d8a ## symbol stub for: __ZNSt3__1lsINS_11char_traitsIcEEEERNS_13basic_ostreamIcT_EES6_PKc │ 000000010000190a movq %rax, %rdi │ 000000010000190d leaq __ZNSt3__1L4endlIcNS_11char_traitsIcEEEERNS_13basic_ostreamIT_T0_EES7_(%rip), %rsi # 

它表明__TIME__信息已粘贴到二进制文件中,这使其具有不确定性。 让我们看看如何避免这种情况。

Microsoft Visual Studio的可能解决方案


Microsoft Visual Studio具有Microsoft未记录的链接器/ Brepro标志。 该标志将时间戳从“便携式可执行文件”格式设置为-1,如下图所示。



要使用CMake激活此标志,我们必须在创建.exe时添加以下几行:

 add_link_options("/Brepro") 

.lib这些行

 set_target_properties( TARGET PROPERTIES STATIC_LIBRARY_OPTIONS "/Brepro" ) 

问题在于此标志使二进制文件在我们最终的二进制.exe中可播放(相对于文件格式中的时间戳),但不会从.lib中删除所有时间戳(与Mach-O目标文件相同的问题,我们在上面讨论过)。 .lib文件的COFF头文件中的TimeDateStamp字段将保留。 从二进制.lib文件中删除此信息的唯一方法是通过用任何已知值替换与TimeDateStamp字段相对应的字节来修复.lib

GCC和CLANG的可能解决方案


  • gcc检测到SOURCE_DATE_EPOCH环境变量的存在。 如果设置了此变量,则其值指示UNIX时间戳,它将用于替换__DATE____TIME__宏中的当前日期和时间,从而使内置时间戳变得可重现。 可以将值设置为已知的时间戳记,例如对源文件或包进行最后更改的时间。
  • clang使用ZERO_AR_DATE ,如果设置了ZERO_AR_DATE ,则会重置存档文件中提供的ZERO_AR_DATE ,将其设置为0。请注意,这不会修复__DATE____TIME__ 。 如果要修复此宏的影响,则必须修复二进制文件或伪造系统时间。

让我们继续我们的MacOS示例项目,并查看设置ZERO_AR_DATE环境ZERO_AR_DATE时的结果。

 export ZERO_AR_DATE=1 

现在,如果我们编译可执行文件和库(在源代码中删除__DATE__宏), __DATE__得到:

 b5dce09c593658ee348fd0f7fae22c94 helloA b5dce09c593658ee348fd0f7fae22c94 helloB 0a4a0de3df8cc7f053f2fcb6d8b75e6d CMakeFiles/HelloLibA.dir/hello_world.cpp.o 0a4a0de3df8cc7f053f2fcb6d8b75e6d CMakeFiles/HelloLibB.dir/hello_world.cpp.o 9f9a9af4bb3e220e7a22fb58d708e1e5 libHelloLibA.a 9f9a9af4bb3e220e7a22fb58d708e1e5 libHelloLibB.a 

现在所有校验和都相同。 .a分析扩展名为.a的文件头:

 > otool -a libHelloLibA.a Archive : libHelloLibA.a 0100644 503/20 612 0 #1/20 0100644 503/20 13036 0 #1/28 > otool -a libHelloLibB.a Archive : libHelloLibB.a 0100644 503/20 612 0 #1/20 0100644 503/20 13036 0 #1/28 

我们可以看到库头的timestamp字段设置为零。

我们顺利地走到本文第一部分的结尾。 可以在这里阅读该材料的续篇。

Source: https://habr.com/ru/post/zh-CN467741/


All Articles