La traduction de l'article a été préparée spécialement pour les étudiants du cours "Développeur C ++" .
Qu'est-ce qu'un assemblage déterministe?
Un assemblage déterministe est le processus d'assemblage du même code source avec le même environnement et les mêmes instructions d'assemblage, dans lequel les mêmes fichiers binaires sont créés dans tous les cas, même s'ils sont créés sur différentes machines, dans différents répertoires et avec des noms différents . De tels assemblys sont parfois appelés assemblages jouables ou scellés, s'il est garanti qu'ils créeront les mêmes fichiers binaires même lors de la compilation à partir de différents dossiers.
Les assemblages déterministes ne se produisent pas par eux-mêmes. Ils ne sont pas créés dans des projets ordinaires et les raisons pour lesquelles cela ne se produit pas peuvent être différentes pour chaque système d'exploitation ou compilateur.
Les assemblages déterministes doivent être garantis pour un
environnement d'assemblage donné. Cela signifie que certaines variables, telles que le
système d'exploitation, les versions du système de construction et l'architecture cible , restent probablement les mêmes dans différents assemblys.
Ces dernières années, diverses organisations, telles que
Chromium ,
Reproductible builds ou
Yocto , ont déployé de grands efforts pour réaliser des assemblages déterministes.
L'importance des assemblages déterministes
Il y a deux raisons principales pour lesquelles les assemblages déterministes sont si importants:
- La sécurité Changer les binaires au lieu du code source peut rendre les modifications invisibles pour les auteurs originaux. Cela peut être fatal dans des environnements critiques pour la sécurité tels que la médecine, l'aviation et l'espace. Des résultats potentiellement identiques pour ces matériaux permettent à des tiers de parvenir à un consensus sur le bon résultat.
- Traçabilité et contrôle binaire . Si vous souhaitez disposer d'un référentiel pour stocker vos fichiers binaires, il est fort probable que vous ne souhaitiez pas créer de fichiers binaires avec des sommes de contrôle aléatoires à partir de sources dans la même révision. Cela peut amener le système de référentiel à stocker différents fichiers binaires sous différentes versions alors qu'ils devraient être identiques. Par exemple, si vous travaillez sous Windows ou MacOS, la bibliothèque possède des champs avec l'heure de création / modification des fichiers objets qui y sont inclus, ce qui entraînera des différences dans les fichiers binaires.
Fichiers binaires impliqués dans le processus de construction en C / C ++
Il existe différents types de binaires créés pendant le processus de génération en C / C ++, selon le système d'exploitation.
Microsoft Windows Les plus importants sont les fichiers avec les extensions
.obj
,
.lib
dll
et
.exe
. Ils sont tous conformes à la spécification du format exécutable portable (PE). Ces fichiers peuvent être analysés avec des outils comme
dumpbin .
Linux Les fichiers avec les extensions
.o
,
.a
,
.so
et sans extensions (pour les fichiers binaires exécutables) correspondent au format des fichiers exécutables et liables (ELF). Le contenu des fichiers ELF peut être analysé à l'aide de
readelf .
Mac OS Les fichiers avec les extensions
.o
,
.a
,
.dylib
et sans extensions (pour les fichiers binaires exécutables) sont conformes à la spécification du format Mach-O. Ces fichiers peuvent être vérifiés à l'aide de l'application
otool , qui fait partie de la boîte à outils
Xcode sur MacOS.
Sources de variations
De nombreux facteurs différents peuvent rendre vos assemblages
non déterministes . Les facteurs varient pour différents systèmes d'exploitation et compilateurs. Chaque compilateur possède certains paramètres pour corriger les sources de variation. À ce jour,
gcc
et
clang
sont les compilateurs qui contiennent plus d'options de correction. Il existe des options non documentées pour
msvc
que vous pouvez essayer, mais à la fin, vous devrez probablement corriger les binaires pour obtenir des assemblys déterministes.
Horodatages ajoutés par le compilateur / l'éditeur de liens
Il existe deux raisons principales pour lesquelles nos binaires peuvent contenir des informations temporelles qui les rendent illisibles:
- Utilisation des
__TIME__
__DATE__
ou __TIME__
dans la source. - Lorsqu'un format de fichier vous oblige Ă stocker des informations temporelles dans des fichiers objets. C'est le cas du format Portable Executable sous Windows et Mach-O sous MacOS. Sous Linux, les fichiers ELF ne codent aucun horodatage.
Regardons un exemple où ces informations se terminent par la compilation d'une bibliothèque statique du projet de base hello world sur MacOS.
. ├── CMakeLists.txt ├── hello_world.cpp ├── hello_world.hpp ├── main.cpp └── run_build.sh
La bibliothèque affiche un message dans le terminal:
#include "hello_world.hpp" #include <iostream> void HelloWorld::PrintMessage(const std::string & message) { std::cout << message << std::endl; }
Et l'application s'en servira pour afficher le message «Bonjour tout le monde!»:
#include <iostream> #include "hello_world.hpp" int main(int argc, char** argv) { HelloWorld hello; hello.PrintMessage("Hello World!"); return 0; }
Nous utiliserons CMake pour construire le projet:
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)
Nous allons créer deux bibliothèques différentes avec le même code source, ainsi que deux fichiers binaires avec les mêmes sources. Générez le projet et exécutez
md5sum
pour voir les sommes de contrĂ´le de tous les fichiers binaires:
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
Nous obtenons une conclusion comme celle-ci:
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
Ceci est intéressant car les
helloB
helloA
et
helloB
ont les mêmes sommes de contrôle, ainsi que les fichiers objets intermédiaires Mach-O
hello_world.cpp.o
, mais cela ne peut pas ĂŞtre dit pour les fichiers avec l'extension
.a
. En effet, ils stockent des informations sur les fichiers d'objet intermédiaires dans un format d'archive. L'en-tête de ce format comprend un champ appelé
st_time
défini par l'appel système
stat
. Vérifiez
libHelloLibA.a
et
libHelloLibB.a
utilisant
otool
pour afficher les en-tĂŞtes:
> 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
Nous voyons que le fichier contient plusieurs champs temporaires qui rendent notre assemblage non déterministe. Notez que ces champs ne s'appliquent pas au fichier exécutable final car ils ont la même somme de contrôle. Ce problème peut également se produire lors de la génération sous Windows avec Visual Studio, mais avec un fichier PE au lieu de Mach-O.
À ce stade, nous pouvons essayer d'aggraver les choses et de rendre nos binaires également non déterministes. Modifiez le fichier
main.cpp
pour qu'il inclue la macro
__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; }
Vérifiez à nouveau les sommes de contrôle des fichiers:
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
Nous voyons que maintenant nous avons différents binaires. Nous pourrions analyser l'exécutable avec un outil comme un
diffoscope , qui montre la différence entre deux fichiers binaires:
> diffoscope helloA helloB --- helloA +++ helloB ├── otool -arch x86_64 -tdvV {} │┄ Code for architecture x86_64 │ @@ -16,15 +16,15 @@ │ 00000001000018da jmp 0x1000018df │ 00000001000018df 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 #
Il montre que les informations
__TIME__
été collées dans le binaire, ce qui les rend non déterministes. Voyons ce qui peut être fait pour éviter cela.
Solutions possibles pour Microsoft Visual Studio
Microsoft Visual Studio a un indicateur d'éditeur de liens / Brepro qui n'est pas documenté par Microsoft. Cet indicateur définit les horodatages du format exécutable portable sur -1, comme illustré dans la figure ci-dessous.

Pour activer cet indicateur avec CMake, nous devons ajouter les lignes suivantes lors de la création du
.exe
:
add_link_options("/Brepro")
ou ces lignes pour
.lib
set_target_properties( TARGET PROPERTIES STATIC_LIBRARY_OPTIONS "/Brepro" )
Le problème est que cet indicateur rend les binaires jouables (par rapport aux horodatages au format de fichier) dans notre fichier binaire final .exe, mais ne supprime pas tous les horodatages du .lib (le même problème qu'avec les fichiers objets Mach-O, dont nous avons parlé plus haut). Le champ TimeDateStamp du
fichier d'en-tĂŞte COFF pour les fichiers
.lib
restera. La seule façon de supprimer ces informations du fichier binaire
.lib
est de corriger le
.lib
en remplaçant les octets correspondant au champ TimeDateStamp par toute valeur connue.
Solutions possibles pour GCC et CLANG
- gcc détecte l'existence de la variable d'environnement SOURCE_DATE_EPOCH. Si cette variable est définie, sa valeur indique l'horodatage UNIX qui sera utilisé pour remplacer la date et l'heure actuelles dans les macros
__DATE__
et __TIME__
afin que les horodatages intégrés deviennent reproductibles. La valeur peut être définie sur un horodatage connu, tel que l'heure de la dernière modification des fichiers source ou du package. - clang utilise
ZERO_AR_DATE
, qui, s'il est défini, réinitialise l' ZERO_AR_DATE
fourni dans les fichiers d'archive, en le définissant sur 0. Notez que cela ne __DATE__
__TIME__
__DATE__
ou __TIME__
. Si nous voulons corriger l'effet de cette macro, nous devons soit corriger les binaires, soit simuler l'heure système.
Continuons avec notre exemple de projet pour MacOS et voyons quels seront les résultats lors de la
ZERO_AR_DATE
la
ZERO_AR_DATE
environnement
ZERO_AR_DATE
.
export ZERO_AR_DATE=1
Maintenant, si nous compilons notre fichier exécutable et nos bibliothèques (en supprimant la macro
__DATE__
dans les sources), nous obtenons:
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
Toutes les sommes de contrôle sont désormais identiques. Analysons les en-têtes de fichiers avec l'extension
.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
Nous pouvons voir que le champ d'
timestamp
de l'en-tête de bibliothèque a été mis à zéro.
Nous sommes arrivés en douceur à la fin de la première partie de l'article. La suite du matériel peut être lue ici .