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

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



Baca bagian pertama



Informasi folder perakitan didistribusikan dalam file biner


Jika file sumber yang sama dikompilasi dalam folder yang berbeda, kadang-kadang informasi folder ditransfer ke file biner. Ini dapat terjadi terutama karena dua alasan:

  • Menggunakan makro yang berisi informasi tentang file saat ini, seperti makro __FILE__ .
  • Buat binari debug yang menyimpan informasi tentang di mana sumbernya.

Melanjutkan contoh halo dunia kami di MacOS, mari kita bagi sumber sehingga kita dapat menunjukkan efek lokasi pada binari akhir. Struktur proyek akan mirip dengan yang di bawah ini.

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

Mari kita kumpulkan file biner kita dalam mode debug.

 cd srcA/build cmake -DCMAKE_BUILD_TYPE=Debug .. make cd .. && cd .. cd srcB/build cmake -DCMAKE_BUILD_TYPE=Debug .. make cd .. && cd .. md5sum srcA/build/hello md5sum srcB/build/hello md5sum srcA/build/CMakeFiles/HelloLib.dir/hello_world.cpp.o md5sum srcB/build/CMakeFiles/HelloLib.dir/hello_world.cpp.o md5sum srcA/build/libHelloLib.a md5sum srcB/build/libHelloLib.a     : 3572a95a8699f71803f3e967f92a5040 srcA/build/hello 7ca693295e62de03a1bba14853efa28c srcB/build/hello 76e0ae7c4ef79ec3be821ccf5752730f srcA/build/CMakeFiles/HelloLib.dir/hello_world.cpp.o 5ef044e6dcb73359f46d48f29f566ae5 srcB/build/CMakeFiles/HelloLib.dir/hello_world.cpp.o dc941156608b578c91e38f8ecebfef6d srcA/build/libHelloLib.a 1f9697ef23bf70b41b39ef3469845f76 srcB/build/libHelloLib.a 

Informasi tentang folder ditransfer dari file objek ke file yang dapat dieksekusi akhir, yang membuat majelis kami tidak dapat diproduksi kembali. Kita dapat melihat perbedaan antara binari menggunakan diffoscope untuk melihat di mana informasi folder tertanam.

 > diffoscope helloA helloB --- srcA/build/hello +++ srcB/build/hello @@ -1282,20 +1282,20 @@ ... 00005070: 5f77 6f72 6c64 5f64 6562 7567 2f73 7263 _world_debug/src -00005080: 412f 006d 6169 6e2e 6370 7000 2f55 7365 A/.main.cpp./Use +00005080: 422f 006d 6169 6e2e 6370 7000 2f55 7365 B/.main.cpp./Use 00005090: 7273 2f63 6172 6c6f 732f 446f 6375 6d65 rs/carlos/Docume 000050a0: 6e74 732f 6465 7665 6c6f 7065 722f 7265 nts/developer/re 000050b0: 7072 6f64 7563 6962 6c65 2d62 7569 6c64 producible-build 000050c0: 732f 7361 6e64 626f 782f 6865 6c6c 6f5f s/sandbox/hello_ -000050d0: 776f 726c 645f 6465 6275 672f 7372 6341 world_debug/srcA +000050d0: 776f 726c 645f 6465 6275 672f 7372 6342 world_debug/srcB 000050e0: 2f62 7569 6c64 2f43 4d61 6b65 4669 6c65 /build/CMakeFile 000050f0: 732f 6865 6c6c 6f2e 6469 722f 6d61 696e s/hello.dir/main 00005100: 2e63 7070 2e6f 005f 6d61 696e 005f 5f5a .cpp.o._main.__Z ... @@ -1336,15 +1336,15 @@ ... 000053c0: 6962 6c65 2d62 7569 6c64 732f 7361 6e64 ible-builds/sand 000053d0: 626f 782f 6865 6c6c 6f5f 776f 726c 645f box/hello_world_ -000053e0: 6465 6275 672f 7372 6341 2f62 7569 6c64 debug/srcA/build +000053e0: 6465 6275 672f 7372 6342 2f62 7569 6c64 debug/srcB/build 000053f0: 2f6c 6962 4865 6c6c 6f4c 6962 2e61 2868 /libHelloLib.a(h 00005400: 656c 6c6f 5f77 6f72 6c64 2e63 7070 2e6f ello_world.cpp.o 00005410: 2900 5f5f 5a4e 3130 4865 6c6c 6f57 6f72 ).__ZN10HelloWor ... 

Kemungkinan solusi


Sekali lagi, keputusan akan tergantung pada kompiler yang digunakan:

  • msvc tidak dapat mengatur parameter untuk menghindari menambahkan informasi ini ke file biner. Satu-satunya cara untuk mendapatkan binari yang dapat direproduksi adalah dengan menggunakan alat perbaikan lagi untuk menghapus informasi ini selama tahap pembuatan. Harap perhatikan bahwa karena kami memperbaiki binari untuk menghasilkan binari yang dapat direproduksi, folder yang digunakan untuk rakitan yang berbeda harus memiliki panjang karakter yang sama.
  • gcc memiliki tiga flag kompiler untuk mengatasi masalah ini:
    • -fdebug-prefix-map=OLD=NEW dapat menghapus prefiks direktori dari informasi debug.
    • -fmacro-prefix-map=OLD=NEW tersedia sejak gcc 8 dan menyelesaikan masalah irreproducibilitas dengan menggunakan makro __FILE__.
    • -ffile-prefix-map=OLD=NEW tersedia sejak gcc 8 dan merupakan gabungan dari -fdebug-awalan-peta dan -fmacro-awalan-peta
  • clang mendukung -fdebug-prefix-map=OLD=NEW sejak versi 3.8 dan sedang bekerja mendukung dua bendera lain untuk versi masa depan.

Cara terbaik untuk mengatasi masalah ini adalah menambahkan flag ke opsi kompiler. Saat menggunakan CMake:

 target_compile_options(target PUBLIC "-ffile-prefix-map=${CMAKE_SOURCE_DIR}=.") 

Urutan file dalam sistem build


Urutan file dapat menjadi masalah jika direktori dibaca untuk membuat daftar file mereka. Sebagai contoh, Unix tidak memiliki urutan deterministik di mana readdir () dan listdir () harus mengembalikan konten direktori, jadi mempercayai fungsi-fungsi ini untuk memberi makan sistem perakitan dapat menyebabkan majelis non-deterministik.

Masalah yang sama terjadi, misalnya, jika sistem build Anda menyimpan file untuk linker dalam sebuah wadah (misalnya, dalam kamus python biasa), yang dapat mengembalikan elemen dalam urutan non-deterministik. Ini akan menyebabkan file akan ditautkan dalam urutan yang berbeda setiap kali, dan file biner yang berbeda akan dibuat.

Kami dapat mensimulasikan masalah ini dengan mengatur ulang file di CMake. Jika kita memodifikasi contoh sebelumnya untuk memiliki lebih dari satu file sumber untuk perpustakaan:

 . ├── CMakeLists.txt ├── CMakeListsA.txt ├── CMakeListsB.txt ├── hello_world.cpp ├── hello_world.hpp ├── main.cpp ├── sources0.cpp ├── sources0.hpp ├── sources1.cpp ├── sources1.hpp ├── sources2.cpp └── sources2.hpp 

Kita dapat melihat bahwa hasil kompilasi berbeda jika kita mengubah urutan file di CMakeLists.txt :

 cmake_minimum_required(VERSION 3.0) project(HelloWorld) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) add_library(HelloLib hello_world.cpp sources0.cpp sources1.cpp sources2.cpp) add_executable(hello main.cpp) target_link_libraries(hello HelloLib) 

Jika kita membuat dua rakitan berturut-turut dengan nama A dan B, menukar sources0.cpp dan sources1.cpp dalam daftar file, kita mendapatkan checksum berikut:

 30ab264d6f8e1784282cd1a415c067f2 helloA cdf3c9dd968f7363dc9e8b40918d83af helloB 707c71bc2a8def6885b96fb67b84d79c hello_worldA.cpp.o 707c71bc2a8def6885b96fb67b84d79c hello_worldB.cpp.o 694ff3765b688e6faeebf283052629a3 sources0A.cpp.o 694ff3765b688e6faeebf283052629a3 sources0B.cpp.o 0db24dc6a94da1d167c68b96ff319e56 sources1A.cpp.o 0db24dc6a94da1d167c68b96ff319e56 sources1B.cpp.o fd0754d9a4a44b0fcc4e4f3c66ad187c sources2A.cpp.o fd0754d9a4a44b0fcc4e4f3c66ad187c sources2B.cpp.o baba9709d69c9e5fd51ad985ee328172 libHelloLibA.a 72641dc6fc4f4db04166255f62803353 libHelloLibB.a 

File objek .o identik, tetapi .a library dan executable tidak. Ini karena urutan penyisipan di perpustakaan tergantung pada urutan di mana file terdaftar.

Kompiler menciptakan keacakan


Masalah ini terjadi, misalnya, dalam gcc ketika Optimasi Tautan-Waktu (bendera -flto ) -flto . Opsi ini memperkenalkan nama yang dihasilkan secara acak ke dalam file biner. Satu-satunya cara untuk menghindari masalah ini adalah dengan menggunakan flag - frandom-seed . Opsi ini menyediakan seed yang digunakan gcc alih-alih angka acak. Ini digunakan untuk menghasilkan nama simbol tertentu, yang harus berbeda di setiap file yang dikompilasi. Ini juga digunakan untuk menempatkan perangko unik dalam file cakupan data dan file objek yang menghasilkannya. Parameter ini harus berbeda untuk setiap file sumber. Salah satu opsi adalah mengatur checksum file sehingga kemungkinan tabrakan sangat rendah. Misalnya, di CMake, ini bisa dilakukan menggunakan fungsi seperti ini:

 set(LIB_SOURCES ./src/source1.cpp ./src/source2.cpp ./src/source3.cpp) foreach(_file ${LIB_SOURCES}) file(SHA1 ${_file} checksum) string(SUBSTRING ${checksum} 0 8 checksum) set_property(SOURCE ${_file} APPEND_STRING PROPERTY COMPILE_FLAGS "-frandom-seed=0x${checksum}") endforeach() 

Beberapa tips untuk menggunakan Conan


Conan hooks dapat membantu kami membuat build kami dapat direproduksi. Fitur ini memungkinkan Anda untuk menyesuaikan perilaku klien pada titik-titik tertentu.

Salah satu cara untuk menggunakan kait adalah dengan mengatur variabel lingkungan pada tahap pre_build . Pada contoh di bawah ini, fungsi set_environment , dan kemudian lingkungan dikembalikan pada langkah reset_environment menggunakan reset_environment .

 def set_environment(self): if self._os == "Linux": self._old_source_date_epoch = os.environ.get("SOURCE_DATE_EPOCH") timestamp = "1564483496" os.environ["SOURCE_DATE_EPOCH"] = timestamp self._output.info( "set SOURCE_DATE_EPOCH: {}".format(timestamp)) elif self._os == "Macos": os.environ["ZERO_AR_DATE"] = "1" self._output.info( "set ZERO_AR_DATE: {}".format(timestamp)) def reset_environment(self): if self._os == "Linux": if self._old_source_date_epoch is None: del os.environ["SOURCE_DATE_EPOCH"] else: os.environ["SOURCE_DATE_EPOCH"] = self._old_source_date_epoch elif self._os == "Macos": del os.environ["ZERO_AR_DATE"] 

Hook juga dapat berguna untuk memperbaiki binari pada tahap post_build . Ada berbagai alat untuk menganalisis dan memperbaiki file biner, seperti ducible , pefile , pe-parse atau strip-nondeterminism . Contoh kait untuk memperbaiki biner PE menggunakan ducible mungkin:

 class Patcher(object): ... def patch(self): if self._os == "Windows" and self._compiler == "Visual Studio": for root, _, filenames in os.walk(self._conanfile.build_folder): for filename in filenames: filename = os.path.join(root, filename) if ".exe" in filename or ".dll" in filename: self._patch_pe(filename) def _patch_pe(self, filename): patch_tool_location = "C:/ducible/ducible.exe" if os.path.isfile(patch_tool_location): self._output.info("Patching {} with md5sum: {}".format(filename,md5sum(filename))) self._conanfile.run("{} {}".format(patch_tool_location, filename)) self._output.info("Patched file: {} with md5sum: {}".format(filename,md5sum(filename))) ... def pre_build(output, conanfile, **kwargs): lib_patcher.init(output, conanfile) lib_patcher.set_environment() def post_build(output, conanfile, **kwargs): lib_patcher.patch() lib_patcher.reset_environment() 

Kesimpulan


Majelis deterministik adalah tugas yang kompleks, terkait erat dengan sistem operasi dan toolkit yang digunakan. Pendahuluan ini dimaksudkan untuk membantu memahami penyebab paling umum dari kurangnya determinisme dan cara mengatasinya.

Referensi


Informasi umum



Alat-alatnya


Alat Perbandingan Biner


Alat Perbaikan File


Alat Analisis File


Baca bagian pertama

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


All Articles