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