تم إعداد ترجمة المقال خاصة لطلاب الدورة التدريبية "C ++ Developer" .
→
اقرأ الجزء الأول
يتم توزيع معلومات مجلد التجميع في الملفات الثنائية
إذا تم تصنيف الملفات المصدر نفسها في مجلدات مختلفة ، في بعض الأحيان يتم نقل معلومات المجلد إلى ملفات ثنائية. يمكن أن يحدث هذا بشكل رئيسي لسببين:
- استخدام وحدات الماكرو التي تحتوي على معلومات حول الملف الحالي ، مثل
__FILE__
الماكرو. - إنشاء ثنائيات التصحيح التي تخزن معلومات حول مكان المصادر.
استمرارًا لمثال hello world على نظام MacOS ، دعنا نقسم المصدر حتى نتمكن من إظهار تأثير الموقع على الثنائيات النهائية. سيكون هيكل المشروع مماثلاً للهيكل التالي.
. ├── 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
دعنا نجمع ملفاتنا الثنائية في وضع التصحيح.
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
يتم نقل المعلومات حول المجلد من ملفات الكائنات إلى الملفات القابلة للتنفيذ النهائية ، مما يجعل تجميعاتنا غير قابلة للإنتاج. يمكننا أن نرى الاختلافات بين الثنائيات باستخدام المنشور لمعرفة أين يتم تضمين معلومات المجلد.
> 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 ...
الحلول الممكنة
مرة أخرى ، يعتمد القرار على المترجم المستخدم:
- لا يمكن لـ msvc تعيين معلمات لتجنب إضافة هذه المعلومات إلى الملفات الثنائية. الطريقة الوحيدة للحصول على ثنائيات قابلة للتكرار هي استخدام أداة الإصلاح مرة أخرى لإزالة هذه المعلومات أثناء مرحلة البناء. يرجى ملاحظة أنه نظرًا لأننا نصلح الثنائيات لإنتاج ثنائيات قابلة للتكرار ، فإن المجلدات المستخدمة للتجميعات المختلفة يجب أن تكون بنفس الطول بالأحرف.
- يحتوي
gcc
على ثلاثة أعلام برنامج التحويل البرمجي للتغلب على هذه المشكلة:
-fdebug-prefix-map=OLD=NEW
يمكنه إزالة بادئات الدليل من معلومات التصحيح.-fmacro-prefix-map=OLD=NEW
متوفر منذ gcc 8 ويحل مشكلة عدم القدرة على الإنتاج باستخدام الماكرو __FILE__.-ffile-prefix-map=OLD=NEW
متاح منذ gcc 8 وهو اتحاد -fdebug-prefix-map و -fmacro-prefix-map
- يدعم
-fdebug-prefix-map=OLD=NEW
منذ الإصدار 3.8 ويعمل على دعم -fdebug-prefix-map=OLD=NEW
للإصدارات المستقبلية.
أفضل طريقة لحل هذه المشكلة هي إضافة علامات إلى خيارات برنامج التحويل البرمجي. عند استخدام CMake:
target_compile_options(target PUBLIC "-ffile-prefix-map=${CMAKE_SOURCE_DIR}=.")
ترتيب الملفات في نظام البناء
يمكن أن يكون ترتيب الملفات مشكلة في حالة قراءة الدلائل لإنشاء قائمة بملفاتها. على سبيل المثال ، لا يحتوي نظام Unix على ترتيب محدد حيث يجب على readdir () و listdir () إرجاع محتويات الدليل ، لذلك فإن الثقة في هذه الوظائف لتغذية نظام التجميع يمكن أن يؤدي إلى تجميعات غير حتمية.
تحدث نفس المشكلة ، على سبيل المثال ، إذا كان نظام الإنشاء الخاص بك يقوم بتخزين الملفات الخاصة بالرابط في حاوية (على سبيل المثال ، في قاموس بيثون عادي) ، والذي يمكنه إرجاع العناصر بترتيب غير محدد. سيؤدي ذلك إلى ربط الملفات بترتيب مختلف في كل مرة ، وسيتم إنشاء ملفات ثنائية مختلفة.
يمكننا محاكاة هذه المشكلة عن طريق إعادة ترتيب الملفات في CMake. إذا عدّلنا المثال السابق ليكون لدينا أكثر من ملف مصدر للمكتبة:
. ├── 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
يمكننا أن نرى أن نتائج
CMakeLists.txt
مختلفة إذا قمنا بتغيير ترتيب الملفات في
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)
إذا قمنا
sources0.cpp
متتاليين بالأسماء A و B ، مع تبديل
sources0.cpp
و
sources0.cpp
في قائمة الملفات ،
sources0.cpp
نحصل على الاختبارات التالية:
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
ملفات
.o
للكائنات متطابقة ، لكن المكتبات والتنفيذيات ليست كذلك. وذلك لأن ترتيب الإدراج في المكتبة يعتمد على ترتيب سرد الملفات.
مترجم خلق العشوائية
تحدث هذه المشكلة ، على سبيل المثال ، في gcc عند
-flto
وقت الارتباط (علامة
-flto
). يقدم هذا الخيار أسماء تم إنشاؤها عشوائيًا في ملفات ثنائية. الطريقة الوحيدة لتجنب هذه المشكلة هي استخدام العلم -
frandom-seed
. يوفر هذا الخيار بذرة يستخدمها gcc بدلاً من الأرقام العشوائية. يتم استخدامه لإنشاء أسماء رموز محددة ، والتي يجب أن تكون مختلفة في كل ملف مترجم. كما يستخدم لوضع الطوابع الفريدة في ملفات تغطية البيانات وملفات الكائنات التي تنتجها. يجب أن تكون هذه المعلمة مختلفة لكل ملف مصدر. أحد الخيارات هو تعيين المجموع الاختباري للملف بحيث يكون احتمال التصادم منخفضًا جدًا. على سبيل المثال ، في CMake ، يمكن القيام بذلك باستخدام دالة مثل هذا:
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()
بعض النصائح لاستخدام كونان
السنانير كونان يمكن أن تساعدنا في جعل بنينا استنساخه. تتيح لك هذه الميزة تخصيص سلوك العميل في بعض النقاط.
يمكن أن
pre_build
إحدى طرق استخدام السنانير في تعيين متغيرات البيئة في مرحلة ما قبل
pre_build
. في المثال أدناه ، يتم
set_environment
دالة
set_environment
، ثم استعادة البيئة في خطوة
reset_environment
باستخدام
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"]
يمكن أن تكون الخطافات مفيدة أيضًا لإصلاح الثنائيات في مرحلة
post_build
. هناك أدوات متنوعة لتحليل وتصحيح الملفات الثنائية ، مثل
pefile
أو
pefile
أو
pe-parse
pefile
أو
strip-nondeterminism
. قد يكون مثال الخطاف لتثبيت ثنائي PE باستخدام
ducible
:
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()
النتائج
التجميعات الحتمية هي مهمة معقدة ، ترتبط ارتباطًا وثيقًا بنظام التشغيل ومجموعة الأدوات المستخدمة. كان من المفترض أن تساعد هذه المقدمة في فهم الأسباب الأكثر شيوعًا لنقص الحتمية وكيفية معالجتها.
مراجع
معلومات عامة
الأدوات
أدوات المقارنة الثنائيةأدوات إصلاح الملفأدوات تحليل الملفات→
اقرأ الجزء الأول