Introduction aux assemblages déterministes en C / C ++. 2e partie

La traduction de l'article a été préparée spécialement pour les étudiants du cours "Développeur C ++" .



→ Lire la première partie



Les informations du dossier d'assemblage sont distribuées dans des fichiers binaires


Si les mêmes fichiers source sont compilés dans des dossiers différents, les informations du dossier sont parfois transférées dans des fichiers binaires. Cela peut se produire principalement pour deux raisons:

  • Utilisation de macros contenant des informations sur le fichier actuel, telles que la macro __FILE__ .
  • CrĂ©ez des binaires de dĂ©bogage qui stockent des informations sur l'emplacement des sources.

Poursuivant notre exemple de bonjour sur MacOS, divisons la source afin de pouvoir montrer l'effet de l'emplacement sur les binaires finaux. La structure du projet sera similaire Ă  celle ci-dessous.

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

Collectons nos fichiers binaires en mode débogage.

 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 

Les informations sur le dossier sont transférées des fichiers objets vers les fichiers exécutables finaux, ce qui rend nos assemblages irreproductibles. Nous pouvons voir les différences entre les binaires à l'aide d'un diffoscope pour voir où les informations du dossier sont intégrées.

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

Solutions possibles


Encore une fois, la décision dépendra du compilateur utilisé:

  • msvc ne peut pas dĂ©finir de paramètres pour Ă©viter d'ajouter ces informations aux fichiers binaires. La seule façon d'obtenir des fichiers binaires reproductibles est d'utiliser Ă  nouveau l'outil de rĂ©paration pour supprimer ces informations pendant la phase de gĂ©nĂ©ration. Veuillez noter que puisque nous corrigeons les binaires pour produire des binaires reproductibles, les dossiers utilisĂ©s pour diffĂ©rents assemblys doivent avoir la mĂŞme longueur en caractères.
  • gcc a trois drapeaux de compilation pour contourner ce problème:
    • -fdebug-prefix-map=OLD=NEW peut supprimer les prĂ©fixes de rĂ©pertoire des informations de dĂ©bogage.
    • -fmacro-prefix-map=OLD=NEW disponible depuis gcc 8 et rĂ©sout le problème d'irrĂ©productibilitĂ© en utilisant la macro __FILE__.
    • -ffile-prefix-map=OLD=NEW est disponible depuis gcc 8 et est une union de -fdebug-prefix-map et -fmacro-prefix-map
  • clang pris en charge -fdebug-prefix-map=OLD=NEW depuis la version 3.8 et travaille sur la prise en charge de deux autres indicateurs pour les futures versions.

La meilleure façon de résoudre ce problème consiste à ajouter des indicateurs aux options du compilateur. Lors de l'utilisation de CMake:

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

L'ordre des fichiers dans le système de construction


L'ordre des fichiers peut être un problème si les répertoires sont lus pour faire une liste de leurs fichiers. Par exemple, Unix n'a pas d'ordre déterministe dans lequel readdir () et listdir () doivent retourner le contenu d'un répertoire, donc faire confiance à ces fonctions pour alimenter le système d'assemblage peut conduire à des assemblages non déterministes.

Le même problème se produit, par exemple, si votre système de génération stocke des fichiers pour l'éditeur de liens dans un conteneur (par exemple, dans un dictionnaire Python standard), qui peut renvoyer des éléments dans un ordre non déterministe. Cela entraînera les fichiers à lier dans un ordre différent à chaque fois, et différents fichiers binaires seront créés.

Nous pouvons simuler ce problème en réorganisant les fichiers dans CMake. Si nous modifions l'exemple précédent pour avoir plus d'un fichier source pour la bibliothèque:

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

Nous pouvons voir que les résultats de la compilation sont différents si nous changeons l'ordre des fichiers dans 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) 

Si nous faisons deux assemblys consécutifs avec les noms A et B, en échangeant les sources0.cpp et sources1.cpp dans la liste des fichiers, nous obtenons les sommes de contrôle suivantes:

 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 

Les fichiers objet .o sont identiques, mais les bibliothèques .a et les exécutables ne le sont pas. En effet, l'ordre d'insertion dans la bibliothèque dépend de l'ordre dans lequel les fichiers sont répertoriés.

Aléatoire créé par le compilateur


Ce problème se produit, par exemple, dans gcc lorsque les optimisations de temps de -flto (indicateur -flto ) sont -flto . Cette option introduit des noms générés aléatoirement dans des fichiers binaires. La seule façon d'éviter ce problème est d'utiliser le drapeau - frandom-seed . Cette option fournit une valeur de départ que gcc utilise au lieu de nombres aléatoires. Il est utilisé pour générer des noms de symboles spécifiques, qui doivent être différents dans chaque fichier compilé. Il est également utilisé pour placer des tampons uniques dans les fichiers de couverture des données et les fichiers d'objets qui les produisent. Ce paramètre doit être différent pour chaque fichier source. Une option consiste à définir la somme de contrôle du fichier afin que la probabilité de collision soit très faible. Par exemple, dans CMake, cela peut être fait en utilisant une fonction comme celle-ci:

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

Quelques conseils pour utiliser Conan


Les crochets Conan peuvent nous aider à rendre nos versions reproductibles. Cette fonctionnalité vous permet de personnaliser le comportement du client à certains points.

Une façon d'utiliser les hooks peut être de définir des variables d'environnement à l'étape pre_build . Dans l'exemple ci-dessous, la fonction set_environment est set_environment , puis l'environnement est restauré à l'étape reset_environment aide de 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"] 

Les crochets peuvent également être utiles pour réparer les binaires à l'étape post_build . Il existe différents outils pour analyser et corriger les fichiers binaires, tels que ducible , pefile , pe-parse ou strip-nondeterminism . Un exemple de crochet pour fixer un binaire PE en utilisant ducible pourrait être:

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

Conclusions


Les assemblages déterministes sont une tâche complexe, étroitement liée au système d'exploitation et à la boîte à outils utilisés. Cette introduction était censée aider à comprendre les causes les plus courantes du manque de déterminisme et comment y remédier.

Les références


Informations générales



Les outils


Outils de comparaison binaires


Outils de réparation de fichiers


Outils d'analyse de fichiers


→ Lire la première partie

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


All Articles