DllMain وقفل المحمِّل ── السبب الحقيقي لعبارة «لا تفعل شيئاً في تهيئة الـDLL»

· · Windows, DLL, تطوير Windows, C++, استكشاف الأخطاء, تعدّد مؤشّرات الترابط, Win32 API

«التطبيق يتجمّد عند البدء، لكن فقط في بيئة معيّنة.» «عندما نحمِّل DLL الخاصّ بنا، LoadLibrary أحياناً لا يعود أبداً.» «يجمد فقط عند توقيت بدء خدمة.» ── تابع تحقيقات كهذه بما يكفي، وفي أكثر الأحيان تصل إلى المكان نفسه. شيفرة تهيئة الـDLL ── أي DllMain.

وثائق مايكروسوفت تحذِّر بشأن DllMain بنبرة قويّة على نحو غير معتاد. لا تستدعِ LoadLibrary. لا تتزامن مع مؤشّرات ترابط أخرى. لا تستدعِ دوال User أو Shell أو COM. DllMain المثاليّة كعب فارغ ── لماذا اللغة بهذه القوّة؟ السبب يتركّز في آليّة داخليّة واحدة، قفل المحمِّل. موجَّه إلى المطوِّرين الذين يكتبون DLLs وإضافات وأغلفة C++/CLI على Windows، يشرح هذا المقال، من المصادر الأوّليّة، كيف يعمل قفل المحمِّل، والبنية التي تجعل الجمود يصمد، والتصميم الآمن وإجراء التحقيق.

1. الخلاصة أوّلاً

  • DllMain تُستدعى وهي تمسك قفل المحمِّل، قفلاً مشتركاً يوجد واحد منه بالضبط لكلّ عمليّة. لذا فإنّ استدعاء، من DllMain، عملٍ يحاول أخذ قفل المحمِّل (مباشرةً أو بصورة غير مباشرة) يخلق إمكان جمود، أو انهيار من لمس DLL لم يُهيَّأ بعد.1
  • استدعاء LoadLibrary / FreeLibrary محظور. يخلق اعتماداً دائريّاً في ترتيب التحميل ويمكن أن يجعل شيفرة التهيئة تعمل ضدّ DLL لم تعمل تهيئته بعد.2
  • التزامن مع مؤشّرات ترابط أخرى محظور أيضاً. إشعارات DLL مُسلسَلة، لذا انتظار بدء مؤشّر ترابط أو خروجه داخل DllMain يترك ذلك المؤشّر نفسه متوقّفاً ينتظر قفل المحمِّل، وتجمد.23
  • ما يمكنك استدعاؤه بأمان، عمليّاً، مجموعة فرعيّة فقط من Kernel32.dll. والوثائق الرسميّة تنصّ بصراحة أنّ «قائمة كاملة بالدوال الآمنة غير موجودة». دوال User وShell وCOM تحمِّل مكوِّنات أخرى وتسبّب انتهاكات وصول.2
  • في DLL مربوط مع CRT، تنطبق القيود نفسها على بنّاءات الكائنات العامّة وهاداتها. تعمل كجزء فعليّ من DllMain.2
  • التصميم الصحيح هو «أجِّل». افعل ما تستطيع من التهيئة وقت الترجمة (ساكناً)؛ أجِّل ما لا تستطيع إلى أوّل استخدام. هذه أفضل ممارسة رسميّة.1
  • DLLs C++/CLI المختلطة خطرة بشكل خاصّ. لتفادي تشغيل MSIL تحت قفل المحمِّل، يجب ترجمة DllMain وشجرة استدعائها أصليّة.4

2. متى وكيف تُستدعى DllMain

DllMain نقطة الدخول التي يستدعيها محمِّل نظام التشغيل عندما يدخل DLL عمليّة أو مؤشّر ترابط أو يغادرهما. ثمّة أربعة إشعارات.

الإشعار التوقيت
DLL_PROCESS_ATTACH عندما يُحمَّل الـDLL في العمليّة
DLL_THREAD_ATTACH عندما يُبدأ مؤشّر ترابط جديد في العمليّة
DLL_THREAD_DETACH عندما يخرج مؤشّر ترابط بشكل طبيعيّ
DLL_PROCESS_DETACH عندما يُلغى تحميل الـDLL، أو عندما تخرج العمليّة

حقيقتان يسهل تفويتهما. الأولى، في كلّ مرّة يُنشأ فيها مؤشّر ترابط واحد، تُستدعى DllMain لكلّ DLL محمَّل أصلاً بـDLL_THREAD_ATTACH. بمعنى آخر DllMain ليست «شيئاً يعمل مرّة عندما يُحمَّل DLL الخاصّ بي»؛ إنّها شيفرة تظلّ تُستدعى لنشاط مؤشّرات ترابط العمليّة. إن لم تحتج ذلك، يمكنك إيقافه باستدعاء DisableThreadLibraryCalls داخل DLL_PROCESS_ATTACH (لا تستدعِه من DLL مربوط مع CRT الساكن).5

الثانية، في DLL مربوط مع CRT (بيئة تشغيل C/C++)، تعمل بنّاءات وهادات كائنات C++ العامّة والساكنة، عبر نقطة دخول CRT، كجزء من DllMain.2 حتّى إن ظننت «DllMain لدينا فارغة، لذا نحن بأمان»، فكائن عامّ بتهيئة معقّدة هو نفسه تشغيل ذلك العمل في DllMain.

التوقيتات الأربعة التي تُستدعى فيها DllMainيعمل DLL_PROCESS_ATTACH عند تحميل الـDLL؛ ويعمل DLL_THREAD_ATTACH وDETACH على كلّ DLL محمَّل أصلاً عند كلّ بدء مؤشّر ترابط وخروجه في العمليّة؛ ويعمل DLL_PROCESS_DETACH عند إلغاء التحميل أو خروج العمليّة؛ وبنّاءات الكائنات الساكنة تعمل أيضاً داخل هذا عبر CRTتحميل الـDLLDLL_PROCESS_ATTACHDLL_THREAD_ATTACH(عند كلّ بدء مؤشّر)DLL_THREAD_DETACH(عند كلّ خروج مؤشّر)DLL_PROCESS_DETACH(عند إلغاء التحميل أو الخروج)بناء الكائنات الساكنة يعمل هنا أيضاً

الشكل 1: تُستدعى DllMain ليس عند التحميل فحسب بل عند كلّ بدء مؤشّر ترابط وخروجه، وتهيئة الكائنات الساكنة تعمل أيضاً كجزء من ذلك.

3. قفل المحمِّل ── قفل واحد يسلسل كلّ إشعار

لماذا قيود DllMain وحدها بهذه الشدّة؟ الجواب في بنية المحمِّل.

لإبقاء سلسلة عمليّات ── تحميل الـDLL وإلغاء تحميله والإشعارات المختلفة ── متّسقة، يسلسل محمِّل نظام التشغيل العمل بقفل محمِّل واحد لكلّ عمليّة. والنقطة المهمّة أنّ DllMain تُستدعى وهذا القفل ممسوك.1 طالما أنت داخل DllMain، كلّ تحميل DLL آخر في تلك العمليّة، وكلّ إشعار بدء مؤشّر ترابط، ينتظر تحرير هذا القفل.

من تلك البنية، تتبع أسباب المحظورات واحداً تلو الآخر.

  • يجب ألا تستدعي LoadLibrary لأنّها تخلق إعادة دخول لقفل المحمِّل، أو اعتماداً دائريّاً في ترتيب التحميل. يمكن أيضاً أن تنتج استدعاء دالّة على DLL لم تنتهِ تهيئته بعد.2
  • التزامن مع مؤشّرات ترابط أخرى خطر لأنّ المؤشّر الذي تنتظره له لحظات يحتاج فيها قفل المحمِّل (إشعارات عند البدء والخروج، واستدعاءات عائلة GetModuleHandle، وما شابه). أنت تمسك قفل المحمِّل وتنتظر الطرف الآخر؛ الطرف الآخر ينتظر قفل المحمِّل ── القلب الكلاسيكيّ لترتيب الأقفال.6
  • دوال User وShell وCOM خطرة لأنّها تحمِّل مكوِّنات نظام أخرى داخليّاً. تلمس مكوِّناً قبل تهيئته، أو بعد تفكيكه، فتحصل على انتهاك وصول.2
لماذا انتظار مؤشّر ترابط داخل DllMain يجمدDllMain، ممسكة قفل المحمِّل، تنتظر خروج مؤشّر عامل، لكنّ العامل الذي يحاول الخروج ينتظر تحرير قفل المحمِّل حتّى يستقبل DLL_THREAD_DETACH، فينتظر أحدهما الآخر ويجمدانالعاملDllMainالمحمِّل(يمسك القفل)العاملDllMainالمحمِّل(يمسك القفل)إشعار الخروج يحتاج القفلDllMain تمسك القفل، والعامل ينتظرDLL_PROCESS_DETACHاطلب الخروج وانتظرأنهِ العمل، ثمّ اخرج

الشكل 2: «DllMain تنتظر خروج مؤشّر ترابط» جمود بنيويّ، لأنّ خروج مؤشّر الترابط نفسه يحتاج قفل المحمِّل.

النقطة أنّ هذا ليس من نوع ما «يحدث إن لم يحالفك الحظّ»؛ مضمون بنيويّاً أن يصمد. الوثائق تقول لك أن تعامل قفل المحمِّل بوصفه قمة هرم الأقفال الذي يعرِّفه التطبيق (الذي يُؤخذ أوّلاً). داخل DllMain أنت تمسك أصلاً ذلك القفل الأعلى، لذا أيّ فعل بالانطلاق من هناك لانتظار شيء آخر خطر ── تلك طريقة مفيدة لتذكّره.6

قلب ترتيب الأقفال بين قفل المحمِّل وقفل خاصّDllMain، ممسكة قفل المحمِّل، تذهب لأخذ قفل خاصّ، بينما عامل، ممسك ذلك القفل الخاصّ، يذهب لأخذ قفل المحمِّل لـ GetModuleHandle أو ما شابه، فينقلب ترتيب الأخذ ويجمدانDllMain: تمسك قفل المحمِّلتذهب لأخذ القفل الخاصّ Gعامل: يمسك القفل الخاصّ Gيذهب لأخذ قفل المحمِّلجمود من ترتيب أخذ مقلوبGetModuleHandle وما شابه تحتاجه داخليّاً

الشكل 3: حتّى واجهة بريئة مثل GetModuleHandle تتطلّب قفل المحمِّل داخليّاً، لذا يمكن لقلب ترتيب مع قفل خاصّ أن يصمد.

كذلك، استدعاء CreateThread من داخل DllMain نفسها غير مستحسَن. مؤشّر الترابط المُنشأ يحتاج قفل المحمِّل لمعالجة إشعار DLL_THREAD_ATTACH، لذا لا يستطيع بدء العمل حتّى تعود DllMain الجارية حالياً وتحرِّر القفل. لذلك انتظار بدء ذلك المؤشّر أو انتهائه داخل DllMain جمود فوريّ. ثمّة مشكلة عمر أيضاً ── إذا، بعد عودة DllMain، أُلغي تحميل الـDLL بينما مؤشّر ترابط لم يبدأ العمل بعد ما زال متروكاً، فإنّ عنوان بدء المؤشّر ما زال يشير إلى شيفرة محرَّرة أصلاً وتنهار.3

4. لغمَان يطأهما مطوِّرو C++ بسهولة

اللغم 1: التهيئة الديناميكيّة للكائنات العامّة. كما قال الفصل 2، بنّاءات الكائنات الساكنة تعمل تحت قيود DllMain. قراءة ملفّ إعدادات، وإقامة منشأة تسجيل، وتهيئة COM، وبدء مؤشّر ترابط ── لحظة وضع كائن عامّ في DLL بنّاؤه يفعل ذلك النوع من العمل، تكون تنفِّذ «أشياء يجب ألا تفعلها في DllMain». التهيئة الثابتة التي تُحسَم وقت الترجمة (أيّ شيء يمكنك جعله constexpr) آمنة؛ التهيئة التي تتضمّن استدعاء دالّة ينبغي تأجيلها.

المسار الذي تصبح فيه تهيئة كائن عامّ لغماًيُؤخذ قفل المحمِّل عند تحميل الـDLL، وبنّاءات الكائنات العامّة تعمل عبر نقطة دخول CRT، لذا LoadLibrary وتزامن مؤشّر الترابط وتهيئة COM داخل تلك البنّاءات تنفيذات لمحظورات DllMainتحميل الـDLL(أخذ قفل المحمِّل)نقطة دخول CRTبنّاء كائن عامّعمل مكافئ لـ LoadLibraryبدء مؤشّر وانتظار انتهائهاستخدام COM أو User32كلّ هذه تقع تحت محظورات DllMain

الشكل 4: حتّى «DllMain فارغة، لذا نحن بأمان» يحيي الخطر نفسه لحظة وجود كائن عامّ بتهيئة معقّدة.

اللغم 2: C++/CLI (التجميعات المختلطة). في تكوين يغلف DLL أصليّاً بـC++/CLI (الشكل المشمول في مقال الغلاف)، ثمّة خطر تشغيل MSIL (شيفرة مُدارة) تحت قفل المحمِّل. تشغيل MSIL يمكن أن يطلق تهيئة CLR أو تحميل تجميعة أخرى. يُصدر المترجم التحذير C4747 على شيفرة تنفِّذ فيها DllMain الـMSIL مباشرةً، لكن لا يستطيع كشف التنفيذ غير المباشر عبر دالّة في وحدة أخرى. ترجم DllMain والدوال المستدعاة منها أصليّة بـ#pragma unmanaged، أو استخدم تكويناً ليست فيه DllMain أصلاً.4

ما إذا كان تنفيذ MSIL تحت قفل المحمِّل قابلاً للكشفالشيفرة التي تنفِّذ فيها DllMain الـMSIL مباشرةً يمكن للمترجم كشفها بالتحذير C4747، أمّا التنفيذ غير المباشر عبر دالّة في وحدة أخرى فلا، لذا عليك منعه بمراجعة شجرة الاستدعاء والإصرار على ترجمة أصليّةاستدعاءات من DllMainنفِّذ MSIL مباشرةًنفِّذ عبر وحدة أخرىقابل للكشف بالتحذير C4747المترجم لا يستطيع كشفهامنع بالمراجعة وpragma unmanaged

الشكل 5: C4747 يحميك فقط ضدّ التنفيذ المباشر. المسارات غير المباشرة لا تُلتقَط إلا بالمراجعة.

5. التصميم الصحيح ── اجعل «أجِّل» السياسة الافتراضيّة

توصية أفضل ممارسة رسميّة واضحة.1

  1. أنهِ ما تستطيع من التهيئة وقت الترجمة (ساكناً). اسأل أوّلاً ما إذا كان يمكن استبدال تهيئة ديناميكيّة بساكنة.
  2. أجِّل الباقي إلى أوّل استخدام. طالما يحدث أوّل استخدام من واجهة عاديّة تُستدعى بعد انتهاء تحميل الـDLL، تعمل التهيئة خارج قفل المحمِّل ويمكنك استخدام شبه كامل واجهة Windows بأمان. للاستبعاد عند أوّل وصول يمكنك استخدام INIT_ONCE (تهيئة لمرّة واحدة) أو سحر C++ الساكن (ساكنات محلّيّة الدالّة). التأجيل ليس دواءً لكلّ داء: إن جُعل ذلك الوصول الأوّل نفسه من DllMain أو مُهيِّئ ساكن، ما زال المُهيِّئ يعمل تحت قفل المحمِّل وتعود تحت القيود نفسها.
  3. استثنِ فقط الإخفاقات التي يجب كشفها مبكّراً. قد يكون لديك متطلَّب أن يجعل ملفّ إعدادات مكسور التحميل نفسه يفشل. حتّى حينها، أبقِه عند الحدّ الأدنى من «حاول وافشل فوراً».
  4. انظر في DisableThreadLibraryCalls في DLL_PROCESS_ATTACH. إن كان الـDLL لا يستخدم إشعارات مؤشّر الترابط، يمكنك إزالة كلفة الإشعار نفسها (إلا عند استخدام CRT الساكن أو TLS الساكن).5
  5. افحص بـApplication Verifier. كثير من الاستدعاءات الخطرة داخل DllMain ممّا يكشفه Application Verifier وقت التشغيل.1
توجيه تصميميّ لتهيئة الـDLLانظر أوّلاً ما إذا كان يمكن جعل التهيئة ساكنة وقت الترجمة؛ إن لم يمكن، الافتراضيّ التأجيل إلى أوّل استخدام، واترك في DllMain فقط الحدّ الأدنى الذي يجب كشفه مبكّراً كفشل تحميلنعملالانعمهل يمكن حسمه وقت الترجمة؟اجعله تهيئة ساكنةهل يجب كشف الفشل عند التحميل؟أجِّل إلى أوّل استخدام(الافتراضيّ)افعل الحدّ الأدنى فقط في DllMainاستبعد بـ INIT_ONCE أو ساكن محلّيّ الدالّة

الشكل 6: ترتيب القرار «هل يمكن أن يكون ساكناً → هل يمكن تأجيله»، وما تتركه في DllMain هو فقط الحدّ الأدنى الذي يجب كشفه مبكّراً.

ما إذا كان ينبغي تطبيق DisableThreadLibraryCalls يمكن حسمه آليّاً بالتفرع التالي.

ما إذا كان ينبغي استدعاء DisableThreadLibraryCallsلا تستدعِه من DLL مربوط مع CRT الساكن؛ إن كان TLS الساكن سارياً يفشل الاستدعاء نفسه فلا تستدعِه؛ إن لم ينطبق أيّ منهما والـDLL لا يستخدم إشعارات مؤشّر الترابط، استدعِه في DLL_PROCESS_ATTACH، متحقّقاً من قيمة الإرجاع، لقطع كلفة الإشعارنعملانعملالانعممربوط مع CRT الساكن؟يجب ألا تستدعيهتستخدم TLS ساكناً؟الاستدعاء يفشل على أيّ حال(FALSE)هل إشعارات مؤشّر الترابط لازمة؟استدعِه في ATTACH(تحقّق من الإرجاع)لا تستدعِه؛ عالج الإشعارات

الشكل 7: الشروط الثلاثة لـCRT الساكن وTLS الساكن وما إذا كانت الإشعارات لازمة تحسم بشكل فريد ما إذا كان ينبغي استدعاؤه.

لـإيقاف مؤشّرات الترابط عند إلغاء التحميل، تعطي الوثائق الرسميّة بروتوكولاً ملموساً. بدلاً من «انتظار» خروج مؤشّرات العامل في DLL_PROCESS_DETACH (عند إلغاء تحميل عبر FreeLibrary)، الشكل هو (1) أشعِر بالخروج بحدث، (2) جانب مؤشّر الترابط يطوي عمله إلى حالة متّسقة، ويُشعِر عائداً، ويدخل انتظاراً لا نهائياً، (3) جانب DllMain يؤكِّد الحالة المتّسقة ثمّ يطوي المؤشّر بـTerminateThread.3 يبدو خشناً، لكنّه موثَّق بوصفه الجواب الواقعيّ داخل قيد «يجب ألا تنتظر الخروج الطبيعيّ لمؤشّر ترابط داخل DllMain».

بروتوكول إيقاف مؤشّر ترابط عند إلغاء التحميلتُشعِر DllMain مؤشّر العامل بالخروج بحدث؛ يطوي العامل عمله إلى حالة متّسقة، ويُشعِر عائداً، ويدخل انتظاراً لا نهائياً؛ تؤكِّد DllMain الحالة المتّسقة ثمّ تُنهي المؤشّرمؤشّر ترابط عاملDllMain(معالجة DETACH)مؤشّر ترابط عاملDllMain(معالجة DETACH)لا انتظار لخروج طبيعيّ، لذا لا جمودأشعِر بالخروج بحدثاطوِ العمل إلى حالة متّسقةأشعِر باكتمال الاتّساق وانتظر إلى الأبدأَنهِ بـ TerminateThread

الشكل 8: بدلاً من «انتظر خروجاً طبيعيّاً»، «انتظر إشارة اتّساق ثمّ اقطعه» يتجنّب تصادماً مع قفل المحمِّل.

كمسألة مبادئ أولى، التصميم الأكثر أماناً تجنّب امتلاك مؤشّرات ترابط في DLL يمكن إلغاء تحميله، والإبقاء على ملكيّة مؤشّر الترابط في جانب EXE.

DLL_PROCESS_DETACH عند خروج العمليّة هو العكس: عدم فعل شيء والعودة هو المثاليّ. عند هذه النقطة يكون كلّ مؤشّر ترابط آخر قد أُنهي قسراً أصلاً، ولا يمكنك الاعتماد على حالة DLLs التابعة أو بيئة التشغيل أيضاً. العمل المعقّد هنا يسبّب فقط جموداً وانهيارات. البيانات التي يجب إبقاؤها ينبغي كتابتها في مسار إيقاف التطبيق نفسه؛ لا تعتمد على هذا الإشعار.3

6. كيف تحقّق عندما تصطدم به

تعليقات قفل المحمِّل لها بصمة يمكن التعرّف عليها.

انظر إلى المكدّسات في تفريغ تعليق. خذ تفريغاً للحظة المتجمّدة وافحص مكدّس كلّ مؤشّر ترابط. إن وجدت زوجاً من مؤشّر ينتظر قفلاً داخل دوال محمِّل ntdll.dll (العائلة التي تبدأ أسماؤها بـLdr) ومؤشّر ينتظر شيئاً آخر داخل DllMain أو مُهيِّئ ساكن (dynamic initializer)، فأنت شبه متأكّد. مؤشّر متوقّف في وسط استدعاء LoadLibrary شخصيّة نموذجيّة أخرى.

بصمة تعليق قفل المحمِّلفي تفريغ تعليق، إن وجدت معاً مؤشّراً ينتظر قفلاً داخل دوال محمِّل ntdll ومؤشّراً ينتظر شيئاً آخر داخل DllMain أو مُهيِّئ ساكن، يمكنك معاملته جمود قفل محمِّل بيقين شبه تامّنعملاتفريغ تعليقمؤشّر ينتظر قفلاً في دوال عائلة Ldrمؤشّر ينتظر داخل DllMain أو مُهيِّئ ساكنكلاهما حاضر؟شبه يقين: جمود قفل المحمِّلحقِّق كتعليق من نوع آخر

الشكل 9: لتعليقات قفل المحمِّل البصمة القابلة للتعرّف «انتظار في Ldr + انتظار داخل DllMain».

اشتبه في طابع «المعتمد على التوقيت». جمود قفل المحمِّل يصمد فقط في اللحظة التي يتزامن فيها تحميل DLL مع بدء مؤشّر ترابط أو خروجه. شروط إعادة الإنتاج مثل «أحياناً عند البدء»، و«فقط على جهاز معيّن»، و«فقط عند التشغيل كخدمة» علامات لهذا النوع من المشكلة.

شغِّل فحصاً وقائيّاً. فعِّل Application Verifier وشغِّل اختباراتك، ويمكنك كشف الاستدعاءات الخطرة داخل DllMain وقت التشغيل.1 لـC++/CLI، لا تتجاهل التحذير C4747؛ في مراجعات الدوال القابلة للوصول من DllMain، أضِف زاوية «الدوال التي تستدعي LoadLibrary بصورة غير مباشرة» (تهيئة COM، وبعض ميّزات CRT، والاستيرادات مؤجَّلة التحميل، وما شابه) إلى قائمة المراجعة، وستلتقط الحوادث قبل شحنها. أوّل استدعاء لاستيراد مؤجَّل التحميل إذ يصبح LoadLibrary داخليّاً نقطة يسهل تفويتها.

7. الخلاصة

  • تُستدعى DllMain وهي تمسك قفل المحمِّل (واحد لكلّ عمليّة، القفل الذي يسلسل كلّ إشعار DLL). كلّ قيد يتبع من ذلك.
  • جوهر المحظورات «لا تستدعِ LoadLibrary / FreeLibrary»، و«لا تتزامن مع مؤشّرات ترابط أخرى»، و«لا تستدعِ دوال تعتمد على DLL غير Kernel32». بنّاءات الكائنات الساكنة وهاداتها التي تعمل عبر CRT تقع تحت القيود نفسها.
  • سياسة التصميم الأساسيّة هي التأجيل. اجعل ساكناً التهيئة التي يمكنك جعلها ساكنة؛ أجِّل الباقي إلى أوّل استخدام. استخدم DisableThreadLibraryCalls وApplication Verifier.
  • إيقاف مؤشّرات الترابط عند إلغاء التحميل يتبع البروتوكول الرسميّ (أشعِر → أكِّد الاتّساق → أَنهِ). DLL_PROCESS_DETACH عند خروج العمليّة مثاليّاً فارغ.
  • في C++/CLI، تشغيل MSIL تحت قفل المحمِّل لغم قائم بذاته. أصرّ على ترجمة أصليّة لشجرة استدعاء DllMain.

قيود DllMain تبدو، في البداية، قائمة محظورات غير معقولة. لكن متى أمسكت النقطة الواحدة أنّ «تُستدعى وهي تمسك قفل المحمِّل، القفل الأعلى»، فكلّ محظور إعادة صياغة للمبدأ نفسه. تذكّره مبدأً، وعندما تقابل حالة حدّ ليست في الوثائق، ينبغي أن تظلّ قادراً على طرح السؤال الصحيح: «هل هذا عمل يُسمح لي بفعله وأنا أمسك القفل؟»

مقالات ذات صلة

مجالات الاستشارة ذات الصلة

تتعامل شركة كومورا سوفت ذ.م.م. مع تحقيق السبب الجذريّ لتعليقات وجمود عند البدء أو تحميل الـDLL (تحليل التفريغ)، ومراجعات التصميم حول DllMain والتهيئة الساكنة، ومعالجة أغلفة C++/CLI وDLLs الإضافات نحو تصميم تهيئة آمن. يمكنك استشارتنا حتّى في مرحلة صعوبة إعادة الإنتاج «يتجمّد فقط عند البدء في بيئة معيّنة».

روابط مرجعيّة

  1. Microsoft Learn, Dynamic-Link Library Best Practices. حول استدعاء DllMain وقفل المحمِّل ممسوك، بحيث تكون الدوال التي يمكنك استدعاؤها مقيَّدة بشدّة؛ وكون DllMain المثاليّة كعباً فارغاً وتأجيل التهيئة قدر الإمكان؛ وتوصية التهيئة الساكنة وقت الترجمة؛ وفعل الحدّ الأدنى فقط للإخفاقات التي يجب كشفها مبكّراً؛ وكشف أخطاء DllMain النموذجيّة بـApplication Verifier.  2 3 4 5 6

  2. Microsoft Learn, DllMain entry point. حول أداء تهيئة وإنهاء بسيطين فقط في نقطة الدخول؛ ولماذا يجب ألا تستدعي LoadLibrary / FreeLibrary (ترتيب تحميل دائريّ واستخدام DLL قبل التهيئة أو بعد الإنهاء)؛ وكون Kernel32.dll مضموناً محمَّلاً أصلاً، بحيث يمكنك استدعاؤه في النطاق الذي لا يحمِّل DLLs أخرى؛ وعدم وجود قائمة مستنفِدة بالدوال الآمنة؛ وتسبّب دوال User وShell وCOM في انتهاكات وصول؛ وكون إشعارات DLL مُسلسَلة، بحيث يتسبّب التواصل مع مؤشّرات ترابط أو عمليّات أخرى في جمود؛ وانطباق القيود نفسها على بنّاءات الكائنات الساكنة وهاداتها عند ربط CRT.  2 3 4 5 6 7

  3. Microsoft Learn, Dynamic-Link Library Best Practices - Best Practices for Synchronization. حول البنية التي تجمد إن انتظرت خروج مؤشّر ترابط داخل DllMain (إشعار DLL_THREAD_DETACH لخروج مؤشّر الترابط يحتاج قفل المحمِّل)؛ وبروتوكول إيقاف مؤشّر ترابط عند إلغاء التحميل (أشعِر بحدث، أكِّد حالة متّسقة، ثمّ أَنهِ)؛ وكون DLL_PROCESS_DETACH عند خروج العمليّة قد أُنهيت فيه مؤشّرات الترابط الأخرى قسراً أصلاً ولا ضمان لاتّساق فضاء العناوين، لذا المعالج المثاليّ فارغ؛ وكون إنشاء مؤشّر ترابط في DllMain يترك إشعارات في الطابور بتهيئة غير مكتملة ويسبّب مشكلات.  2 3 4

  4. Microsoft Learn, Initialization of Mixed Assemblies. حول عدم تنفيذ MSIL تحت قفل المحمِّل؛ وعدم ترجمة DllMain وشجرة استدعائها إلى MSIL والتعامل مع ذلك عبر #pragma unmanaged؛ وإصدار التحذير C4747 عندما تحاول DllMain تنفيذ MSIL مباشرةً، لكن التنفيذ غير المباشر عبر وحدة أخرى غير قابل للكشف؛ وكون المُهيِّئات الديناميكيّة للكائنات الساكنة قادرة على التسبّب في المشكلة نفسها.  2

  5. Microsoft Learn, DisableThreadLibraryCalls function (libloaderapi.h). حول تعطيل إشعارات DLL_THREAD_ATTACH / DLL_THREAD_DETACH لتقليل الكلفة عند إنشاء مؤشّر الترابط وإتلافه؛ وعدم استدعائه من DLL مربوط مع CRT الساكن؛ وعدم أداء التحسين عندما يكون TLS الساكن (thread_local أو __declspec(thread)) سارياً.  2

  6. Microsoft Learn, Dynamic-Link Library Best Practices - Deadlocks Caused by Lock Order Inversion. حول تعريف هرم أقفال والأخذ دائماً بالترتيب نفسه؛ وأخذ المحمِّل قفل المحمِّل قبل استدعاء DllMain، لذا ينبغي أن يجلس قفل المحمِّل في قمّة هرم الأقفال؛ ومراعاة ترتيب الأخذ بين واجهات تأخذ قفل المحمِّل بصورة غير مباشرة، مثل GetModuleFileName، والأقفال الخاصّة؛ ومثال ملموس لجمود من قلب ترتيب الأقفال.  2

أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.

ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.

الأسئلة الشائعة

أسئلة شائعة حول موضوع هذه المقالة.

هل حقّاً لا يمكن فعل أيّ شيء على الإطلاق في DllMain؟
«لا تفعل شيئاً» ليست مبالغة؛ إنّها الموقف التصميميّ الرسميّ، ومايكروسوفت نفسها تقول إنّ DllMain المثاليّة شبه فارغة. ما هو آمن مجموعة فرعيّة من دوال Kernel32.dll ── Kernel32 مضمون التحميل بحلول وقت تشغيل DllMain ── في النطاق الذي لا يحمِّل DLLs أخرى. إنشاء قسم حرج أو كائن مزامنة، واستخدام TLS، أمثلة على ما يمكنك فعله. بالمقابل، LoadLibrary/FreeLibrary، والتزامن مع مؤشّرات ترابط أخرى، واستدعاء دوال في User32 وShell وCOM وما شابه، محظورة لأنّها تسبّب جموداً وانتهاكات وصول. التهيئة التي لست متأكّداً منها ينبغي ألا تُفعَل في DllMain؛ أجِّلها إلى أوّل استخدام.
هل بنّاءات الكائنات العامّة في C++ (الكائنات الساكنة) تقع أيضاً تحت قيود DllMain؟
نعم. عندما يُربَط الـDLL مع CRT (بيئة تشغيل C++)، تعمل بنّاءات وهادات الكائنات العامّة والساكنة، عبر نقطة الدخول التي يوفّرها CRT، كجزء فعليّ من DllMain. ذلك يعني أنّ استدعاء LoadLibrary من بنّاء، وبدء مؤشّر ترابط آخر وانتظار انتهائه، وتهيئة COM، وما شابه، تحمل الخطر نفسه كفعلها في DllMain. لكائن عامّ بتهيئة غير تافهة، أبقِ مؤشّراً وابنِه عند أوّل وصول، أو استخدم ساكناً محلّيّ الدالّة، حتّى يعمل العمل خارج DllMain.
هل ينبغي أن أستدعي DisableThreadLibraryCalls؟
بشروط، نعم. إذا كان الـDLL لا يحتاج إشعارات DLL_THREAD_ATTACH/DETACH، فإنّ استدعاء DisableThreadLibraryCalls في DLL_PROCESS_ATTACH يوقف إشعارات إنشاء مؤشّر الترابط وخروجه ويقلِّل الكلفة في عمليّة تُنشئ مؤشّرات ترابط بكثرة. ثمّة استثناءان. لا تستدعِه من DLL مربوط مع CRT الساكن (CRT الساكن يحتاج إشعارات مؤشّر الترابط). وإن كان TLS الساكن عبر thread_local أو __declspec(thread) سارياً، يفشل الاستدعاء نفسه ويُرجع FALSE، لذا اعتد التحقّق من قيمة الإرجاع. استخدمه على DLL نموذجيّ يستخدم CRT المربوط ديناميكيّاً، بعد أن تتأكّد أنّ لا شيء يعتمد على إشعارات مؤشّر الترابط.
لماذا يتجمّد DLL من C++/CLI (مختلط مُدار) عند البدء؟
السبب النموذجيّ محاولة تشغيل MSIL (شيفرة مُدارة) بينما قفل المحمِّل ممسوك. في تجميعة C++/CLI مختلطة، إذا كانت DllMain أو الدوال المستدعاة منها أو المُهيِّئات الديناميكيّة للكائنات العامّة مُترجَمة إلى MSIL، فقد تُطلَب تهيئة CLR أو تحميل تجميعة أخرى تحت قفل المحمِّل، وقد يجمد ذلك. يُصدر المترجم التحذير C4747 عندما تحاول DllMain نفسها تنفيذ MSIL مباشرةً، لكنّه لا يستطيع كشف التنفيذ غير المباشر عبر وحدة أخرى. التخفيف هو ترجمة DllMain وشجرة استدعائها أصليّة بـ #pragma unmanaged ── أو ألا تكون لديك DllMain أصلاً.
هل يجوز لي تنظيف الموارد في DLL_PROCESS_DETACH؟
الجواب يتغيّر بين «خروج العمليّة» و«إلغاء التحميل عبر FreeLibrary». عند DLL_PROCESS_DETACH في خروج العمليّة، تكون مؤشّرات الترابط الأخرى قد أُنهيت أصلاً، ولا ضمان أنّ فضاء العناوين ما زال متّسقاً، لذا التنظيف كتحرير الذاكرة خطر فعليّاً؛ التوجيه الرسميّ أنّ «المعالج المثاليّ فارغ». اكتب أيّ بيانات يجب إبقاؤها في مسار إيقاف التطبيق نفسه، وهنا أساساً لا تفعل شيئاً وعُد. عند إلغاء تحميل عبر FreeLibrary، تستمرّ العمليّة، لذا تحتاج تنظيفاً كاملاً ── إيقاف مؤشّرات الترابط، وإغلاق المقابض، وما شابه. انتظار خروج مؤشّر ترابط داخل DllMain يجمد، غير أنّك يجب أن تتبع البروتوكول الرسميّ: أشعِر، وانتظر حتّى حالة متّسقة، وأنهِ العمل خارج DllMain.

الملف الشخصي للمؤلف

صفحة الملف الشخصي لمؤلف المقالة.

غو كومورا

مؤسّس شركة كومورا سوفت ذ.م.م.

يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.

العودة إلى المدونة