Pengantar majelis deterministik dalam C / C ++. Bagian 1

Terjemahan artikel ini disiapkan khusus untuk siswa kursus "Pengembang C ++" .




Apa itu majelis deterministik?


Rakitan deterministik adalah proses membangun kode sumber yang sama dengan lingkungan dan instruksi perakitan yang sama, di mana file biner yang sama dibuat dalam kasus apa pun, bahkan jika mereka dibuat pada mesin yang berbeda, di direktori yang berbeda dan dengan nama yang berbeda . Majelis seperti itu kadang-kadang juga disebut majelis yang dapat dimainkan atau disegel jika dijamin bahwa mereka akan membuat biner yang sama bahkan ketika menyusun dari folder yang berbeda.

Majelis deterministik bukanlah sesuatu yang terjadi dengan sendirinya. Mereka tidak dibuat dalam proyek biasa, dan alasan mengapa ini tidak terjadi mungkin berbeda untuk setiap sistem operasi atau kompiler.

Majelis deterministik harus dijamin untuk lingkungan perakitan yang diberikan. Ini berarti bahwa beberapa variabel, seperti sistem operasi, versi sistem pembangunan, dan arsitektur target , mungkin tetap sama di versi berbeda.

Dalam beberapa tahun terakhir, berbagai organisasi, seperti Chromium , Reproducible builds, atau Yocto , telah melakukan upaya besar untuk mencapai majelis deterministik.

Pentingnya majelis deterministik


Ada dua alasan utama mengapa majelis deterministik sangat penting:

  • Keamanan Mengubah biner alih-alih kode sumber dapat membuat perubahan tidak terlihat oleh penulis asli. Ini bisa berakibat fatal di lingkungan yang kritis seperti kedokteran, penerbangan, dan ruang. Hasil yang berpotensi identik untuk bahan-bahan ini memungkinkan pihak ketiga untuk mencapai konsensus pada hasil yang benar.
  • Penelusuran dan kontrol biner . Jika Anda ingin memiliki repositori untuk menyimpan file biner Anda, maka kemungkinan besar Anda tidak ingin membuat file biner dengan checksum acak dari sumber dalam revisi yang sama. Ini dapat menyebabkan sistem repositori untuk menyimpan binari yang berbeda sebagai versi yang berbeda ketika mereka harus sama. Misalnya, jika Anda bekerja di Windows atau MacOS, perpustakaan memiliki bidang dengan waktu pembuatan / modifikasi file objek yang termasuk di dalamnya, yang akan menyebabkan perbedaan dalam file biner.

File biner yang terlibat dalam proses build di C / C ++


Ada berbagai jenis biner yang dibuat selama proses pembangunan di C / C ++, tergantung pada sistem operasi.

Microsoft Windows Yang paling penting adalah file dengan ekstensi .obj , .lib , .lib dll dan .exe . Mereka semua mematuhi spesifikasi format portable executable (PE). File-file ini dapat dianalisis dengan alat-alat seperti dumpbin .
Linux File dengan ekstensi .o , .a , .so dan tanpa ekstensi (untuk file biner yang dapat dieksekusi) sesuai dengan format file yang dapat dieksekusi dan ditautkan (ELF). Isi file ELF dapat dianalisis menggunakan readelf .
OS Mac File dengan ekstensi .o , .a , .dylib , dan tanpa ekstensi (untuk file biner yang dapat dieksekusi) mematuhi spesifikasi format Mach-O. File-file ini dapat diverifikasi menggunakan aplikasi otool , yang merupakan bagian dari toolkit Xcode pada MacOS.

Sumber Variasi


Banyak faktor yang berbeda dapat membuat majelis Anda menjadi non-deterministik . Faktor-faktor akan bervariasi untuk sistem operasi dan kompiler yang berbeda. Setiap kompiler memiliki parameter tertentu untuk memperbaiki sumber variasi. Sampai saat ini, gcc dan clang adalah kompiler yang berisi lebih banyak opsi untuk memperbaiki. Ada beberapa opsi tidak berdokumen untuk msvc yang dapat Anda coba, tetapi pada akhirnya, Anda mungkin harus memperbaiki binari untuk mendapatkan rakitan deterministik.

Stempel Waktu Ditambahkan oleh Compiler / Linker


Ada dua alasan utama mengapa biner kami berisi informasi waktu yang membuatnya tidak dapat diputar:

  • Menggunakan __TIME__ atau __TIME__ di sumber.
  • Ketika format file memaksa Anda untuk menyimpan informasi waktu dalam file objek. Ini adalah kasus format Portable Executable di Windows dan Mach-O pada MacOS. Di Linux, file ELF tidak menyandikan cap waktu.

Mari kita lihat contoh di mana informasi ini berakhir dengan menyusun pustaka statis proyek hello world base di MacOS.

 . ├── CMakeLists.txt ├── hello_world.cpp ├── hello_world.hpp ├── main.cpp └── run_build.sh 

Perpustakaan menampilkan pesan di terminal:

 #include "hello_world.hpp" #include <iostream> void HelloWorld::PrintMessage(const std::string & message) { std::cout << message << std::endl; } 

Dan aplikasi akan menggunakan ini untuk menampilkan pesan "Hello World!":

 #include <iostream> #include "hello_world.hpp" int main(int argc, char** argv) { HelloWorld hello; hello.PrintMessage("Hello World!"); return 0; } 

Kami akan menggunakan CMake untuk membangun proyek:

 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) 

Kami akan membuat dua pustaka yang berbeda dengan kode sumber yang sama, serta dua file biner dengan sumber yang sama. Bangun proyek dan jalankan md5sum untuk melihat checksum dari semua file biner:

 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 

Kami mendapatkan kesimpulan seperti ini:

 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 

Ini menarik karena helloB dan helloB memiliki checksum yang sama, serta file objek perantara Mach-O hello_world.cpp.o , tetapi ini tidak dapat dikatakan untuk file dengan ekstensi .a . Ini karena mereka menyimpan informasi tentang file objek perantara dalam format arsip. Header format ini mencakup bidang yang disebut st_time ditetapkan oleh panggilan sistem stat . Periksa libHelloLibA.a dan libHelloLibB.a menggunakan libHelloLibB.a untuk menunjukkan header:

 > 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 

Kami melihat bahwa file tersebut berisi beberapa bidang sementara yang membuat majelis kami tidak deterministik. Perhatikan bahwa bidang ini tidak berlaku untuk file final yang dapat dieksekusi karena mereka memiliki checksum yang sama. Masalah ini juga dapat terjadi ketika membangun pada Windows dengan Visual Studio, tetapi dengan file PE bukan Mach-O.

Pada titik ini, kita dapat mencoba untuk memperburuk keadaan dan membuat biner kita menjadi non-deterministik. Ubah file main.cpp sehingga termasuk 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; } 

Periksa checksum file lagi:

 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 

Kami melihat bahwa sekarang kami memiliki binari yang berbeda. Kita bisa menganalisis executable dengan alat seperti diffoscope , yang menunjukkan perbedaan antara dua file biner:

 > diffoscope helloA helloB --- helloA +++ helloB ├── otool -arch x86_64 -tdvV {} │┄ Code for architecture x86_64 │ @@ -16,15 +16,15 @@ │ 00000001000018da jmp 0x1000018df00000001000018df 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 # 

Ini menunjukkan bahwa informasi __TIME__ telah disisipkan ke dalam biner, yang membuatnya menjadi non-deterministik. Mari kita lihat apa yang bisa dilakukan untuk menghindari hal ini.

Kemungkinan Solusi untuk Microsoft Visual Studio


Microsoft Visual Studio memiliki tanda tautan / bendera Brepro yang tidak didokumentasikan oleh Microsoft. Bendera ini menetapkan stempel waktu dari format Portable Executable ke -1, seperti terlihat pada gambar di bawah ini.



Untuk mengaktifkan flag ini dengan CMake, kita harus menambahkan baris berikut saat membuat .exe :

 add_link_options("/Brepro") 

atau baris ini untuk .lib

 set_target_properties( TARGET PROPERTIES STATIC_LIBRARY_OPTIONS "/Brepro" ) 

Masalahnya adalah bahwa flag ini membuat binari dimainkan (relatif terhadap cap waktu dalam format file) dalam biner .exe terakhir kita, tetapi tidak menghapus semua cap waktu dari .lib (masalah yang sama seperti dengan file objek Mach-O, yang kita bicarakan di atas). Bidang TimeDateStamp dari file header COFF untuk file .lib akan tetap ada. Satu-satunya cara untuk menghapus informasi ini dari file .lib biner adalah dengan memperbaiki .lib dengan mengganti byte yang sesuai dengan bidang TimeDateStamp dengan nilai yang diketahui.

Kemungkinan solusi untuk GCC dan CLANG


  • gcc mendeteksi keberadaan variabel lingkungan SOURCE_DATE_EPOCH. Jika variabel ini disetel, nilainya menunjukkan cap waktu UNIX yang akan digunakan untuk mengganti tanggal dan waktu saat ini di makro __TIME__ dan __TIME__ sehingga stempel waktu __DATE__ menjadi dapat direproduksi. Nilai dapat diatur ke cap waktu yang dikenal, seperti waktu perubahan terakhir ke file sumber atau paket.
  • dentang menggunakan ZERO_AR_DATE , yang, jika disetel, menyetel ulang ZERO_AR_DATE waktu ZERO_AR_DATE disediakan dalam file arsip, atur ke 0. Perhatikan bahwa ini tidak akan memperbaiki __TIME__ atau __TIME__ . Jika kita ingin memperbaiki efek makro ini, kita harus memperbaiki binari atau memalsukan waktu sistem.

Mari kita lanjutkan dengan proyek sampel kami untuk MacOS dan lihat apa hasilnya ketika mengatur ZERO_AR_DATE lingkungan ZERO_AR_DATE .

 export ZERO_AR_DATE=1 

Sekarang, jika kita mengkompilasi file dan pustaka yang dapat dieksekusi (menghapus __DATE__ makro di sumber), kita mendapatkan:

 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 

Semua checksum sekarang sama. .a menganalisis file header dengan ekstensi .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 

Kita dapat melihat bahwa bidang timestamp dari tajuk perpustakaan disetel ke nol.

Kami dengan lancar sampai pada bagian akhir artikel. Kelanjutan materi bisa dibaca di sini .

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


All Articles