EinfĂĽhrung in deterministische Assemblys in C / C ++. Teil 2

Die Ăśbersetzung des Artikels wurde speziell fĂĽr Studenten des Kurses "C ++ Developer" vorbereitet.



→ Lesen Sie den ersten Teil



Informationen zum Baugruppenordner werden in Binärdateien verteilt


Wenn dieselben Quelldateien in verschiedenen Ordnern kompiliert werden, werden die Ordnerinformationen manchmal in Binärdateien übertragen. Dies kann hauptsächlich aus zwei Gründen geschehen:

  • Verwenden von Makros, die Informationen zur aktuellen Datei enthalten, z. B. das Makro __FILE__ .
  • Erstellen Sie Debug-Binärdateien, in denen Informationen zum Speicherort der Quellen gespeichert sind.

Setzen wir unser Hallo-Welt-Beispiel unter MacOS fort und teilen wir die Quelle auf, damit wir die Auswirkung der Position auf die endgültigen Binärdateien zeigen können. Die Struktur des Projekts wird der folgenden ähnlich sein.

 . ├── 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 

Sammeln wir unsere Binärdateien im Debug-Modus.

 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 

Informationen über den Ordner werden von den Objektdateien in die endgültigen ausführbaren Dateien übertragen, wodurch unsere Assemblys nicht mehr reproduzierbar sind. Wir können die Unterschiede zwischen den Binärdateien mithilfe eines Diffoskops erkennen, um festzustellen, wo die Ordnerinformationen eingebettet sind.

 > 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 ... 

Mögliche Lösungen


Auch hier hängt die Entscheidung vom verwendeten Compiler ab:

  • msvc kann keine Parameter festlegen, um das HinzufĂĽgen dieser Informationen zu Binärdateien zu vermeiden. Die einzige Möglichkeit, reproduzierbare Binärdateien zu erhalten, besteht darin, das Reparaturwerkzeug erneut zu verwenden, um diese Informationen während der Erstellungsphase zu entfernen. Bitte beachten Sie, dass die Ordner, die fĂĽr verschiedene Assemblys verwendet werden, die gleiche Länge in Zeichen haben sollten, da wir Binärdateien reparieren, um reproduzierbare Binärdateien zu erstellen.
  • gcc verfĂĽgt ĂĽber drei Compiler-Flags, um dieses Problem zu gcc :
    • -fdebug-prefix-map=OLD=NEW kann Verzeichnispräfixe aus Debug-Informationen entfernen.
    • -fmacro-prefix-map=OLD=NEW seit gcc 8 verfĂĽgbar und löst das Irreproduzierbarkeitsproblem mithilfe des Makros __FILE__.
    • -ffile-prefix-map=OLD=NEW ist seit gcc 8 verfĂĽgbar und ist eine Vereinigung von -fdebug-prefix-map und -fmacro-prefix-map
  • clang unterstĂĽtzt -fdebug-prefix-map=OLD=NEW seit Version 3.8 und arbeitet daran, zwei weitere Flags fĂĽr zukĂĽnftige Versionen zu unterstĂĽtzen.

Der beste Weg, um dieses Problem zu lösen, besteht darin, den Compileroptionen Flags hinzuzufügen. Bei Verwendung von CMake:

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

Die Reihenfolge der Dateien im Build-System


Die Reihenfolge der Dateien kann problematisch sein, wenn Verzeichnisse gelesen werden, um eine Liste ihrer Dateien zu erstellen. Beispielsweise hat Unix keine deterministische Reihenfolge, in der readdir () und listdir () den Inhalt eines Verzeichnisses zurĂĽckgeben mĂĽssen. Wenn Sie also darauf vertrauen, dass diese Funktionen das Assembly-System versorgen, kann dies zu nicht deterministischen Assemblys fĂĽhren.

Das gleiche Problem tritt beispielsweise auf, wenn Ihr Build-System Dateien für den Linker in einem Container speichert (z. B. in einem regulären Python-Wörterbuch), der Elemente in einer nicht deterministischen Reihenfolge zurückgeben kann. Dadurch werden die Dateien jedes Mal in einer anderen Reihenfolge verknüpft und es werden unterschiedliche Binärdateien erstellt.

Wir können dieses Problem simulieren, indem wir die Dateien in CMake neu anordnen. Wenn wir das vorherige Beispiel so ändern, dass es mehr als eine Quelldatei für die Bibliothek enthält:

 . ├── 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 

Wir können sehen, dass die Kompilierungsergebnisse unterschiedlich sind, wenn wir die Reihenfolge der Dateien in 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) 

Wenn wir zwei aufeinanderfolgende Assemblys mit den Namen A und B sources0.cpp und die sources1.cpp sources0.cpp und sources1.cpp in der Liste der Dateien austauschen, erhalten wir die folgenden PrĂĽfsummen:

 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 

Objekt- .o Dateien sind identisch, .a-Bibliotheken und ausführbare Dateien jedoch nicht. Dies liegt daran, dass die Reihenfolge des Einfügens in die Bibliothek von der Reihenfolge abhängt, in der die Dateien aufgelistet sind.

Der Compiler hat Zufälligkeit erzeugt


Dieses Problem tritt beispielsweise in gcc auf, wenn Link-Time Optimizations ( -flto Flag) -flto . Diese Option führt zufällig generierte Namen in Binärdateien ein. Die einzige Möglichkeit, dieses Problem zu vermeiden, ist die Verwendung des Flags - frandom-seed . Diese Option bietet einen Startwert, den gcc anstelle von Zufallszahlen verwendet. Es wird verwendet, um bestimmte Symbolnamen zu generieren, die in jeder kompilierten Datei unterschiedlich sein müssen. Es wird auch verwendet, um eindeutige Stempel in Datenabdeckungsdateien und Objektdateien zu platzieren, die diese erzeugen. Dieser Parameter sollte für jede Quelldatei unterschiedlich sein. Eine Möglichkeit besteht darin, die Prüfsumme der Datei so einzustellen, dass die Wahrscheinlichkeit einer Kollision sehr gering ist. In CMake kann dies beispielsweise mit einer Funktion wie der folgenden erfolgen:

 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() 

Einige Tipps zur Verwendung von Conan


Conan- Haken können uns helfen, unsere Builds reproduzierbar zu machen. Mit dieser Funktion können Sie das Verhalten des Clients an bestimmten Punkten konfigurieren.

Eine Möglichkeit, Hooks zu verwenden, besteht darin, Umgebungsvariablen in der Phase pre_build . Im folgenden Beispiel wird die Funktion set_environment , und anschließend wird die Umgebung im Schritt reset_environment mithilfe von reset_environment .

 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"] 

Hooks können auch nützlich sein, um Binärdateien in der Phase post_build zu post_build . Es gibt verschiedene Tools zum Analysieren und Korrigieren von Binärdateien, z. B. ducible , pefile , pe-parse oder strip-nondeterminism . Ein Beispielhaken zum Fixieren einer PE-Binärdatei mit ducible könnte sein:

 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() 

Schlussfolgerungen


Deterministische Assemblys sind eine komplexe Aufgabe, die eng mit dem verwendeten Betriebssystem und Toolkit zusammenhängt. Diese Einführung sollte helfen, die häufigsten Ursachen für mangelnden Determinismus zu verstehen und wie man sie angeht.

Referenzen


allgemeine Informationen



Die Werkzeuge


Binäre Vergleichstools


Dateireparatur-Tools


Tools zur Dateianalyse


→ Lesen Sie den ersten Teil

Source: https://habr.com/ru/post/de468555/


All Articles