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

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



阅读第一部分



程序集文件夹信息分布在二进制文件中


如果将相同的源文件编译在不同的文件夹中,则有时文件夹信息会传输到二进制文件中。 发生这种情况主要有两个原因:

  • 使用包含有关当前文件信息的宏,例如__FILE__宏。
  • 创建用于存储有关源位置信息的调试二进制文件。

继续我们在MacOS上的hello world示例,让我们分割源,以便我们可以显示最终二进制文件中位置的影响。 该项目的结构将与以下结构相似。

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

让我们以调试模式收集二进制文件。

 cd srcA/build cmake -DCMAKE_BUILD_TYPE=Debug .. make cd .. && cd .. cd srcB/build cmake -DCMAKE_BUILD_TYPE=Debug .. make cd .. && cd .. md5sum srcA/build/hello md5sum srcB/build/hello md5sum srcA/build/CMakeFiles/HelloLib.dir/hello_world.cpp.o md5sum srcB/build/CMakeFiles/HelloLib.dir/hello_world.cpp.o md5sum srcA/build/libHelloLib.a md5sum srcB/build/libHelloLib.a     : 3572a95a8699f71803f3e967f92a5040 srcA/build/hello 7ca693295e62de03a1bba14853efa28c srcB/build/hello 76e0ae7c4ef79ec3be821ccf5752730f srcA/build/CMakeFiles/HelloLib.dir/hello_world.cpp.o 5ef044e6dcb73359f46d48f29f566ae5 srcB/build/CMakeFiles/HelloLib.dir/hello_world.cpp.o dc941156608b578c91e38f8ecebfef6d srcA/build/libHelloLib.a 1f9697ef23bf70b41b39ef3469845f76 srcB/build/libHelloLib.a 

有关该文件夹的信息从目标文件传输到最终的可执行文件,这使我们的程序集不可复制。 我们可以使用衍射仪查看二进制文件之间的差异,以查看文件夹信息的嵌入位置。

 > diffoscope helloA helloB --- srcA/build/hello +++ srcB/build/hello @@ -1282,20 +1282,20 @@ ... 00005070: 5f77 6f72 6c64 5f64 6562 7567 2f73 7263 _world_debug/src -00005080: 412f 006d 6169 6e2e 6370 7000 2f55 7365 A/.main.cpp./Use +00005080: 422f 006d 6169 6e2e 6370 7000 2f55 7365 B/.main.cpp./Use 00005090: 7273 2f63 6172 6c6f 732f 446f 6375 6d65 rs/carlos/Docume 000050a0: 6e74 732f 6465 7665 6c6f 7065 722f 7265 nts/developer/re 000050b0: 7072 6f64 7563 6962 6c65 2d62 7569 6c64 producible-build 000050c0: 732f 7361 6e64 626f 782f 6865 6c6c 6f5f s/sandbox/hello_ -000050d0: 776f 726c 645f 6465 6275 672f 7372 6341 world_debug/srcA +000050d0: 776f 726c 645f 6465 6275 672f 7372 6342 world_debug/srcB 000050e0: 2f62 7569 6c64 2f43 4d61 6b65 4669 6c65 /build/CMakeFile 000050f0: 732f 6865 6c6c 6f2e 6469 722f 6d61 696e s/hello.dir/main 00005100: 2e63 7070 2e6f 005f 6d61 696e 005f 5f5a .cpp.o._main.__Z ... @@ -1336,15 +1336,15 @@ ... 000053c0: 6962 6c65 2d62 7569 6c64 732f 7361 6e64 ible-builds/sand 000053d0: 626f 782f 6865 6c6c 6f5f 776f 726c 645f box/hello_world_ -000053e0: 6465 6275 672f 7372 6341 2f62 7569 6c64 debug/srcA/build +000053e0: 6465 6275 672f 7372 6342 2f62 7569 6c64 debug/srcB/build 000053f0: 2f6c 6962 4865 6c6c 6f4c 6962 2e61 2868 /libHelloLib.a(h 00005400: 656c 6c6f 5f77 6f72 6c64 2e63 7070 2e6f ello_world.cpp.o 00005410: 2900 5f5f 5a4e 3130 4865 6c6c 6f57 6f72 ).__ZN10HelloWor ... 

可能的解决方案


同样,该决定将取决于所使用的编译器:

  • msvc无法设置参数,以避免将此信息添加到二进制文件中。 获取可复制二进制文件的唯一方法是在构建阶段再次使用修复工具删除此信息。 请注意,由于我们修复了二进制文件以产生可复制的二进制文件,因此用于不同程序集的文件夹的字符长度应相同。
  • gcc具有三个编译器标志来解决此问题:
    • -fdebug-prefix-map=OLD=NEW可以从调试信息中删除目录前缀。
    • 自gcc 8起, -fmacro-prefix-map=OLD=NEW可用,并通过使用__FILE__宏解决了不可再现性问题。
    • -ffile-prefix-map=OLD=NEW自gcc 8起可用,并且是-fdebug-prefix-map和-fmacro-prefix-map的联合。
  • 自3.8版以来, clang支持-fdebug-prefix-map=OLD=NEW并且正在为将来的版本支持其他两个标志。

解决此问题的最佳方法是在编译器选项中添加标志。 使用CMake时:

 target_compile_options(target PUBLIC "-ffile-prefix-map=${CMAKE_SOURCE_DIR}=.") 

构建系统中文件的顺序


如果读取目录以列出其文件列表,则文件的顺序可能会出现问题。 例如,Unix没有确定性的顺序,在该顺序中readdir()和listdir()必须返回目录的内容,因此信任这些函数来提供汇编系统可能导致不确定的汇编。

例如,如果构建系统将链接程序的文件存储在容器中(例如,在常规的python字典中),则该问题可能会以不确定的顺序返回元素,从而发生相同的问题。 这将导致文件每次以不同的顺序链接,并且将创建不同的二进制文件。

我们可以通过在CMake中重新排列文件来模拟此问题。 如果我们修改前面的示例,使该库具有多个源文件:

 . ├── CMakeLists.txt ├── CMakeListsA.txt ├── CMakeListsB.txt ├── hello_world.cpp ├── hello_world.hpp ├── main.cpp ├── sources0.cpp ├── sources0.hpp ├── sources1.cpp ├── sources1.hpp ├── sources2.cpp └── sources2.hpp 

如果我们更改CMakeLists.txt文件的顺序,我们可以看到编译结果是不同的:

 cmake_minimum_required(VERSION 3.0) project(HelloWorld) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) add_library(HelloLib hello_world.cpp sources0.cpp sources1.cpp sources2.cpp) add_executable(hello main.cpp) target_link_libraries(hello HelloLib) 

如果我们使用名称A和B制作两个连续的程序集,交换文件列表中的sources0.cppsources0.cppsources0.cpp得到以下校验和:

 30ab264d6f8e1784282cd1a415c067f2 helloA cdf3c9dd968f7363dc9e8b40918d83af helloB 707c71bc2a8def6885b96fb67b84d79c hello_worldA.cpp.o 707c71bc2a8def6885b96fb67b84d79c hello_worldB.cpp.o 694ff3765b688e6faeebf283052629a3 sources0A.cpp.o 694ff3765b688e6faeebf283052629a3 sources0B.cpp.o 0db24dc6a94da1d167c68b96ff319e56 sources1A.cpp.o 0db24dc6a94da1d167c68b96ff319e56 sources1B.cpp.o fd0754d9a4a44b0fcc4e4f3c66ad187c sources2A.cpp.o fd0754d9a4a44b0fcc4e4f3c66ad187c sources2B.cpp.o baba9709d69c9e5fd51ad985ee328172 libHelloLibA.a 72641dc6fc4f4db04166255f62803353 libHelloLibB.a 

对象.o文件相同,但.a库和可执行文件不同。 这是因为在库中的插入顺序取决于文件的列出顺序。

编译器产生的随机性


例如,当-flto 链接时间优化-flto标志)时,在gcc中会发生此问题。 此选项将随机生成的名称引入二进制文件。 避免此问题的唯一方法是使用flag- frandom-seed 。 此选项提供了gcc使用的种子而不是随机数。 它用于生成特定的符号名称,每个编译文件中的名称都必须不同。 它还用于在数据覆盖文件和生成它们的对象文件中放置唯一图章。 每个源文件的此参数都应该不同。 一种选择是设置文件的校验和,以使冲突的可能性非常低。 例如,在CMake中,可以使用以下函数来完成此操作:

 set(LIB_SOURCES ./src/source1.cpp ./src/source2.cpp ./src/source3.cpp) foreach(_file ${LIB_SOURCES}) file(SHA1 ${_file} checksum) string(SUBSTRING ${checksum} 0 8 checksum) set_property(SOURCE ${_file} APPEND_STRING PROPERTY COMPILE_FLAGS "-frandom-seed=0x${checksum}") endforeach() 

使用柯南的一些技巧


柯南钩子可以帮助我们使建造物具有可复制性。 此功能使您可以在某些时候自定义客户端的行为。

使用挂钩的一种方法是在pre_build阶段设置环境变量。 在下面的示例中,将set_environment函数,然后使用reset_environmentpost_build步骤中还原环境。

 def set_environment(self): if self._os == "Linux": self._old_source_date_epoch = os.environ.get("SOURCE_DATE_EPOCH") timestamp = "1564483496" os.environ["SOURCE_DATE_EPOCH"] = timestamp self._output.info( "set SOURCE_DATE_EPOCH: {}".format(timestamp)) elif self._os == "Macos": os.environ["ZERO_AR_DATE"] = "1" self._output.info( "set ZERO_AR_DATE: {}".format(timestamp)) def reset_environment(self): if self._os == "Linux": if self._old_source_date_epoch is None: del os.environ["SOURCE_DATE_EPOCH"] else: os.environ["SOURCE_DATE_EPOCH"] = self._old_source_date_epoch elif self._os == "Macos": del os.environ["ZERO_AR_DATE"] 

挂钩对于在post_build阶段修复二进制文件也很有用。 有多种工具可用于分析和纠正二进制文件,例如pefilepefilepe-parsestrip-nondeterminism 。 使用ducible修复PE二进制文件的示例挂钩可能是:

 class Patcher(object): ... def patch(self): if self._os == "Windows" and self._compiler == "Visual Studio": for root, _, filenames in os.walk(self._conanfile.build_folder): for filename in filenames: filename = os.path.join(root, filename) if ".exe" in filename or ".dll" in filename: self._patch_pe(filename) def _patch_pe(self, filename): patch_tool_location = "C:/ducible/ducible.exe" if os.path.isfile(patch_tool_location): self._output.info("Patching {} with md5sum: {}".format(filename,md5sum(filename))) self._conanfile.run("{} {}".format(patch_tool_location, filename)) self._output.info("Patched file: {} with md5sum: {}".format(filename,md5sum(filename))) ... def pre_build(output, conanfile, **kwargs): lib_patcher.init(output, conanfile) lib_patcher.set_environment() def post_build(output, conanfile, **kwargs): lib_patcher.patch() lib_patcher.reset_environment() 

结论


确定性程序集是一项复杂的任务,与所使用的操作系统和工具包密切相关。 本介绍旨在帮助理解缺乏确定性的最常见原因以及如何解决这些问题。

参考文献


一般资讯



工具


二进制比较工具


文件修复工具


文件分析工具


阅读第一部分

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


All Articles