DllMain وقفل المحمِّل ── السبب الحقيقي لعبارة «لا تفعل شيئاً في تهيئة الـDLL»
· آخر تحديث: · غو كومورا · Windows, DLL, تطوير Windows, C++, تحقيق الأعطال, تعدّد مؤشّرات الترابط, Win32 API
سجل التعديلات (النسخة الأولى، نُشرت في 22 Aug، 2026)
- النشر الأول
الاستشهاد بهذا المقال(DOI (الأرشيف المسجّل): 10.5281/zenodo.22176693)
تشير معرّفات DOI أدناه إلى إصدارات مؤرشفة سابقًا، وقد تختلف عن النص الحالي. للإشارة إلى النص الحالي، استخدم رابط هذه الصفحة.
غو كومورا (2026). DllMain وقفل المحمِّل ── السبب الحقيقي لعبارة «لا تفعل شيئاً في تهيئة الـDLL». شركة كومورا سوفت ذ.م.م.. https://comcomponent.com/ar/blog/dllmain-loader-lock/
- DOI (الأرشيف المسجّل)
- 10.5281/zenodo.22176693
- DOI (آخر إصدار مسجّل)
- 10.5281/zenodo.22241135
«عندما نحمِّل DLL الخاصّ بنا، لا يعود LoadLibrary أبداً.» «يتجمّد فقط على أجهزة معيّنة، أو فقط عند بدء الخدمة.» عند أعطال كهذه، من أوائل الأماكن التي ينبغي فحصها شيفرة تهيئة الـDLL: DllMain وكلّ ما تستدعيه.
DllMain ليست كدالّة تهيئة عاديّة في تطبيق. إنّها دالّة يستدعيها نظام التشغيل وهو يمسك قفل المحمِّل، لذا ما يجوز فعله فيها مقيَّد بشدّة. موقف مايكروسوفت أنّ «DllMain المثاليّة كعب فارغ» يأتي من أنّ هذا القيد لا يصيب DLL الخاصّ بك وحدها، بل يصيب أيضاً سائر الـDLLs ومؤشّرات الترابط في العمليّة.1
المقال موجَّه إلى المطوِّرين الذين يكتبون DLLs وإضافات وأغلفة C++/CLI على Windows. يرتّب الموضوع بهذا الترتيب: لماذا يتوقّف، ثمّ أين تنقل التهيئة، ثمّ كيف تُنهي، ثمّ كيف تحقِّق في تعليق.
1. الخلاصة أوّلاً ── أبقِ DllMain صغيرة وغيِّر متى يجري العمل
بدل البحث عن طريقة آمنة لكتابة شيء داخل DllMain، الأساس تقليل العمل الذي يعمل هناك. قرِّر ما يبقى بالترتيب التالي.1
| القرار | السياسة الأساسيّة | أين تقرأ المزيد |
|---|---|---|
| متى تُهيِّئ | اجعل ساكناً ما يمكن حسمه وقت الترجمة؛ وأجِّل الباقي إلى أوّل استخدام بعد اكتمال التحميل | القسم 5 |
| ماذا تستدعي من DllMain | تجنّب LoadLibrary / FreeLibrary، والمزامنة مع مؤشّرات ترابط أخرى، واستخدام User وShell وCOM وما شابه. افحص الاستدعاءات غير المباشرة أيضاً |
القسم 3 |
| ماذا تفعل عند الإنهاء | افصل حالة إلغاء تحميل الـDLL وحدها عن حالة خروج العمليّة كلّها | القسم 6 |
ما يسهل تفويته أنّ القيود نفسها تسري على بنّاءات وهادات كائنات C++ العامّة والساكنة. في C++/CLI، احذف أيضاً كلّ مسار ينفِّذ MSIL تحت قفل المحمِّل. فراغ جسم DllMain نفسه لا يكفي للحكم.23
إن كنت تحقِّق الآن في تعليق، ابدأ من القسم 7؛ وإن كنت تراجع تصميماً، اقرأ الأقسام 2 إلى 6 بالترتيب، فتقابل كلّ حظر بعلاجه.
2. الآليّة ── تُستدعى DllMain داخل قفل المحمِّل
2.1 تُستدعى لا عند التحميل فقط، بل أيضاً عند بدء مؤشّرات الترابط وخروجها
DllMain نقطة الدخول التي يستدعيها محمِّل نظام التشغيل عندما يدخل DLL عمليّة أو مؤشّر ترابط أو يغادرهما. الإشعارات أربعة.2
| الإشعار | التوقيت |
|---|---|
| DLL_PROCESS_ATTACH | عندما يُحمَّل الـDLL في العمليّة |
| DLL_THREAD_ATTACH | عندما يبدأ مؤشّر ترابط جديد في العمليّة |
| DLL_THREAD_DETACH | عندما يخرج مؤشّر ترابط بشكل طبيعيّ |
| DLL_PROCESS_DETACH | عندما يُلغى تحميل الـDLL، أو عندما تخرج العمليّة |
إشعار بدء مؤشّر الترابط لا يصل إلى الـDLL الذي أنشأ مؤشّر الترابط فحسب، بل إلى كلّ DLL محمَّل في العمليّة. DllMain ليست «شيفرة تعمل مرّة عندما يُحمَّل DLL الخاصّ بي». في عمليّة تُنشئ مؤشّرات ترابط بكثرة، تعمل عند كلّ إشعار.2
إن لم تحتج الإشعارات، ثمّة خيار باستدعاء DisableThreadLibraryCalls داخل DLL_PROCESS_ATTACH. غير أنّه لا يُستخدم في DLL مربوط مع CRT الساكن، ولـ TLS الساكن شرط خاصّ به، لذا يعالج القسم 5.3 ذلك القرار على حدة.4
2.2 أن تُستدعى والقفل ممسوك هو نقطة انطلاق كلّ قيد
ليحفظ محمِّل نظام التشغيل اتّساق تحميل الـDLL وإلغاء تحميله وإشعاراته، يسلسلها بقفل محمِّل واحد لكلّ عمليّة. المهمّ أنّه يأخذ هذا القفل قبل استدعاء DllMain ويظلّ يمسكه طوال تشغيل DllMain.1
خلال ذلك، أيّ مؤشّر ترابط آخر في العمليّة نفسها يحاول تحميل DLL أو إكمال إشعار بدء مؤشّر ترابط أو خروجه ينتظر تحرير القفل. ليست مكاناً يجوز أن تضع فيه انتظاراً طويلاً لمجرّد أنّ ذلك يناسب DLL الخاصّ بك.
flowchart TB
accTitle: لماذا يؤثّر العمل في DllMain على إشعارات DLL عبر العمليّة
accDescr: يأخذ المحمِّل قفل المحمِّل المشترك للعمليّة قبل استدعاء DllMain، وتحميلات DLL وإشعارات مؤشّرات الترابط على مؤشّرات أخرى تنتظر تحريره، لذا يؤثّر العمل في DllMain على DLLs ومؤشّرات ترابط أخرى أيضاً
loader["المحمِّل يأخذ القفل المشترك"] --> dll["تعمل DllMain"]
dll --> ret["تعود DllMain"]
ret --> unlock["يُحرَّر قفل المحمِّل"]
other["تحميل DLL أو إشعار على مؤشّر ترابط آخر"] --> wait["ينتظر تحرير القفل نفسه"]
unlock --> resume["يمكن للعمل المنتظِر أن يتقدّم"]
wait --> resume
الشكل 1: القفل المشترك ممسوك طوال تشغيل DllMain، لذا يوقف انتظار هناك تقدّم DLLs ومؤشّرات ترابط أخرى.
المحظورات التالية تصير مفهومة حين تسأل «هل يحتاج هذا العمل، مباشرة أو بصورة غير مباشرة، قفل المحمِّل أو تهيئة DLL آخر؟»
3. لماذا يتوقّف ── فهم المحظورات عبر أربعة مسارات
3.1 استدعاء شيء يحمِّل DLL آخر
تجنّب استدعاء LoadLibrary / FreeLibrary من DllMain. يخلق LoadLibrary اعتماداً دائريّاً في ترتيب التحميل ويؤدّي إلى استخدام DLL قبل أن تعمل شيفرة تهيئته. في جانب الإنهاء كذلك خطر استخدام DLL سبقت معالجته أو تحريره.2
ألّا تكون قد كتبت LoadLibrary بنفسك لا يجعلك آمناً. بعض دوال User وShell وCOM تحمِّل مكوِّنات نظام أخرى داخليّاً، وقد تلمس مكوِّناً لم يُهيَّأ بعد أو حُرِّر أصلاً فتسبّب انتهاك وصول.2
خطّ الأساس لما يمكن استدعاؤه بأمان هو دوال Kernel32.dll التي لا تحمِّل DLLs أخرى، لأن Kernel32.dll مضمون التحميل بحلول وقت تشغيل DllMain. مثلاً يمكنك إنشاء كائنات مزامنة كالأقسام الحرجة وكائنات mutex، ويمكنك استخدام TLS. غير أنّ الوثائق الرسميّة تنصّ صراحة على أنّه لا توجد قائمة شاملة بالدوال الآمنة. لا تستنتج «إنّه في Kernel32، فكلّ شيء جائز» ولا «يمكنني إنشاء كائن مزامنة، إذن يجوز لي انتظار مؤشّرات ترابط أخرى».2
3.2 الانتظار داخل DllMain لخروج مؤشّر ترابط آخر
الجمود الكلاسيكيّ يحدث عند إلغاء تحميل الـDLL: تطلب DllMain من مؤشّر ترابط عامل أن يتوقّف ثمّ تنتظر خروجه.
حتّى بعد أن يُنهي العامل عمله الخاصّ، عليه المرور بإشعار DLL_THREAD_DETACH عند خروج مؤشّر الترابط. ذلك الإشعار يحتاج قفل المحمِّل الذي تمسكه DllMain المنتظِرة. النتيجة أنّ DllMain تنتظر العامل، والعامل ينتظر عودة DllMain.5
sequenceDiagram
accTitle: لماذا يجمد انتظار مؤشّر ترابط في DllMain
accDescr: DllMain، ممسكة قفل المحمِّل، تنتظر خروج مؤشّر ترابط عامل، لكنّ مؤشّر الترابط العامل الخارج ينتظر تحرير قفل المحمِّل من أجل إشعار DLL_THREAD_DETACH، فينتظر كلّ منهما الآخر ويجمدان
participant L as المحمِّل (يمسك القفل)
participant D as DllMain
participant W as مؤشّر الترابط العامل
L->>D: إشعار DLL_PROCESS_DETACH
D->>W: طلب الخروج وانتظار الاكتمال
W->>W: إنهاء العمل والاتّجاه لخروج مؤشّر الترابط
Note over W: إشعار الخروج يحتاج قفل المحمِّل
Note over D,W: DllMain تنتظر ممسكة القفل، وW ينتظر القفل
الشكل 2: حتّى بعد أن يُنهي العامل عمله، لا يستطيع إكمال إشعار خروج مؤشّر الترابط، لذا لا ينتهي انتظار الخروج داخل DllMain.
هذه ليست حالة «يتوقّف إن كنت سيّئ الحظّ»؛ الانتظار المتبادل قائم بنيويّاً. التنظيف اللازم عند إلغاء التحميل يُعالَج في القسم 6، منفصلاً عن العمل عند خروج العمليّة.
3.3 قفلك الخاصّ وقفل المحمِّل يُؤخذان بترتيب معكوس
ليس بدء مؤشّر الترابط وخروجه فحسب؛ واجهات مثل GetModuleHandle تحتاج قفل المحمِّل داخليّاً أيضاً. عندما يتقاطع المساران التاليان، ينعكس ترتيب أخذ الأقفال.6
- في جانب DllMain، شيفرة تمسك قفل المحمِّل أصلاً تحاول أخذ القفل الخاصّ G.
- في جانب العامل، شيفرة تمسك القفل الخاصّ G أصلاً تستدعي واجهة وتحاول أخذ قفل المحمِّل.
flowchart TB
accTitle: انقلاب الترتيب بين قفل المحمِّل وقفل خاصّ
accDescr: تذهب DllMain إلى قفل خاصّ وهي تمسك قفل المحمِّل، ويذهب مؤشّر ترابط عامل إلى قفل المحمِّل، من أجل GetModuleHandle وما شابه، وهو يمسك القفل الخاصّ، فينعكس ترتيب الأخذ ويجمدان
d["DllMain: تمسك قفل المحمِّل"] --> dg["تذهب إلى القفل الخاصّ G"]
w["العامل: يمسك القفل الخاصّ G"] --> wl["يذهب إلى قفل المحمِّل"]
dg -.-> dead["جمود من انقلاب ترتيب الأخذ"]
wl -.-> dead
wl -.-> api["مطلوب داخليّاً من GetModuleHandle وغيرها"]
الشكل 3: عندما يأخذ أحد الطرفين قفل المحمِّل ثمّ G، والآخر G ثمّ قفل المحمِّل، ينتظر كلّ منهما تحرير الآخر.
التوجيه الرسميّ يطلب أن تعامل قفل المحمِّل بوصفه قمة هرم أقفال التطبيق، أي القفل الذي يُؤخذ أوّلاً. بحلول دخولك DllMain، ذلك القفل ممسوك أصلاً. افحص لا اسم الدالّة التي تستدعيها فحسب، بل أيضاً أيّ أقفال تمسكها عند الاستدعاء.6
3.4 مجرّد إنشاء مؤشّر ترابط يُبقي مشكلتَي انتظار البدء والعمر
CreateThread داخل DllMain غير مستحسن أيضاً. لا يستطيع مؤشّر الترابط الجديد بدء تشغيل دالّة مؤشّر الترابط حتّى تُعالَج إشعار DLL_THREAD_ATTACH. لأنّ DllMain الحاليّة تمسك قفل المحمِّل، انتظار بدء مؤشّر الترابط أو انتهائه داخل تلك DllMain يجمد.5
عدم الانتظار لا يحلّ كلّ شيء أيضاً. إن أُلغي تحميل الـDLL بعد عودة DllMain وقبل أن يبدأ مؤشّر الترابط الذي أنشأته بالعمل، يشير عنوان بدء مؤشّر الترابط إلى شيفرة حُرِّرت أصلاً، ويبقى الانهيار ممكناً.5
flowchart TB
accTitle: مشكلتان تبقيان عند إنشاء مؤشّر ترابط في DllMain
accDescr: مؤشّر ترابط أُنشئ في DllMain ينتظر قفل المحمِّل من أجل إشعار البدء، فإن انتظرت DllMain بدءه أو اكتماله تجامدا، وحتّى إن عادت DllMain بلا انتظار ينتهي عمر الشيفرة وينهار إن أُلغي تحميل الـDLL قبل أن يبدأ مؤشّر الترابط بالعمل
create["إنشاء مؤشّر ترابط داخل DllMain"] --> pending["مؤشّر الترابط الجديد ينتظر إشعار البدء"]
pending --> q{"انتظار البدء أو الاكتمال داخل DllMain؟"}
q -->|"نعم"| dead["انتظار متبادل والقفل ممسوك"]
q -->|"لا"| returns["العودة من DllMain"]
returns --> race["إلغاء تحميل الـDLL قبل أن يبدأ مؤشّر الترابط"]
race --> crash["الشيفرة عند عنوان البدء اختفت"]
الشكل 4: عدم الانتظار داخل DllMain وحماية عمر الـDLL الذي يستخدمه مؤشّر الترابط المُنشأ مطلبان منفصلان.
4. موضعان خطران حتّى عندما تكون DllMain فارغة
4.1 التهيئة الديناميكيّة للكائنات العامّة والساكنة
في DLL مربوط مع CRT (بيئة تشغيل C/C++)، تعمل بنّاءات وهادات كائنات C++ العامّة والساكنة عبر نقطة دخول CRT. إنّها جزء فعليّ من DllMain وتخضع للقيود نفسها.2
وضع تحميل ملفّ إعداد، أو بدء آليّة السجلّ، أو تهيئة COM، أو بدء مؤشّر ترابط في بنّاء يخفي مكان الاستدعاء فقط؛ لحظة التشغيل ما زالت داخل قفل المحمِّل. إن تضمّن ذلك العمل المعقّد تحميل DLL آخر أو مزامنة مع مؤشّرات ترابط، فهو خطر تماماً ككتابته في جسم DllMain.
flowchart TB
accTitle: كيف تصير تهيئة كائن عامّ لغماً
accDescr: تحميل الـDLL يأخذ قفل المحمِّل وتعمل بنّاءات الكائنات العامّة عبر CRT، لذا فإنّ LoadLibrary أو مزامنة مؤشّر ترابط أو تهيئة COM داخلها تنفيذ لما تحظره DllMain
load["تحميل الـDLL (أخذ قفل المحمِّل)"] --> crt["نقطة دخول CRT"]
crt --> ctor["بنّاء الكائن العامّ"]
ctor --> ng1["عمل يكافئ LoadLibrary"]
ctor --> ng2["بدء مؤشّر ترابط وانتظار انتهائه"]
ctor --> ng3["استخدام COM أو User32"]
ng1 -.-> risk["كلّ هذا من محظورات DllMain"]
ng2 -.-> risk
ng3 -.-> risk
الشكل 5: راجع لا جسم DllMain فحسب، بل أيضاً تهيئة الكائنات الساكنة وإنهاءها المستدعاة من CRT.
عامل تهيئة الثوابت وقت الترجمة، مثلاً ما يمكن جعله constexpr، بمعزل عن التهيئة المعقّدة وقت التشغيل. أجِّل التهيئة الديناميكيّة التي تتضمّن استدعاءات دوال، ورتِّب لأن يحدث أوّل وصول خارج DllMain أيضاً.
4.2 MSIL يُنفَّذ في مستدعًى من C++/CLI
تغليف DLL أصليّ بـ C++/CLI مشمول في تغليف DLL أصليّ بـ C++/CLI في الممارسة. في هذا التكوين، راقب المسارات التي تنفِّذ MSIL (شيفرة مُدارة) تحت قفل المحمِّل. إن تطلّب تنفيذ MSIL تهيئة CLR أو تحميل تجميعة أخرى، فالجمود ممكن.3
يُصدر المترجم التحذير C4747 للشيفرة التي تحاول فيها DllMain تنفيذ MSIL مباشرة. لكنّه لا يستطيع كشف التنفيذ غير المباشر عبر دالّة في وحدة أخرى. غياب التحذير وحده ليس أساساً لاعتبار الشيفرة آمنة. المُهيِّئات الديناميكيّة للكائنات الساكنة أيضاً ضمن النطاق.3
flowchart TB
accTitle: إمكان كشف تنفيذ MSIL تحت قفل المحمِّل
accDescr: الشيفرة التي تنفِّذ فيها DllMain الـMSIL مباشرة يمكن أن يكشفها المترجم بالتحذير C4747، أمّا التنفيذ غير المباشر عبر دالّة في وحدة أخرى فلا، لذا يجب منعه بمراجعة شجرة الاستدعاء والترجمة أصليّة في كلّ المسار
d2["استدعاء من DllMain"] --> dir["ينفِّذ MSIL مباشرة"]
d2 --> ind["ينفِّذ عبر وحدة أخرى"]
dir --> c47["يُكشَف بالتحذير C4747"]
ind --> nc["المترجم لا يستطيع كشفه"]
nc -.-> rv["المنع بالمراجعة وpragma unmanaged"]
الشكل 6: بالإضافة إلى المسار المباشر الذي يكشفه C4747، راجع الاستدعاءات التي تمرّ عبر وحدة أخرى.
العلاج ترجمة DllMain وكلّ دالّة يمكن الوصول إليها منها شيفرة أصليّة بـ #pragma unmanaged، أو ألا تكون لديك DllMain أصلاً. وحتّى في الحالة الأخيرة، لا تغفل المسارات غير المباشرة كالمُهيِّئات الساكنة.3
5. تصميم التهيئة ── اجعلها ساكنة، أجِّلها، أبقِ الحدّ الأدنى فقط
5.1 قبل أن تُبقي شيئاً في DllMain، اسأل هل يمكن تغيير توقيته
خطّ الأساس الرسميّ إكمال ما تستطيع من التهيئة وقت الترجمة وتأجيل الباقي أبعد ما يمكن. لا يبقى، استثناءً وبالحدّ الأدنى، إلّا العمل الذي يجب كشفه مبكّراً بوصفه فشل تحميل.1
مثلاً، قد توجد متطلّبة بجعل تحميل الـDLL نفسه يفشل لأنّ ملفّ إعداد يعتمد عليه تالف. حتّى عندئذ، ضيِّقه إلى «حاول العمل المطلوب وافشل فوراً» بدل تشغيل تهيئة أخرى أوّلاً ثمّ الفشل بعدها. هذا ليس استثناءً يبيح تهيئة معقّدة وقت التحميل.1
flowchart TB
accTitle: إرشادات تصميم تهيئة الـDLL
accDescr: انظر أوّلاً هل يمكن جعل التهيئة ساكنة وقت الترجمة؛ وإلّا فالأصل تأجيلها إلى أوّل استخدام، ولا تُبقِ في DllMain إلّا الحدّ الأدنى ممّا يجب كشفه مبكّراً بوصفه فشل تحميل
q1{"يمكن حسمه وقت الترجمة؟"} -->|"نعم"| s["اجعله تهيئة ساكنة"]
q1 -->|"لا"| q2{"يجب كشف الفشل عند التحميل؟"}
q2 -->|"لا"| lazy["أجِّل إلى أوّل استخدام (الأصل)"]
q2 -->|"نعم"| min["افعل الحدّ الأدنى فقط في DllMain"]
lazy -.-> once["احمِ بـ INIT_ONCE أو ساكن محلّيّ الدالّة"]
الشكل 7: انظر في التهيئة الساكنة والمؤجَّلة أوّلاً، ولا تُبقِ في DllMain إلّا الحدّ الأدنى الذي يحتاج كشفاً مبكّراً.
5.2 التهيئة المؤجَّلة يجب تصميمها حتّى «من يستدعيها أوّلاً»
لاستبعاد التداخل عند أوّل استخدام، يمكنك استخدام التهيئة لمرّة واحدة بـ INIT_ONCE أو ساكن محلّيّ الدالّة في C++ (ساكن سحريّ). الفكرة نقل الكائنات العامّة المعقّدة إلى مؤشّر يُنشأ عند أوّل وصول أو إلى ساكن محلّيّ الدالّة.
غير أنّ التأجيل وحده لا يخرجك من قفل المحمِّل. يُرفَع القيد فقط عندما يأتي أوّل وصول من «واجهة عاديّة تُستدعى بعد اكتمال تحميل الـDLL». في تلك الحالة يمكن تصميمه تهيئة عاديّة يجوز لها استخدام شبه كامل لواجهة Windows.1
بالمقابل، إن حدث ذلك الوصول الأوّل من DllMain أو مُهيِّئ ساكن، تنتهي التهيئة إلى العمل تحت قفل المحمِّل. بعد استخراج دالّة تهيئة، أكِّد من يستدعيها أوّلاً، ومتى.
flowchart TB
accTitle: متى تخرج التهيئة المؤجَّلة من قفل المحمِّل
accDescr: إن جاء أوّل وصول للتهيئة المؤجَّلة من واجهة عاديّة بعد اكتمال التحميل، يمكن التهيئة خارج قفل المحمِّل، أمّا إن جاء من DllMain أو مُهيِّئ ساكن فتعمل تحت القيود نفسها
first{"من أين يأتي أوّل وصول؟"}
first -->|"واجهة عاديّة بعد اكتمال التحميل"| outside["تهيئة خارج قفل المحمِّل"]
first -->|"DllMain أو مُهيِّئ ساكن"| inside["تهيئة تحت القيود نفسها"]
outside --> once["احمِ بـ INIT_ONCE أو ساكن محلّيّ الدالّة"]
inside --> move["انقل أيضاً لحظة أوّل وصول"]
الشكل 8: INIT_ONCE والساكن محلّيّ الدالّة يتولّيان استبعاد التداخل للتهيئة؛ وهما لا يضمنان أن تُستدعى خارج قفل المحمِّل.
5.3 قرِّر DisableThreadLibraryCalls بثلاثة شروط
في DLL لا يعتمد على إشعارات مؤشّر الترابط، يوقف استدعاء DisableThreadLibraryCalls في DLL_PROCESS_ATTACH إشعارات DLL_THREAD_ATTACH / DLL_THREAD_DETACH. في عمليّة تُنشئ مؤشّرات ترابط بكثرة، يقلِّل هذا كلفة الإشعار.4
قبل تطبيقه، افحص CRT الساكن، وTLS الساكن، وما إذا كان شيء يستخدم الإشعارات. DLL مربوط مع CRT الساكن يجب ألا يستدعيه، لأنّ CRT نفسه يحتاج إشعارات مؤشّر الترابط. عندما يكون TLS الساكن عبر thread_local أو __declspec(thread) سارياً، يفشل الاستدعاء نفسه ويعيد FALSE.4
flowchart TB
accTitle: قرار استدعاء DisableThreadLibraryCalls
accDescr: DLL مربوط مع CRT الساكن يجب ألا يستدعيه، ومع TLS الساكن يفشل الاستدعاء نفسه فلا يُستدعى، أمّا DLL ليس منهما ولا يستخدم إشعارات مؤشّر الترابط فيمكنه استدعاؤه في DLL_PROCESS_ATTACH مع فحص قيمة الإرجاع لخفض كلفة الإشعار
q1{"مربوط مع CRT الساكن؟"} -->|"نعم"| no2["يجب ألا يُستدعى"]
q1 -->|"لا"| q2{"يستخدم TLS الساكن؟"}
q2 -->|"نعم"| eff["يفشل الاستدعاء على أيّ حال (FALSE)"]
q2 -->|"لا"| q3{"يحتاج إشعارات مؤشّر الترابط؟"}
q3 -->|"لا"| yes["استدعِه في ATTACH (افحص قيمة الإرجاع)"]
q3 -->|"نعم"| keep["لا تستدعِه؛ عالج الإشعارات"]
الشكل 9: ميِّز CRT الساكن حيث يجب عدم استخدامه عن TLS الساكن حيث يفشل، وافحص قيمة الإرجاع حتّى عندما تكون الإشعارات غير لازمة.
هذا تحسين يُنظَر فيه لـDLL نموذجيّ يستخدم CRT المربوط ديناميكيّاً ويستوفي هذه الشروط. DisableThreadLibraryCalls موجود لتقليل الإشعارات؛ وليس وسيلة لفعل تهيئة معقّدة في DllMain.
6. الإنهاء ── افصل إلغاء تحميل الـDLL عن خروج العمليّة
6.1 الـDLL_PROCESS_DETACH نفسه، لكنّ ما يبقى بعده يختلف
يُسلَّم DLL_PROCESS_DETACH عند إلغاء تحميل الـDLL وحدها وعند خروج العمليّة كلّها. اسم الإشعار واحد، لكنّ مقدّمات التنظيف تختلف.5
عند إلغاء التحميل عبر FreeLibrary، تستمرّ العمليّة بعدها. لذا يجب إيقاف مؤشّرات الترابط، وتنظيف المقابض المفتوحة والموارد المحجوزة والحالة التي تحتاج إبقاءً، وما شابه، تنظيفاً صحيحاً. غير أنّه، كما بيّن القسم 3.2، انتظار الخروج الطبيعيّ لمؤشّر ترابط داخل DllMain لهذا الغرض يجمد.5
الأفضل تجنّب تصميم يملك فيه DLL قابل لإلغاء التحميل مؤشّرات ترابط أصلاً؛ نقل ملكيّة مؤشّرات الترابط إلى جانب الـEXE هو الخيار الأكثر أماناً. لتصميم قائم يملك فيه الـDLL مؤشّرات ترابط، انظر بروتوكول الإيقاف التالي مع قيوده معاً، لا أحدهما دون الآخر.
6.2 بروتوكول الإيقاف الموثَّق رسميّاً عندما يملك الـDLL عاملاً
توثِّق أفضل ممارسات مايكروسوفت إجراء إيقاف عند إلغاء التحميل ينتظر لا «الخروج الطبيعيّ لمؤشّر الترابط» بل «إشارة أنّه بلغ حالة متّسقة».5
- جانب DllMain يُشعر العامل بالتوقّف، باستخدام حدث.
- يطوي العامل عمله الحاليّ إلى حالة متّسقة، يُشعر بالاكتمال، ويدخل انتظاراً لا نهائياً.
- جانب DllMain يؤكِّد الحالة المتّسقة ويُنهي مؤشّر الترابط بـ
TerminateThread.
الإجراء يبدو خشناً، لكنّه وُثِّق تحت قيد أنّ انتظار الخروج الطبيعيّ يجعل إشعار الخروج يصطدم بقفل المحمِّل. المقدّمة أنّ عمل بلوغ الحالة المتّسقة يطيع أيضاً القيود نفسها كـDllMain. إن دخل ذلك العمل تحميل DLL آخر أو انتظار قفل المحمِّل، فالجمود مع الجانب المنتظِر للإشارة لا مفرّ منه.5
sequenceDiagram
accTitle: انتظار اكتمال الاتّساق عند إلغاء التحميل لا الخروج الطبيعيّ
accDescr: تُشعر DllMain العامل بالتوقّف، ويبلغ العامل حالة متّسقة وهو يطيع القيود نفسها كـDllMain، ويُشعر بالعودة ويدخل انتظاراً لا نهائياً، ثمّ تؤكِّد DllMain الاتّساق قبل إنهاء مؤشّر الترابط
participant D as DllMain (عند إلغاء التحميل)
participant W as مؤشّر الترابط العامل
D->>W: إشارة التوقّف بحدث
W->>W: بلوغ حالة متّسقة تحت القيود نفسها
W->>D: إشارة بلوغ الاتّساق
W->>W: دخول انتظار لا نهائيّ
D->>W: تأكيد الاتّساق ثمّ TerminateThread
Note over D,W: ما يُنتظَر إشارة الاتّساق لا الخروج الطبيعيّ
الشكل 10: حتّى مع هذا البروتوكول، يجب ألّا ينتظر العمل الذي يُنشئ الحالة المتّسقة قفل المحمِّل.
هذا لا يعني أنّ أيّ مؤشّر ترابط عامل يجوز قطعه كما هو. انظر أوّلاً هل يمكن ملكيّة مؤشّر الترابط خارج الـDLL.
6.3 عند خروج العمليّة، المثاليّ العودة دون فعل شيء
بحلول تسليم DLL_PROCESS_DETACH عند خروج العمليّة، تكون مؤشّرات الترابط الأخرى قد خرجت أو أُنهيت قسراً، ولا يُعتمَد على اتّساق فضاء العناوين. حالة الـDLLs وبيئات التشغيل المعتمدة لا يُوثَق بها أيضاً، فيصير التنظيف كتحرير الذاكرة خطراً لا عوناً. التوجيه الرسميّ يقول أيضاً إنّ المعالج المثاليّ في هذه الحالة فارغ.5
اكتب البيانات التي يجب إبقاؤها في شيفرة إيقاف التطبيق نفسه. لا تستخدم DLL_PROCESS_DETACH بوصفها آخر موضع يمكن فيه ترتيب أيّ شيء.
flowchart TB
accTitle: مقدّمات التنظيف تختلف بين إلغاء تحميل الـDLL وخروج العمليّة
accDescr: عندما يُلغى تحميل الـDLL وحدها تستمرّ العمليّة لذا يجب تنظيف الموارد، لكن تجنّب انتظار الخروج الطبيعيّ داخل DllMain؛ وعند خروج العمليّة لا تعتمد على اتّساق مؤشّرات الترابط الأخرى والموارد، وعد أساساً دون فعل شيء، واكتب الحالة المراد حفظها مسبقاً في شيفرة إيقاف التطبيق
reason{"سبب DLL_PROCESS_DETACH؟"}
reason -->|"إلغاء تحميل الـDLL فقط"| alive["العمليّة تستمرّ بعدها"]
alive --> cleanup["نظِّف الموارد المتبقّية بصحّة"]
cleanup -.-> nojoin["لا تنتظر خروجاً طبيعيّاً داخل DllMain"]
reason -->|"خروج العمليّة"| processEnd["حالة مؤشّرات الترابط الأخرى والموارد غير مؤكَّدة"]
processEnd --> empty["عد أساساً دون فعل شيء"]
empty -.-> save["احفظ مسبقاً في شيفرة إيقاف التطبيق"]
الشكل 11: لا تقرِّر «هل التنظيف لازم» فحسب، بل هل تستمرّ العمليّة وهل يمكن الوثوق بالموارد المستخدمة للتنظيف.
7. التحقيق في التعليق ── ابحث عن الجانب المنتظِر لقفل المحمِّل والجانب الماسك له
7.1 أكِّد في ملفّ التفريغ زوج مؤشّرات الترابط المنتظِرة بعضها
التقط ملفّ تفريغ لحظة التجمّد وافحص مكدّس كلّ مؤشّر ترابط. البصمة النموذجيّة زوج: مؤشّر ترابط ينتظر قفلاً داخل دالّة محمِّل في ntdll.dll يبدأ اسمها بـ Ldr، ومؤشّر ترابط ينتظر شيئاً آخر داخل DllMain أو مُهيِّئ ساكن (dynamic initializer).
مؤشّر ترابط متوقّف في منتصف استدعاء LoadLibrary مشارك نموذجيّ آخر. متى اتّصلت علاقة الجانب المنتظِر للقفل بالجانب الماسك له وهو ينتظر شيئاً آخر، يمكنك شبه الجزم أنّه جمود قفل محمِّل.
flowchart TB
accTitle: تأكيد انتظار متبادل لقفل المحمِّل من تفريغ تعليق
accDescr: في مكدّس كلّ مؤشّر ترابط ابحث عن انتظار قفل في دوال Ldr وانتظار داخل DllMain أو مُهيِّئ ساكن، وأكِّد أنّ الاثنين ينتظران بعضهما، وإن لم يظهر الزوج النموذجيّ فحقِّق أيضاً في أنواع تعليق أخرى
dump["التقاط تفريغ أثناء التعليق"] --> stacks["فحص مكدّس كلّ مؤشّر ترابط"]
stacks --> ldr["انتظار قفل في دوال Ldr"]
stacks --> init["انتظار داخل DllMain أو مُهيِّئ ساكن"]
ldr --> pair{"هل تتّصل علاقة الانتظار المتبادل؟"}
init --> pair
pair -->|"نعم"| found["جمود قفل المحمِّل"]
pair -->|"غير ظاهر"| more["حقِّق أيضاً في أنواع تعليق أخرى"]
الشكل 12: لا تقرِّر من مكدّس واحد؛ ابحث عن زوج الجانب المنتظِر لقفل المحمِّل والجانب الماسك له وهو ينتظر.
شروط مثل «أحياناً عند البدء»، أو «على جهاز معيّن فقط»، أو «فقط عند التشغيل كخدمة» قرائن أيضاً. يتشكّل الانتظار المتبادل عندما يتزامن تحميل DLL مع بدء مؤشّر ترابط أو خروجه، لذا تغيِّر فروق البيئة وتوقيت التنفيذ كيفيّة ظهوره.
7.2 امنع بـ Application Verifier ومراجعة المستدعَيات
تشغيل الاختبارات مع تفعيل Application Verifier يكشف أخطاء DllMain النموذجيّة وقت التشغيل.1 في C++/CLI، لا تتجاهل التحذير C4747، وافحص المسارات غير المباشرة التي لا يكشفها التحذير بمنظور القسم 4.2.
في المراجعة، ابحث بين العمل الذي يمكن الوصول إليه من DllMain عن دوال تستدعي LoadLibrary بصورة غير مباشرة. تهيئة COM، وبعض ميزات CRT، واستيرادات التحميل المؤجَّل (delay-load) أمور للفحص. خصوصاً، يسهل تفويت أنّ أوّل استدعاء لدالّة استيراد محمَّلة مؤجَّلاً يصير LoadLibrary داخليّاً. حتّى إن عاش اسم الدالّة خارج DllMain، فما دام المستدعي هو DllMain، يبقى القيد.
8. الخلاصة ── انظر لا إلى أسماء الدوال بل إلى «متى، وتحت أيّ قفل، يعمل»
قيود DllMain تُفهَم كلّها من نقطة واحدة: تُستدعى وهي تمسك قفل المحمِّل المشترك للعمليّة. لا DllMain الخاصّة بك وحدها، بل أيضاً التهيئة الساكنة والإنهاء في CRT والاستدعاءات غير المباشرة في C++/CLI تدخل النطاق نفسه.
ابدأ المراجعة بسؤال هل يمكن جعل التهيئة ساكنة وهل يمكن تأجيل الباقي إلى ما بعد اكتمال التحميل. افحص حتّى أوّل وصول للعمل المؤجَّل، وإن كانت الإشعارات غير لازمة فانظر في DisableThreadLibraryCalls بعد فحص شروط CRT الساكن وTLS الساكن.
في جانب الإنهاء، افصل إلغاء تحميل الـDLL وحدها عن خروج العمليّة كلّها. إن ملك الـDLL عاملاً، اتّبع بروتوكول الإشارة وفحص الاتّساق والإنهاء، وانقل ملكيّة مؤشّرات الترابط إلى جانب الـEXE حيث أمكن. قرِّب DllMain عند خروج العمليّة من الفراغ، وضع حفظ البيانات في شيفرة إيقاف التطبيق نفسه.
flowchart TB
accTitle: ترتيب مراجعة DllMain وما تستدعيه
accDescr: افحص النطاق شاملاً لا DllMain فحسب بل التهيئة الساكنة والاستدعاءات غير المباشرة، وانقل التهيئة إلى ساكنة أو بعد التحميل، وصمِّم الإيقاف والتنظيف وفق سبب الإنهاء، ثمّ أكِّد بـ Application Verifier وملفّات التفريغ
scope["DllMain والتهيئة الساكنة والمستدعَيات"] --> timing["نقل التهيئة إلى ساكنة أو بعد التحميل"]
timing --> first["فحص أوّل وصول للعمل المؤجَّل أيضاً"]
first --> shutdown["تصميم التنظيف حسب سبب الإنهاء"]
shutdown --> verify["التأكيد بـ Verifier والتحذيرات وملفّات التفريغ"]
الشكل 13: تتبّع لا مضمون التهيئة فحسب، بل لحظة تشغيلها وعمرها عند الإنهاء، يتيح معاملة المحظورات سياسة تصميم واحدة.
السؤال عند حالة حدّيّة هو «هل هذا عمل يجوز تشغيله وقفل المحمِّل ممسوك؟». ألّا تحشو تهيئة مشكوكاً فيها في DllMain، وأن تغيِّر متى تعمل بدل ذلك، نقطة انطلاق تصميم DLL آمن.
مقالات ذات صلة
- آليّة حلّ أسماء DLL في Windows - ترتيب البحث وSxS
- تغليف DLL أصليّ بـ C++/CLI في الممارسة
- أفضل الممارسات العمليّة لتعدّد مؤشّرات الترابط: نسخة C++
- الإيقاظات الزائفة ── لماذا تستيقظ متغيّرات الشرط «دون إشعار» وكيف تنتظر بشكل صحيح على Windows
- قراءة ملفّات تفريغ الأعطال بـ WinDbg وSOS ── مدخل عمليّ إلى التحليل بعد الجمع
- أساسيّات COM STA/MTA - نماذج مؤشّرات الترابط وكيف تتجنّب التعليق
مجالات الاستشارة ذات الصلة
تتعامل شركة كومورا سوفت ذ.م.م. مع تحقيق السبب الجذريّ (تحليل ملفّات التفريغ) لتعليق وجمود عند البدء أو تحميل DLL، ومراجعة التصميم حول DllMain والتهيئة الساكنة، وإعادة عمل أغلفة C++/CLI وDLLs الإضافات نحو تصميم تهيئة آمن. نرحّب بالاستشارة حتّى من مرحلة «يتجمّد البدء في بيئة معيّنة فقط» صعبة الإعادة.
روابط مرجعيّة
-
Microsoft Learn, Dynamic-Link Library Best Practices. حول استدعاء DllMain وقفل المحمِّل ممسوك، فتكون الدوال التي يمكنها استدعاؤها مقيَّدة بشدّة؛ وكون DllMain المثاليّة كعباً فارغاً مع تأجيل التهيئة أبعد ما يمكن؛ والتوصية بالتهيئة الساكنة وقت الترجمة؛ وفعل الحدّ الأدنى فقط للإخفاقات التي يجب كشفها مبكّراً؛ وكشف أخطاء DllMain النموذجيّة بـ Application Verifier. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7
-
Microsoft Learn, DllMain entry point. حول أداء تهيئة وإنهاء بسيطين فقط في نقطة الدخول؛ ولماذا يجب عدم استدعاء LoadLibrary / FreeLibrary (ترتيب تحميل دائريّ واستخدام DLL قبل التهيئة أو بعد الإنهاء)؛ وكون Kernel32.dll مضموناً محمَّلاً فيمكن استدعاؤه في النطاق الذي لا يحمِّل DLLs أخرى؛ وعدم وجود قائمة شاملة بالدوال الآمنة؛ وتسبّب دوال User وShell وCOM بانتهاكات وصول؛ وتسلسل إشعارات DLL بحيث يسبّب التواصل مع مؤشّرات ترابط أو عمليّات أخرى جموداً؛ وسريان القيود نفسها على بنّاءات وهادات الكائنات الساكنة عند الربط مع CRT. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7
-
Microsoft Learn, Initialization of Mixed Assemblies. حول عدم تنفيذ MSIL تحت قفل المحمِّل؛ وعدم ترجمة DllMain وشجرة استدعائها إلى MSIL ومعالجة ذلك بـ #pragma unmanaged؛ وإصدار التحذير C4747 عندما تحاول DllMain تنفيذ MSIL مباشرة، بينما لا يمكن كشف التنفيذ غير المباشر عبر وحدة أخرى؛ وإمكان أن تسبّب المُهيِّئات الديناميكيّة للكائنات الساكنة المشكلة نفسها. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, DisableThreadLibraryCalls function (libloaderapi.h). حول تعطيل إشعارات DLL_THREAD_ATTACH / DLL_THREAD_DETACH لخفض الكلفة عند إنشاء مؤشّر الترابط وإتلافه؛ وعدم استدعائها من DLL مربوط مع CRT الساكن؛ وعدم أداء التحسين عندما يكون TLS الساكن (thread_local أو __declspec(thread)) سارياً. ↩ ↩2 ↩3
-
Microsoft Learn, Dynamic-Link Library Best Practices - Best Practices for Synchronization. حول البنية التي تجمد عندما تنتظر DllMain خروج مؤشّر ترابط (إشعار DLL_THREAD_DETACH عند خروج مؤشّر الترابط يحتاج قفل المحمِّل)؛ وبروتوكول إيقاف مؤشّرات الترابط عند إلغاء التحميل (أشعِر بحدث، أكِّد حالة متّسقة، ثمّ أَنهِ)؛ وDLL_PROCESS_DETACH عند خروج العمليّة حيث أُنهيت مؤشّرات الترابط الأخرى قسراً أصلاً ولا يُضمَن اتّساق فضاء العناوين والمعالج المثاليّ فارغ؛ وإنشاء مؤشّر ترابط في DllMain فيترك إشعارات معلَّقة والتهيئة غير مكتملة ويسبّب مشكلات. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8
-
Microsoft Learn, Dynamic-Link Library Best Practices - Deadlocks Caused by Lock Order Inversion. حول تعريف هرم أقفال والأخذ دائماً بالترتيب نفسه؛ وأخذ المحمِّل قفل المحمِّل قبل استدعاء DllMain لذا ينبغي أن يجلس قفل المحمِّل في قمة هرم الأقفال؛ والحفاظ على ترتيب الأخذ بين واجهات تأخذ قفل المحمِّل بصورة غير مباشرة مثل GetModuleFileName والأقفال الخاصّة؛ ومثال ملموس لجمود سببه انقلاب ترتيب الأقفال. ↩ ↩2
مقالات ذات صلة
أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.
واجهة مجمع مؤشّرات الترابط Win32 ── تزامن دون إنشاء مؤشّرات ترابط، عبر CreateThreadpoolWork
أتستدعي CreateThread في كلّ مكان في الشيفرة الأصليّة؟ دليل من المصادر الأوّليّة لواجهة مجمع مؤشّرات الترابط Win32: كائنات work وtimer وwa...
لماذا تتعطّل الوسائط ── قواعد وسائط سطر أوامر Windows
في Windows لا توجد مصفوفة وسائط؛ يصل إلى CreateProcess سلسلة واحدة ويتولّى الطرف المستقبل تقسيمها. نشرح قواعد CommandLineToArgvW وCRT و.N...
ما الذي يبقى بعد موت الأب ── تربية العمليات الابنة في كائن Job
لماذا يبقى مساعد SDK ماسكاً الكاميرا أو منفذ COM بعد إنهاء الواجهة قسراً. كيف تجعل كائن Job شجرة العمليات وحدة واحدة وتصمم عمر العملية ال...
الأنابيب المسمّاة عمليّاً ── IPC القياسيّ في Windows من التصميم إلى الأمان
دليل عمليّ للأنابيب المسمّاة، وسيلة IPC القياسيّة في Windows. يغطّي وضع البايت مقابل وضع الرسالة، وخوادم متعدّدة العملاء، وأمان ACL وانتح...
تطبيقات تتعطّل عند الاستئناف من السكون ── كيف تعمل أحداث الطاقة وكيف تبني تطبيقات أعمال تصمد أمام الاستئناف
لماذا تنقطع تطبيقات الأعمال بعد استئناف الحاسوب المحمول من السكون: إشعارات WM_POWERBROADCAST، وModern Standby، وتصميم إعادة الاتّصال، وكب...
أين يتصل هذا الموضوع
ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.
تطوير تطبيقات ويندوز
ندعم تطوير برامج ويندوز للأعمال، وتكامل الأجهزة، وأدوات التواصل.
الأسئلة الشائعة
أسئلة شائعة حول موضوع هذه المقالة.
- هل حقّاً لا يمكن فعل أيّ شيء على الإطلاق في DllMain؟
- «لا تفعل شيئاً» ليست مبالغة؛ إنّها سياسة التصميم الرسميّة، ومايكروسوفت نفسها تقول إنّ DllMain المثاليّة كعب شبه فارغ. ما هو آمن يقتصر على دوال Kernel32.dll، المضمون تحميلها بحلول وقت تشغيل DllMain، في النطاق الذي لا يحمِّل DLLs أخرى. مثلاً يمكنك إنشاء أقسام حرجة وكائنات mutex واستخدام 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.
الملف الشخصي للمؤلف
صفحة الملف الشخصي لمؤلف المقالة.
غو كومورا
مؤسّس شركة كومورا سوفت ذ.م.م.
يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.