EinfĂŒhrung in deterministische Assemblys in C / C ++. Teil 1

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 .

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


All Articles