
* رابط إلى المكتبة والفيديو التجريبي في نهاية المقال. لفهم ما يحدث ومن هم هؤلاء الناس ، أوصي بقراءة المقال السابق .
في المقالة الأخيرة ، تعرفنا على نهج يسمح بإعادة تحميل رمز c ++ الساخن. "الرمز" في هذه الحالة هو الوظائف والبيانات وعملهم المنسق مع بعضهم البعض. لا توجد مشاكل خاصة بالوظائف ، فنحن نعيد توجيه تدفق التنفيذ من الوظيفة القديمة إلى الجديدة ، ويعمل كل شيء. تنشأ المشكلة مع البيانات (المتغيرات الثابتة والعالمية) ، أي مع إستراتيجية تزامنها في الكود القديم والجديد. في أول تطبيق ، كانت هذه الإستراتيجية خرقاء للغاية: ببساطة نقوم بنسخ قيم جميع المتغيرات الثابتة من الكود القديم إلى الكود الجديد ، بحيث يعمل الكود الجديد ، بالإشارة إلى المتغيرات الجديدة ، مع قيم الكود القديم. بالطبع ، هذا غير صحيح ، وسوف نحاول اليوم تصحيح هذا الخطأ عن طريق حل عدد من المشكلات الصغيرة والمثيرة للاهتمام في نفس الوقت.
يحذف المقال التفاصيل المتعلقة بالعمل الميكانيكي ، مثل قراءة الأحرف والانتقال من ملفات elf و mach-o. ينصب التركيز على النقاط الدقيقة التي صادفتها في عملية التنفيذ ، والتي قد تكون مفيدة لشخص يبحث ، مثلي مؤخرًا ، عن إجابات.
جوهر
دعنا نتخيل أن لدينا فئة (أمثلة تركيبية ، من فضلك لا تبحث عن معنى فيها ، فقط الكود مهم):
لا شيء خاص ولكن متغير ثابت. تخيل الآن أننا نريد تغيير طريقة printDescription()
إلى:
void Entity::printDescription() { std::cout << "DESCRIPTION: " << m_description << std::endl; }
ماذا يحدث بعد تحديث الكود؟ بالإضافة إلى أساليب فئة Entity
، يحصل المتغير الثابت m_livingEntitiesCount
أيضًا على المكتبة برمز جديد. لن يحدث شيء سيء إذا قمنا ببساطة بنسخ قيمة هذا المتغير من الكود القديم إلى الكود الجديد ومواصلة استخدام المتغير الجديد ، نسيان المتغير القديم ، لأن جميع الأساليب التي تستخدم هذا المتغير مباشرة في المكتبة مع الكود الجديد.
C ++ مرنة للغاية وغنية. بينما أناقة حل بعض المشاكل في حدود c ++ على رمز الرائحة الكريهة ، أنا أحب هذه اللغة. على سبيل المثال ، تخيل أن مشروعك لا يستخدم rtti. في نفس الوقت ، يجب أن يكون لديك تطبيق من Any
فئة بواجهة آمنة نوعًا إلى حد ما:
class Any { public: template <typename T> explicit Any(T&& value) { ... } template <typename T> bool is() const { ... } template <typename T> T& as() { ... } };
لن ندخل في تفاصيل تنفيذ هذه الفئة. المهم بالنسبة إلينا هو أننا نحتاج للتطبيق من أجل نوع من التعيين غير uint64_t
(كيان وقت التحويل البرمجي) في قيمة المتغير ، على سبيل المثال ، uint64_t
(كيان وقت التشغيل) ، أي أنواع "التعداد". عند استخدام rtti ، تتوفر أشياء مثل type_info
و ، أكثر ملاءمة لنا ، type_index
لنا. ولكن ليس لدينا rtti. في هذه الحالة ، اختراق مشترك إلى حد ما (أو حل أنيق؟) هل هذه الوظيفة:
template <typename T> uint64_t typeId() { static char someVar; return reinterpret_cast<uint64_t>(&someVar); }
عندها سيبدو تطبيق Any
فئة على النحو التالي:
class Any { public: template <typename T> explicit Any(T&& value) : m_typeId(typeId<std::decay<T>::type>())
لكل نوع ، سيتم إنشاء مثيل للوظيفة بدقة 1 مرة ، على التوالي ، سيكون لكل إصدار من هذه الوظيفة المتغير الثابت الخاص بها ، ومن الواضح أن عنوانه فريد من نوعه. ماذا يحدث عند إعادة تحميل الكود باستخدام هذه الوظيفة؟ سيتم إعادة توجيه المكالمات إلى الإصدار القديم من الوظيفة إلى الجديدة. سيكون للمتغير الجديد المتغير الثابت الذي تمت تهيئته بالفعل (قمنا بنسخ متغير القيمة والحراسة). لكننا لسنا مهتمين بالمعنى ، نحن نستخدم العنوان فقط. وسيكون عنوان المتغير الجديد مختلفًا. وبالتالي ، أصبحت البيانات غير متناسقة: في الحالات التي تم إنشاؤها بالفعل من Any
فئة ، سيتم تخزين عنوان المتغير الثابت القديم ، وستقارن الطريقة is()
مع عنوان الجديد ، و "هذا Any
يكون هو نفسه Any
" ©.
خطة
لحل هذه المشكلة ، تحتاج إلى شيء أكثر ذكاء من مجرد النسخ. بعد قضاء بعض الأمسيات على Google ، وقراءة الوثائق ، وكود المصدر ونظام واجهة برمجة التطبيقات ، تم بناء الخطة التالية في رأسي:
- بعد بناء الكود الجديد ، نمر بالانتقال .
- من خلال عمليات الترحيل هذه ، نحصل على جميع الأماكن في الشفرة التي تستخدم متغيرات ثابتة (وأحيانًا عالمية).
- بدلاً من عناوين الإصدارات الجديدة من المتغيرات ، نستبدل عناوين الإصدارات القديمة في مكان النقل.
في هذه الحالة ، لن يكون هناك روابط لبيانات جديدة ، وسيستمر التطبيق بأكمله في العمل مع الإصدارات القديمة من المتغيرات حتى العنوان. يجب أن تعمل. هذا لا يمكن أن تفشل في العمل.
الترحيل
عندما يقوم المترجم بإنشاء رمز الجهاز ، فإنه يدرج عدة بايت كافية لكتابة العنوان الحقيقي للمتغير أو الوظيفة إلى هذا المكان في كل مكان حيث يتم استدعاء الوظيفة أو يتم تحميل عنوان المتغير ، وأيضًا تقوم بإنشاء تغيير موقع. لا يستطيع تسجيل العنوان الحقيقي على الفور ، لأنه في هذه المرحلة لا يعرف هذا العنوان. يمكن أن تكون الوظائف والمتغيرات بعد الارتباط في أقسام مختلفة ، في أماكن مختلفة من الأقسام ، في الأقسام النهائية يمكن تحميلها على عناوين مختلفة في وقت التشغيل.
النقل يحتوي على معلومات:
- في أي عنوان تحتاج إلى كتابة عنوان الوظيفة أو المتغير
- عنوان أي وظيفة أو متغير في الكتابة
- الصيغة التي يجب أن يحسب بها هذا العنوان
- عدد البايتات المحجوزة لهذا العنوان
في أنظمة التشغيل المختلفة ، يتم تمثيل عمليات الترحيل بشكل مختلف ، ولكن في النهاية تعمل جميعها وفقًا لنفس المبدأ. على سبيل المثال ، في elf (Linux) ، توجد عمليات الترحيل في أقسام .rela
الخاصة (في الإصدار 32 بت هذا هو .rel
) ، والتي تشير إلى القسم بالعنوان الذي يحتاج إلى إصلاح (على سبيل المثال ، .rela.text
- القسم الذي توجد فيه عمليات الترحيل ، يتم تطبيقه على قسم النص.) ، ويقوم كل إدخال بتخزين معلومات حول الرمز الذي تريد إدراج عنوانه في موقع النقل. في mach-o (macOS) ، يكون العكس هو الحال ؛ لا يوجد قسم منفصل لعمليات الترحيل ؛ وبدلاً من ذلك ، يحتوي كل قسم على مؤشر لجدول عمليات الترحيل التي ينبغي تطبيقها على هذا القسم ، ولكل سجل في هذا الجدول إشارة إلى رمز علائقي.
على سبيل المثال ، لمثل هذا الرمز (مع الخيار -fPIC
):
int globalVariable = 10; int veryUsefulFunction() { static int functionLocalVariable = 0; functionLocalVariable++; return globalVariable + functionLocalVariable; }
سيقوم المترجم بإنشاء مثل هذا القسم مع الانتقال إلى نظام Linux:
Relocation section '.rela.text' at offset 0x1a0 contains 4 entries: Offset Info Type Symbol's Value Symbol's Name + Addend 0000000000000007 0000000600000009 R_X86_64_GOTPCREL 0000000000000000 globalVariable - 4 000000000000000d 0000000400000002 R_X86_64_PC32 0000000000000000 .bss - 4 0000000000000016 0000000400000002 R_X86_64_PC32 0000000000000000 .bss - 4 000000000000001e 0000000400000002 R_X86_64_PC32 0000000000000000 .bss - 4
ومثل هذا الجدول نقل على macOS:
RELOCATION RECORDS FOR [__text]: 000000000000001b X86_64_RELOC_SIGNED __ZZ18veryUsefulFunctionvE21functionLocalVariable 0000000000000015 X86_64_RELOC_SIGNED _globalVariable 000000000000000f X86_64_RELOC_SIGNED __ZZ18veryUsefulFunctionvE21functionLocalVariable 0000000000000006 X86_64_RELOC_SIGNED __ZZ18veryUsefulFunctionvE21functionLocalVariable
وهنا هي وظيفة veryUsefulFunction()
على نظام Linux):
0000000000000000 <_Z18veryUsefulFunctionv>: 0: 55 push rbp 1: 48 89 e5 mov rbp,rsp 4: 48 8b 05 00 00 00 00 mov rax,QWORD PTR [rip+0x0] b: 8b 0d 00 00 00 00 mov ecx,DWORD PTR [rip+0x0] 11: 83 c1 01 add ecx,0x1 14: 89 0d 00 00 00 00 mov DWORD PTR [rip+0x0],ecx 1a: 8b 08 mov ecx,DWORD PTR [rax] 1c: 03 0d 00 00 00 00 add ecx,DWORD PTR [rip+0x0] 22: 89 c8 mov eax,ecx 24: 5d pop rbp 25: c3 ret
وهكذا بعد ربط الكائن بالمكتبة الديناميكية:
00000000000010e0 <_Z18veryUsefulFunctionv>: 10e0: 55 push rbp 10e1: 48 89 e5 mov rbp,rsp 10e4: 48 8b 05 05 21 00 00 mov rax,QWORD PTR [rip+0x2105] 10eb: 8b 0d 13 2f 00 00 mov ecx,DWORD PTR [rip+0x2f13] 10f1: 83 c1 01 add ecx,0x1 10f4: 89 0d 0a 2f 00 00 mov DWORD PTR [rip+0x2f0a],ecx 10fa: 8b 08 mov ecx,DWORD PTR [rax] 10fc: 03 0d 02 2f 00 00 add ecx,DWORD PTR [rip+0x2f02] 1102: 89 c8 mov eax,ecx 1104: 5d pop rbp 1105: c3 ret
هناك 4 أماكن فيها 4 بايت محجوزة لعنوان المتغيرات الحقيقية.
في أنظمة مختلفة ، تكون مجموعة عمليات الترحيل المحتملة هي الخاصة بك. على نظام Linux على x86-64 ، ما يصل إلى 40 نوعًا من عمليات الترحيل . لا يوجد سوى 9 منهم على macOS على x86-64. يمكن تقسيم جميع أنواع الترحيل بشكل مشروط إلى مجموعتين:
- عمليات نقل وقت الارتباط - عمليات الترحيل المستخدمة في عملية ربط ملفات الكائنات بملف قابل للتنفيذ أو مكتبة ديناميكية
- عمليات الترحيل في وقت التحميل - عمليات الترحيل التي يتم تطبيقها وقت تحميل المكتبة الديناميكية في ذاكرة العملية
المجموعة الثانية تشمل نقل الوظائف والمتغيرات المصدرة. عند تحميل مكتبة ديناميكية في ذاكرة العملية ، وبالنسبة لجميع عمليات النقل الديناميكية (بما في ذلك نقل المتغيرات العامة) ، يبحث الرابط عن تعريف الرموز في جميع المكتبات المحملة بالفعل ، بما في ذلك في البرنامج نفسه ، ويستخدم عنوان أول رمز مناسب لإعادة التوطين. وبالتالي ، لا يجب القيام بأي شيء مع عمليات النقل هذه ؛ حيث سيجد الرابط المتغير من تطبيقنا نفسه ، حيث إنه سوف يندرج في قائمة المكتبات والبرامج المحملة مسبقًا ، وسيحل محل عنوانه في الكود الجديد ، متجاهلاً الإصدار الجديد من هذا المتغير.
هناك نقطة خفية مرتبطة بـ macOS ورابطها الديناميكي. ينفذ نظام التشغيل MacOS ما يسمى بآلية مساحة الاسم ثنائية المستوى. إذا كانت وقحة ، فعند تحميل مكتبة ديناميكية ، فسيقوم الرابط أولاً بالبحث عن الأحرف في هذه المكتبة ، وإذا لم يعثر عليها ، فسيبحث في الآخرين. يتم ذلك لأغراض الأداء ، بحيث يتم حل عمليات الترحيل بسرعة ، وهو أمر منطقي بشكل عام. لكن هذا يكسر تدفقنا فيما يتعلق بالمتغيرات العالمية. لحسن الحظ ، يوجد في ld في نظام التشغيل -flat_namespace
علامة خاصة - -flat_namespace
، وإذا كنت تقوم بإنشاء مكتبة باستخدام هذه العلامة ، فستكون خوارزمية البحث عن الأحرف مطابقة لتلك الموجودة في نظام Linux.
تتضمن المجموعة الأولى نقل المتغيرات الثابتة - بالضبط ما نحتاج إليه. المشكلة الوحيدة هي أن عمليات الترحيل هذه ليست موجودة في المكتبة المترجمة ، حيث يتم حلها بالفعل بواسطة برنامج linker. لذلك ، سوف نقرأها من ملفات الكائنات التي تم تجميع المكتبة منها.
الأنواع المحتملة لعمليات الترحيل محدودة أيضًا بما إذا كانت الشفرة المُجمَّعة تعتمد على الموضع أم لا. نظرًا لأننا نجمع الشفرة الخاصة بنا في وضع الموافقة المسبقة عن علم (رمز مستقل عن الموضع) ، فإن عمليات الترحيل تستخدم نسبيًا. إجمالي عمليات الترحيل التي تهمنا هي:
- عمليات الترحيل من قسم
.rela.text
على نظام Linux والانتقال المشار إليه في قسم __text
نظام __text
، - الذي يستخدم أحرفًا من أقسام
.bss
و .bss
على Linux و __data
و __bss
و __common
على __common
و - عمليات الترحيل من النوع
R_X86_64_PC32
و R_X86_64_PC64
على Linux و X86_64_RELOC_SIGNED
و X86_64_RELOC_SIGNED_1
و X86_64_RELOC_SIGNED_2
و X86_64_RELOC_SIGNED_4
على X86_64_RELOC_SIGNED_4
النقطة الدقيقة المرتبطة بقسم __common
. يحتوي Linux أيضًا على قسم *COM*
مماثل. قد تقع المتغيرات العامة في هذا القسم . ولكن ، بينما قمت باختبار وتجميع مجموعة من مقتطفات التعليمات البرمجية ، على نظام Linux ، كانت عمليات نقل الأحرف من أقسام *COM*
ديناميكية دائمًا ، مثل المتغيرات العامة العادية. في الوقت نفسه ، في نظام التشغيل MacOS ، يتم نقل هذه الأحرف أحيانًا أثناء الربط إذا كانت الوظيفة والحرف في نفس الملف. لذلك ، من المنطقي التفكير في هذا القسم على نظام التشغيل MacOS عند قراءة الحروف والانتقال.
حسنًا ، لدينا الآن مجموعة من جميع عمليات الترحيل التي نحتاج إليها ، ماذا نفعل معهم؟ المنطق هنا بسيط. عندما يقوم الرابط بربط المكتبة ، فإنه يكتب عنوان الرمز المحسوب بواسطة صيغة معينة في عنوان الترحيل. بالنسبة لعمليات الترحيل الخاصة بنا على كلا النظامين الأساسيين ، تحتوي هذه الصيغة على عنوان الرمز كمصطلح. وبالتالي ، فإن العنوان المحسوب الذي تم تسجيله بالفعل في مجموعة الوظائف لديه النموذج:
resultAddr = newVarAddr + addend - relocAddr
في الوقت نفسه ، نعرف عناوين كلا الإصدارين من المتغيرات - القديمة ، التي تعيش بالفعل في التطبيق ، والجديدة. يبقى لنا تغييره وفقًا للصيغة:
resultAddr = resultAddr - newVarAddr + oldVarAddr
واكتبه على عنوان الترحيل. بعد ذلك ، ستستخدم جميع الوظائف في الكود الجديد الإصدارات الحالية من المتغيرات ، وسوف تكمن المتغيرات الجديدة ببساطة ولا تفعل شيئًا. ما تحتاجه! ولكن هناك نقطة واحدة خفية.
تنزيل المكتبة برمز جديد
عندما يقوم النظام بتحميل مكتبة ديناميكية في ذاكرة العملية ، يكون وضعها مجانًا في أي مكان في مساحة العنوان الافتراضية. على Ubuntu 18.04 ، يتم تحميل التطبيق على 0x00400000
، 0x00400000
الديناميكية مباشرة بعد ld-2.27.so
على العناوين في منطقة 0x7fd3829bd000
. المسافة بين عناوين تنزيل البرنامج والمكتبة أكبر بكثير من الرقم الذي يتناسب مع عدد صحيح 32 بت موقعة. وفي عمليات نقل وقت الارتباط ، يتم حجز 4 بايت فقط لعناوين الأحرف المستهدفة.
بعد تدخين وثائق المترجمين والرابطات ، قررت تجربة -mcmodel=large
خيار -mcmodel=large
. يفرض المترجم إنشاء رمز دون أي افتراضات حول المسافة بين الأحرف ، وبالتالي يفترض أن تكون جميع العناوين 64 بت. ولكن هذا الخيار ليس صديقًا للموافقة المسبقة عن علم ، كما لو كان -mcmodel=large
لا يمكن استخدامه مع -fPIC
، على الأقل في نظام التشغيل -fPIC
. ما زلت لا أفهم ما هي المشكلة ، ربما على نظام التشغيل MacOS لا توجد عمليات نقل مناسبة لهذه الحالة.
في المكتبة تحت النوافذ ، يتم حل هذه المشكلة على النحو التالي. تخصيص تخصيص قطعة من الذاكرة الظاهرية بالقرب من موقع تنزيل التطبيق ، وهو ما يكفي لاستيعاب الأقسام الضرورية من المكتبة. بعد ذلك ، يتم تحميل المقاطع فيه بأيدي ، ويتم ضبط الحقوق اللازمة على صفحات الذاكرة مع الأقسام المقابلة ، ويتم فك كل عمليات الترحيل بالأيدي ، ويتم تصحيح كل شيء آخر. أنا كسول. لم أرغب حقًا في القيام بكل هذا العمل مع عمليات نقل وقت التحميل ، خاصة على نظام Linux. ولماذا يعرف رابط ديناميكي بالفعل كيف يفعل؟ بعد كل شيء ، فإن الأشخاص الذين كتبوه يعرفون أكثر مما أعرف.
لحسن الحظ ، وجدت الوثائق الخيارات اللازمة للإشارة إلى مكان تنزيل مكتبتنا الديناميكية:
- Apple ld:
-image_base 0xADDRESS
- LLVM lld:
--image-base=0xADDRESS
- GNU ld:
-Ttext-segment=0xADDRESS
يجب تمرير هذه الخيارات إلى الرابط في وقت ربط المكتبة الديناميكية. هناك 2 الصعوبات.
الأول يتعلق بجنو. لكي تعمل هذه الخيارات ، تحتاج إلى:
- في وقت تحميل المكتبة ، كانت المنطقة التي نريد تحميلها فيها مجانية
- يجب أن يكون العنوان المحدد في الخيار مضاعفًا لحجم الصفحة (على x86-64 Linux و macOS هو
0x1000
) - على نظام Linux على الأقل ، يجب أن يكون العنوان المحدد في الخيار مضاعفًا لمحاذاة شريحة
PT_LOAD
بمعنى ، إذا قام الموصل بتعيين المحاذاة إلى 0x10000000
، فلا يمكن تحميل هذه المكتبة على العنوان 0x10001000
، على الرغم من أن العنوان محاذي لحجم الصفحة. إذا لم يتم استيفاء أحد هذه الشروط ، فستقوم المكتبة بتحميل "كالمعتاد". لديّ GNU ld 2.30 على PT_LOAD
، وعلى عكس LLVM lld ، فإنه يقوم افتراضيًا بتعيين محاذاة قطاع 0x20000
إلى 0x20000
، وهو خارج الصورة تمامًا. للتغلب على هذا ، بالإضافة إلى الخيار -Ttext-segment=...
، حدد -z max-page-size=0x1000
. قضيت يومًا حتى أدركت سبب عدم تحميل المكتبة للمكان الذي أحتاج إليه.
الصعوبة الثانية - يجب أن يكون عنوان التنزيل معروفًا في مرحلة الربط بالمكتبة. ليس من الصعب تنظيمه. في نظام Linux ، يكفي تحليل pseudo-file /proc/<pid>/maps
، والعثور على أقرب قطعة غير مشغولة بالبرنامج ، والتي ستلائمها المكتبة ، وتستخدم عنوان بداية هذه القطعة عند الارتباط. يمكن تقدير حجم مكتبة المستقبل تقريبًا عن طريق النظر إلى أحجام ملفات الكائنات ، أو عن طريق تحليلها وحساب أحجام جميع الأقسام. في النهاية ، لا نحتاج إلى رقم محدد ، ولكننا نحتاج إلى حجم تقريبي بهامش.
لا يحتوي نظام MacOS على /proc/*
؛ بدلاً من ذلك ، يُقترح استخدام الأداة المساعدة vmmap
. يحتوي الإخراج من vmmap -interleaved <pid>
على نفس المعلومات مثل proc/<pid>/maps
. ولكن هنا تنشأ صعوبة أخرى. إذا قام أحد التطبيقات بإنشاء عملية تابعة تنفذ هذا الأمر ، وتم تحديد معرف العملية الحالية كـ <pid>
، فسيتم تعليق البرنامج. كما أفهمها ، تتوقف vmmap
عن عملية قراءة تعيينات الذاكرة الخاصة بها ، وعلى ما يبدو ، إذا كانت هذه هي عملية الاستدعاء ، vmmap
خطأ ما. في هذه الحالة ، تحتاج إلى تحديد علامة إضافية -forkCorpse
بحيث تنشئ vmmap
عملية تابعة فارغة من عمليتنا ، وإزالة التعيين منها وقتلها ، وبالتالي عدم مقاطعة البرنامج.
هذا كل ما نحتاج إلى معرفته.
وضع كل ذلك معا
مع هذه التعديلات ، تبدو خوارزمية إعادة تحميل الرمز النهائي كما يلي:
- ترجمة التعليمات البرمجية الجديدة إلى ملفات الكائنات
- بالنسبة إلى ملفات الكائنات ، نقدر حجم المكتبة المستقبلية
- قراءة ملفات نقل الكائن
- نحن نبحث عن قطعة مجانية من الذاكرة الافتراضية بجانب التطبيق
- نحن نبني مكتبة ديناميكية مع الخيارات اللازمة ،
dlopen
عبر dlopen
- رمز التصحيح وفقًا لعمليات نقل وقت الارتباط
- وظيفة التصحيح
- انسخ المتغيرات الثابتة التي لم تشارك في الخطوة 6
تندرج متغيرات الحماية للمتغيرات الثابتة فقط في الخطوة 8 ، بحيث يمكن نسخها بأمان (وبالتالي الحفاظ على "تهيئة" المتغيرات الثابتة نفسها).
الخاتمة
نظرًا لأن هذا يعد أداة تطوير حصريًا ، وليس مخصصًا لأي إنتاج ، فإن أسوأ شيء يمكن أن يحدث إذا لم يتم احتواء المكتبة التالية التي تحتوي على رمز جديد في الذاكرة أو تم تحميلها عن طريق الخطأ في عنوان مختلف هو إعادة تشغيل التطبيق الذي تم تصحيحه. عند إجراء الاختبارات ، يتم تحميل 31 مكتبة برمز محدّث في الذاكرة بدورها.
للتأكد من اكتمالها ، هناك ثلاث قطع أثقل في التنفيذ:
- الآن يتم تحميل المكتبة التي تحتوي على الكود الجديد في الذاكرة المجاورة للبرنامج ، على الرغم من أن الكود من مكتبة ديناميكية أخرى تم تحميلها بعيدًا يمكنه الدخول إليها. للإصلاح ، تحتاج إلى تتبع ملكية وحدات الترجمة إلى مكتبة وبرنامج واحد أو آخر ، وتقسيم المكتبة بالرمز الجديد إذا لزم الأمر.
- إعادة تحميل التعليمات البرمجية في تطبيق متعدد الخيوط لا تزال غير موثوقة (مع اليقين يمكنك فقط إعادة تحميل التعليمات البرمجية التي يتم تشغيلها في نفس مؤشر الترابط مثل مكتبة runloop). للتثبيت ، من الضروري نقل جزء من التطبيق إلى برنامج منفصل ، ويجب أن يوقف هذا البرنامج ، قبل الترقيع ، العملية بكل مؤشرات الترابط والتصحيح وإعادته إلى العمل. لا أعرف كيف أفعل هذا بدون برنامج خارجي.
- الوقاية من تعطل التطبيق العرضي بعد تحديث الكود. بعد إصلاح الكود ، يمكنك إلغاء تحديد المؤشر غير الصحيح في الكود الجديد عن طريق الخطأ ، وبعد ذلك سيتعين عليك إعادة تشغيل التطبيق. لا بأس ، ولكن لا يزال. يبدو وكأنه السحر الأسود ، ما زلت في التفكير.
ولكن بالفعل بدأ التنفيذ الحالي يفيدني شخصياً ، وهو ما يكفي للاستخدام في وظيفتي الرئيسية. يستغرق التعود قليلاً ، لكن الرحلة عادية.
إذا وصلت إلى هذه النقاط الثلاث ووجدت في تنفيذها كمية كافية من الأشياء المثيرة للاهتمام ، سأشاركها بالتأكيد.
عرض
نظرًا لأن التطبيق يسمح لك بإضافة وحدات بث جديدة أثناء الطيران ، فقد قررت تسجيل مقطع فيديو قصير أكتب فيه لعبة بسيطة بذيئة من نقطة الصفر حول سفينة فضائية تحرّق مساحات الكون وتطلق الكويكبات المربعة. حاولت ألا أكتب بأسلوب "الكل في ملف واحد" ، لكن ، إن أمكن ، رتب كل شيء على الرفوف ، مما أدى إلى توليد الكثير من الملفات الصغيرة (لذلك كان هناك الكثير من الكتابة). بالطبع ، يتم استخدام إطار العمل للرسم والمدخلات والنوافذ وأشياء أخرى ، ولكن رمز اللعبة نفسه تمت كتابته من نقطة الصفر.
الميزة الرئيسية - لقد قمت بتشغيل التطبيق 3 مرات فقط: في البداية ، عندما كان لديه مشهد فارغ فقط ، ومرتين بعد السقوط بسبب إهمالي. اللعبة بأكملها سكب تدريجيا في عملية كتابة التعليمات البرمجية. الوقت الحقيقي - حوالي 40 دقيقة. بشكل عام ، اهلا وسهلا بكم.
كما هو الحال دائمًا ، سأكون سعيدًا بأي نقد ، شكرًا!
رابط للتنفيذ