تم إعداد ترجمة المقال خاصة لطلاب الدورة التدريبية "C ++ Developer" .
ما هو التجمع الحتمية؟
التجميع الحتمية هو عملية تجميع نفس شفرة المصدر بنفس تعليمات التجميع والبيئة ، والتي يتم فيها إنشاء نفس الملفات الثنائية في أي حال ، حتى لو كانت مصنوعة على أجهزة مختلفة ، في دلائل مختلفة وبأسماء مختلفة . يشار إلى هذه التجميعات أحيانًا أيضًا بأنها تجميعات قابلة للتشغيل أو مختومة ، إذا كان مضمونًا أنها ستنشئ الثنائيات نفسها حتى عند التجميع من مجلدات مختلفة.
التجميعات الحتمية ليست شيئًا يحدث بحد ذاته. لا يتم إنشاؤها في مشاريع عادية ، وقد تختلف أسباب عدم حدوث ذلك لكل نظام تشغيل أو برنامج مترجم.
يجب ضمان التجميعات الحتمية
لبيئة تجميع معينة. هذا يعني أن بعض المتغيرات ، مثل
نظام التشغيل ، وإصدارات نظام البناء ، والهندسة المستهدفة ، من المفترض أن تظل كما هي في التجميعات المختلفة.
في السنوات الأخيرة ، بذلت العديد من المؤسسات ، مثل
Chromium أو
Reproducible builds أو
Yocto ، جهودًا كبيرة لتحقيق التجمعات الحتمية.
أهمية الجمعيات الحتمية
هناك سببان رئيسيان لكون التجمعات الحتمية مهمة جدًا:
- الأمن. يمكن أن يؤدي تغيير الثنائيات بدلاً من الكود المصدري إلى جعل التغييرات غير مرئية للمؤلفين الأصليين. هذا يمكن أن يكون قاتلاً في بيئات السلامة الحرجة مثل الطب والطيران والفضاء. تتيح النتائج المماثلة المحتملة لهذه المواد للأطراف الثالثة الوصول إلى توافق في الآراء بشأن النتيجة الصحيحة.
- التتبع والتحكم الثنائي . إذا كنت ترغب في الحصول على مستودع لتخزين الملفات الثنائية الخاصة بك ، فمن الأرجح أنك لا ترغب في إنشاء ملفات ثنائية مع اختباري عشوائي من مصادر في نفس المراجعة. يمكن أن يتسبب هذا في قيام نظام المستودع بتخزين ثنائيات مختلفة كإصدارات مختلفة عندما تكون متماثلة. على سبيل المثال ، إذا كنت تعمل في Windows أو MacOS ، فإن المكتبة بها حقول مع وقت إنشاء / تعديل ملفات الكائنات المضمنة فيها ، مما سيؤدي إلى اختلافات في الملفات الثنائية.
الملفات الثنائية المشاركة في عملية الإنشاء في C / C ++
هناك أنواع مختلفة من الثنائيات التي يتم إنشاؤها أثناء عملية الإنشاء في C / C ++ ، وفقًا لنظام التشغيل.
مايكروسوفت ويندوز الأكثر أهمية هي الملفات ذات الملحقات
.obj
،
.lib
dll
و.
.exe
. جميعهم يتوافق مع مواصفات تنسيق قابل للتنفيذ المحمولة (PE). يمكن تحليل هذه الملفات باستخدام أدوات مثل
dumpbin .
لينكس. تتوافق الملفات ذات الامتدادات
.o
و
.a
و
.so
وبدون ملحقات (للملفات الثنائية القابلة للتنفيذ) مع تنسيق الملفات القابلة للتنفيذ والرابط (ELF). يمكن تحليل محتويات ملفات ELF باستخدام
readelf .
نظام التشغيل Mac OS الملفات ذات الامتدادات
.o
و
.a
و
.dylib
وبدون ملحقات (للملفات الثنائية القابلة للتنفيذ) تمتثل لمواصفات تنسيق Mach-O. يمكن التحقق من هذه الملفات باستخدام تطبيق
otool ، الذي يعد جزءًا من مجموعة أدوات
Xcode على نظام MacOS.
مصادر الاختلافات
يمكن للعديد من العوامل المختلفة أن تجعل المجالس الخاصة بك
غير حتمية . العوامل سوف تختلف لأنظمة التشغيل المختلفة والمجمعين. كل مترجم لديه معلمات معينة لتصحيح مصادر الاختلاف. حتى الآن ، فإن
gcc
و
clang
هما المترجمان
clang
يحتويان على المزيد من الخيارات للتثبيت. هناك بعض الخيارات غير الموثقة لـ
msvc
التي يمكنك تجربتها ، ولكن في النهاية ، قد تضطر إلى إصلاح الثنائيات للحصول على تجميعات حتمية.
الطوابع الزمنية أضيفت من قبل المترجم / رابط
هناك سببان رئيسيان وراء احتواء الثنائيات على معلومات الوقت التي تجعلها غير قابلة للعب:
- باستخدام
__TIME__
أو __TIME__
في المصدر. - عندما يفرض عليك تنسيق الملف تخزين معلومات الوقت في ملفات الكائنات. هذه هي حالة Portable Executable format على Windows و Mach-O على MacOS. على نظام Linux ، لا تقوم ملفات ELF بترميز أي طوابع زمنية.
دعونا نلقي نظرة على مثال حيث تنتهي هذه المعلومات من خلال تجميع مكتبة ثابتة لمشروع hello world base على MacOS.
. ├── CMakeLists.txt ├── hello_world.cpp ├── hello_world.hpp ├── main.cpp └── run_build.sh
تعرض المكتبة رسالة في المحطة:
#include "hello_world.hpp" #include <iostream> void HelloWorld::PrintMessage(const std::string & message) { std::cout << message << std::endl; }
وسيستخدم التطبيق هذا لعرض رسالة "Hello World!":
#include <iostream> #include "hello_world.hpp" int main(int argc, char** argv) { HelloWorld hello; hello.PrintMessage("Hello World!"); return 0; }
سوف نستخدم CMake لبناء المشروع:
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)
سنقوم بإنشاء مكتبتين مختلفتين بنفس الكود المصدري ، بالإضافة إلى ملفين ثنائيين لهما نفس المصادر. بناء المشروع وتشغيل
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
نحصل على استنتاج مثل هذا:
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
يعد هذا
helloA
مثيرًا للاهتمام لأن
helloB
و
helloB
لها نفس المجموع الاختباري ، بالإضافة إلى ملفات الكائنات الوسيطة Mach-O
hello_world.cpp.o
، لكن لا يمكن قول ذلك للملفات ذات الملحق
.a
. هذا بسبب قيامهم بتخزين معلومات حول ملفات الكائنات الوسيطة بتنسيق أرشيف. يتضمن رأس هذا التنسيق حقلًا يسمى
st_time
تعيينه بواسطة استدعاء النظام
stat
. تحقق من
libHelloLibA.a
و
libHelloLibB.a
باستخدام
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
نرى أن الملف يحتوي على عدة حقول مؤقتة تجعل التجميع غير حاسم. لاحظ أن هذه الحقول لا تنطبق على الملف القابل للتنفيذ النهائي لأن لديهم نفس المجموع الاختباري. يمكن أن تحدث هذه المشكلة أيضًا عند البناء على Windows باستخدام Visual Studio ، ولكن مع ملف PE بدلاً من Mach-O.
في هذه المرحلة ، يمكننا محاولة جعل الأمور أسوأ وجعل ثنائياتنا أيضًا غير حتمية. قم بتغيير ملف
main.cpp
بحيث يتضمن الماكرو
__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; }
تحقق من اختباري الملفات مرة أخرى:
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
نرى الآن لدينا ثنائيات مختلفة. يمكننا تحليل الملف القابل للتنفيذ باستخدام أداة مثل
المنشور ، والتي توضح الفرق بين ملفين ثنائيين:
> 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 #
يُظهر أنه تم لصق معلومات
__TIME__
في الملف الثنائي ، مما يجعلها غير حاسمة. دعونا نرى ما يمكن القيام به لتجنب هذا.
الحلول الممكنة لبرنامج Microsoft Visual Studio
يحتوي Microsoft Visual Studio على علامة رابط / Brepro غير موثقة بواسطة Microsoft. تحدد هذه العلامة الطوابع الزمنية من التنسيق القابل للتنفيذ المحمول إلى -1 ، كما هو موضح في الشكل أدناه.

لتنشيط هذه العلامة مع CMake ، يجب إضافة الأسطر التالية عند إنشاء
.exe
:
add_link_options("/Brepro")
أو هذه الخطوط ل.
.lib
set_target_properties( TARGET PROPERTIES STATIC_LIBRARY_OPTIONS "/Brepro" )
المشكلة هي أن هذه العلامة تجعل الثنائيات قابلة للتشغيل (بالنسبة إلى الطوابع الزمنية في تنسيق الملف) في ملف exe الثنائية النهائي الخاص بنا ، ولكنها لا تزيل كل الطوابع الزمنية من .lib (نفس المشكلة كما في ملفات كائن Mach-O ، الذي تحدثنا عنه أعلاه). سيبقى الحقل TimeDateStamp من
ملف الرأس COFF لملفات
.lib
. الطريقة الوحيدة لإزالة هذه المعلومات من ملف
.lib
الثنائي هي إصلاح
.lib
عن طريق استبدال وحدات البايت المتوافقة مع حقل TimeDateStamp بأي قيمة معروفة.
الحلول الممكنة لدول مجلس التعاون الخليجي و CLANG
- يكتشف gcc وجود متغير البيئة SOURCE_DATE_EPOCH. إذا تم تعيين هذا المتغير ، تشير قيمته إلى الطابع الزمني UNIX الذي سيتم استخدامه لاستبدال التاريخ والوقت
__TIME__
وحدات الماكرو __TIME__
و __TIME__
بحيث تصبح الطوابع الزمنية المضمنة قابلة للتكرار. يمكن ضبط القيمة على طابع زمني معروف ، مثل الوقت الذي يتم فيه التغيير الأخير إلى الملفات المصدر أو الحزمة. - يستخدم
ZERO_AR_DATE
، والذي ، إذا تم تعيينه ، يعيد تعيين ZERO_AR_DATE
الزمني المقدم في ملفات الأرشيف ، __DATE__
على 0. لاحظ أن هذا لن __DATE__
__TIME__
أو __TIME__
. إذا كنا نريد إصلاح تأثير هذا الماكرو ، يجب علينا إما إصلاح الثنائيات أو مزيفة وقت النظام.
دعنا
ZERO_AR_DATE
مشروعنا عينة لنظام MacOS ونرى ما ستكون النتائج عند تعيين
ZERO_AR_DATE
البيئة
ZERO_AR_DATE
.
export ZERO_AR_DATE=1
الآن ، إذا قمنا بتجميع الملفات والمكتبات القابلة للتنفيذ لدينا (إزالة الماكرو
__DATE__
في المصادر) ،
__DATE__
على:
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
جميع الاختبارية هي نفسها الآن.
.a
نحلل رؤوس الملفات بالملحق
.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
يمكننا أن نرى أن حقل
timestamp
لرأس المكتبة تم ضبطه على الصفر.
وصلنا بسلاسة إلى نهاية الجزء الأول من المقال. يمكن قراءة استمرار المواد هنا .