Die Ăbersetzung des Artikels wurde speziell fĂŒr Studenten des Kurses "C ++ Developer" vorbereitet.
Was ist eine deterministische Versammlung?
Eine deterministische Assemblierung ist der Prozess des Assemblierens desselben Quellcodes mit derselben Umgebung und derselben Assembleranweisung, in dem auf jeden Fall dieselben BinÀrdateien erstellt werden, selbst wenn sie auf verschiedenen Computern, in verschiedenen Verzeichnissen und mit unterschiedlichen Namen erstellt werden . Solche Assemblys werden manchmal auch als abspielbare oder versiegelte Assemblys bezeichnet, wenn garantiert ist, dass sie auch beim Kompilieren aus verschiedenen Ordnern dieselben BinÀrdateien erstellen.
Deterministische Versammlungen passieren nicht von alleine. Sie werden nicht in normalen Projekten erstellt, und die GrĂŒnde, warum dies nicht geschieht, können fĂŒr jedes Betriebssystem oder jeden Compiler unterschiedlich sein.
Deterministische Baugruppen mĂŒssen fĂŒr eine bestimmte
Baugruppenumgebung garantiert werden. Dies bedeutet, dass einige Variablen, wie z. B.
Betriebssystem, Build-Systemversionen und Zielarchitektur , in verschiedenen Builds vermutlich gleich bleiben.
In den letzten Jahren haben verschiedene Organisationen wie
Chromium ,
Reproducible Builds oder
Yocto groĂe Anstrengungen unternommen, um deterministische Baugruppen zu erreichen.
Die Bedeutung deterministischer Versammlungen
Es gibt zwei HauptgrĂŒnde, warum deterministische Versammlungen so wichtig sind:
- Sicherheit Das Ăndern von BinĂ€rdateien anstelle von Quellcode kann die Ănderungen fĂŒr die ursprĂŒnglichen Autoren unsichtbar machen. Dies kann in sicherheitskritischen Umgebungen wie Medizin, Luftfahrt und Weltraum tödlich sein. Potenziell identische Ergebnisse fĂŒr diese Materialien ermöglichen es Dritten, einen Konsens ĂŒber das richtige Ergebnis zu erzielen.
- RĂŒckverfolgbarkeit und binĂ€re Kontrolle . Wenn Sie ein Repository zum Speichern Ihrer BinĂ€rdateien haben möchten, möchten Sie höchstwahrscheinlich keine BinĂ€rdateien mit zufĂ€lligen PrĂŒfsummen aus Quellen in derselben Revision erstellen. Dies kann dazu fĂŒhren, dass das Repository-System verschiedene BinĂ€rdateien als unterschiedliche Versionen speichert, wenn sie identisch sein sollten. Wenn Sie beispielsweise unter Windows oder MacOS arbeiten, enthĂ€lt die Bibliothek Felder zum Zeitpunkt der Erstellung / Ănderung der darin enthaltenen Objektdateien, was zu Unterschieden bei BinĂ€rdateien fĂŒhrt.
BinÀrdateien, die am Erstellungsprozess in C / C ++ beteiligt sind
AbhÀngig vom Betriebssystem gibt es verschiedene Arten von BinÀrdateien, die wÀhrend des Erstellungsprozesses in C / C ++ erstellt werden.
Microsoft Windows Am wichtigsten sind die Dateien mit den Erweiterungen
.obj
,
.lib
.obj
dll
und
.exe
. Sie alle entsprechen der PE-Formatspezifikation (Portable Executable). Diese Dateien können mit Tools wie
dumpbin analysiert werden.
Linux Dateien mit den Erweiterungen
.o
,
.a
,
.so
und ohne Erweiterungen (fĂŒr ausfĂŒhrbare BinĂ€rdateien) entsprechen dem Format ausfĂŒhrbarer und zusammensetzbarer Dateien (ausfĂŒhrbares und verknĂŒpfbares Format, ELF). Der Inhalt von ELF-Dateien kann mit
readelf analysiert werden.
Mac OS Dateien mit den Erweiterungen
.o
,
.a
,
.dylib
und ohne Erweiterungen (fĂŒr ausfĂŒhrbare BinĂ€rdateien) entsprechen der Mach-O-
.dylib
. Diese Dateien können mit der
otool- Anwendung ĂŒberprĂŒft werden, die Teil des
Xcode- Toolkits unter MacOS ist.
Variationsquellen
Viele verschiedene Faktoren können Ihre Baugruppen
nicht deterministisch machen . Die Faktoren variieren fĂŒr verschiedene Betriebssysteme und Compiler. Jeder Compiler verfĂŒgt ĂŒber bestimmte Parameter, um die Variationsquellen zu korrigieren. Bisher sind
gcc
und
clang
die Compiler, die mehr Optionen zum Reparieren enthalten. Es gibt einige undokumentierte Optionen fĂŒr
msvc
, die Sie ausprobieren können, aber am Ende mĂŒssen Sie wahrscheinlich die BinĂ€rdateien reparieren, um deterministische Assemblys zu erhalten.
Vom Compiler / Linker hinzugefĂŒgte Zeitstempel
Es gibt zwei HauptgrĂŒnde, warum unsere BinĂ€rdateien Zeitinformationen enthalten können, die sie unspielbar machen:
- Verwenden der
__TIME__
__DATE__
oder __TIME__
in der Quelle. - Wenn ein Dateiformat Sie zwingt, Zeitinformationen in Objektdateien zu speichern. Dies ist der Fall beim Portable Executable-Format unter Windows und bei Mach-O unter MacOS. Unter Linux codieren ELF-Dateien keine Zeitstempel.
Schauen wir uns ein Beispiel an, in dem diese Informationen mit dem Kompilieren einer statischen Bibliothek des Hello World Base-Projekts unter MacOS enden.
. âââ CMakeLists.txt âââ hello_world.cpp âââ hello_world.hpp âââ main.cpp âââ run_build.sh
Die Bibliothek zeigt im Terminal eine Meldung an:
#include "hello_world.hpp" #include <iostream> void HelloWorld::PrintMessage(const std::string & message) { std::cout << message << std::endl; }
Und die Anwendung wird dies verwenden, um die Meldung "Hallo Welt!" Anzuzeigen:
#include <iostream> #include "hello_world.hpp" int main(int argc, char** argv) { HelloWorld hello; hello.PrintMessage("Hello World!"); return 0; }
Wir werden CMake verwenden, um das Projekt zu erstellen:
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)
Wir werden zwei verschiedene Bibliotheken mit demselben Quellcode sowie zwei BinĂ€rdateien mit denselben Quellen erstellen. Erstellen Sie das Projekt und fĂŒhren Sie
md5sum
, um die PrĂŒfsummen aller BinĂ€rdateien
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
Wir bekommen eine Schlussfolgerung wie folgt:
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
Dies ist interessant, da die
helloA
helloB
helloA
und
helloB
dieselben PrĂŒfsummen sowie die Mach-O-Zwischenobjektdateien
hello_world.cpp.o
haben. Dies gilt jedoch nicht fĂŒr Dateien mit der Erweiterung
.a
. Dies liegt daran, dass sie Informationen zu Zwischenobjektdateien in einem Archivformat speichern. Der Header dieses Formats enthÀlt ein Feld namens
st_time
das vom
stat
-Systemaufruf festgelegt wurde. ĂberprĂŒfen Sie
libHelloLibA.a
und
libHelloLibB.a
mit
otool
, um die Header
otool
:
> 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
Wir sehen, dass die Datei mehrere temporĂ€re Felder enthĂ€lt, die unsere Assembly nicht deterministisch machen. Beachten Sie, dass diese Felder nicht fĂŒr die endgĂŒltige ausfĂŒhrbare Datei gelten, da sie dieselbe PrĂŒfsumme haben. Dieses Problem kann auch beim Erstellen unter Windows mit Visual Studio auftreten, jedoch mit einer PE-Datei anstelle von Mach-O.
An diesem Punkt können wir versuchen, die Dinge noch schlimmer zu machen und unsere BinĂ€rdateien auch nicht deterministisch zu machen. Ăndern Sie die Datei
__TIME__
so, dass sie das Makro
__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; }
ĂberprĂŒfen Sie die PrĂŒfsummen der Dateien erneut:
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
Wir sehen, dass wir jetzt verschiedene BinĂ€rdateien haben. Wir könnten die ausfĂŒhrbare Datei mit einem Werkzeug wie einem
Diffoskop analysieren , das den Unterschied zwischen zwei BinÀrdateien zeigt:
> 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 #
Es zeigt, dass
__TIME__
-Informationen in die BinĂ€rdatei eingefĂŒgt wurden, wodurch sie nicht deterministisch sind. Mal sehen, was getan werden kann, um dies zu vermeiden.
Mögliche Lösungen fĂŒr Microsoft Visual Studio
Microsoft Visual Studio verfĂŒgt ĂŒber ein Linker / Brepro-Flag, das nicht von Microsoft dokumentiert ist. Dieses Flag setzt die Zeitstempel aus dem Portable Executable-Format auf -1 (siehe Abbildung unten).

Um dieses Flag mit CMake zu aktivieren, mĂŒssen wir beim Erstellen der
.exe
Datei die folgenden Zeilen hinzufĂŒgen:
add_link_options("/Brepro")
oder diese Zeilen fĂŒr
.lib
set_target_properties( TARGET PROPERTIES STATIC_LIBRARY_OPTIONS "/Brepro" )
Das Problem ist, dass dieses Flag die BinĂ€rdateien (relativ zu den Zeitstempeln im Dateiformat) in unserer endgĂŒltigen binĂ€ren EXE-Datei abspielbar macht, jedoch nicht alle Zeitstempel aus der LIB entfernt (dasselbe Problem wie bei den Mach-O-Objektdateien). worĂŒber wir oben gesprochen haben). Das TimeDateStamp-Feld aus
der COFF-Headerdatei fĂŒr
.lib
Dateien bleibt erhalten. Die einzige Möglichkeit, diese Informationen aus der binÀren
.lib
Datei zu entfernen, besteht darin, die
.lib
zu reparieren, indem die dem TimeDateStamp-Feld entsprechenden Bytes durch einen bekannten Wert ersetzt werden.
Mögliche Lösungen fĂŒr GCC und CLANG
- gcc erkennt das Vorhandensein der Umgebungsvariablen SOURCE_DATE_EPOCH. Wenn diese Variable festgelegt ist, gibt ihr Wert den UNIX-Zeitstempel an, der verwendet wird, um das aktuelle Datum und die aktuelle Uhrzeit in den Makros
__DATE__
und __TIME__
zu ersetzen, damit die integrierten Zeitstempel reproduzierbar werden. Der Wert kann auf einen bekannten Zeitstempel festgelegt werden, z. B. den Zeitpunkt der letzten Ănderung an den Quelldateien oder dem Paket. - clang verwendet
ZERO_AR_DATE
, das, falls festgelegt, den in den Archivdateien angegebenen ZERO_AR_DATE
auf 0 zurĂŒcksetzt. Beachten Sie, dass dadurch die __TIME__
__DATE__
oder __TIME__
nicht repariert werden. Wenn wir den Effekt dieses Makros korrigieren möchten, mĂŒssen wir entweder die BinĂ€rdateien korrigieren oder die Systemzeit vortĂ€uschen.
ZERO_AR_DATE
wir mit unserem Beispielprojekt fĂŒr MacOS fort und sehen, welche Ergebnisse beim Festlegen der Umgebungsvariablen
ZERO_AR_DATE
.
export ZERO_AR_DATE=1
Wenn wir nun unsere ausfĂŒhrbaren Dateien und Bibliotheken kompilieren (das Makro
__DATE__
in den Quellen entfernen), erhalten wir:
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
Alle PrĂŒfsummen sind jetzt gleich.
.a
die Dateikopfzeilen mit der Erweiterung
.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
Wir können sehen, dass das
timestamp
des Bibliotheksheaders auf Null gesetzt wurde.
Wir sind reibungslos zum Ende des ersten Teils des Artikels gekommen. Die Fortsetzung des Materials kann hier gelesen werden .