DllMain और Loader Lock — DLL आरंभीकरण में "कुछ न करें" कहे जाने का वास्तविक कारण
· Go Komura · Windows, DLL, Windows विकास, C++, समस्या निवारण, मल्टीथ्रेडिंग, Win32 API
“ऐप स्टार्टअप पर हैंग होता है, पर केवल विशेष वातावरण में।” “अपना DLL लोड करते समय LoadLibrary कभी लौटता ही नहीं।” “केवल सेवा प्रारंभ के समय डेडलॉक होता है।” — ऐसी जाँचों को काफी दूर तक ले जाएँ तो, अक्सर, आप उसी जगह पहुँचते हैं। DLL का आरंभीकरण कोड — अर्थात् DllMain।
Microsoft का दस्तावेज़ DllMain के बारे में असामान्य रूप से तीखे स्वर में चेताता है। LoadLibrary न बुलाएँ। दूसरे थ्रेड से सिंक्रोनाइज़ न करें। User, Shell, या COM फ़ंक्शन न बुलाएँ। आदर्श DllMain खाली stub है — भाषा इतनी कड़ी क्यों है? कारण एक आंतरिक तंत्र में केंद्रित है, लोडर लॉक। Windows पर DLL, प्लग-इन, और C++/CLI रैपर लिखने वाले डेवलपरों के लिए, यह लेख प्राथमिक स्रोतों से समझाता है कि लोडर लॉक कैसे काम करता है, वह संरचना जो डेडलॉक को टिकाए रखती है, और सुरक्षित डिज़ाइन तथा जाँच प्रक्रिया।
1. निष्कर्ष पहले
DllMainलोडर लॉक पकड़े बुलाया जाता है, वह साझा लॉक जिसका प्रति प्रोसेस ठीक एक होता है। इसलिएDllMainसे ऐसा काम कॉल करना जो लोडर लॉक लेने की कोशिश करे (सीधे या अप्रत्यक्ष) डेडलॉक की संभावना बनाता है, या अभी आरंभीकृत न हुए DLL को छूने से क्रैश।1LoadLibrary/FreeLibraryकॉल वर्जित है। यह वृत्ताकार लोड-क्रम निर्भरता बनाता है और ऐसे DLL के विरुद्ध आरंभीकरण कोड चला सकता है जिसका अपना आरंभीकरण अभी नहीं चला।2- दूसरे थ्रेड से सिंक्रोनाइज़ करना भी वर्जित है। DLL सूचनाएँ क्रमबद्ध हैं, इसलिए
DllMainके भीतर थ्रेड के शुरू या निकलने की प्रतीक्षा उस थ्रेड को स्वयं लोडर लॉक की प्रतीक्षा में रोक देती है, और डेडलॉक होता है।23 - जो सुरक्षित कॉल कर सकते हैं वह व्यवहार में केवल Kernel32.dll का उपसमुच्चय है। और आधिकारिक दस्तावेज़ साफ़ कहता है कि “सुरक्षित फ़ंक्शनों की पूर्ण सूची मौजूद नहीं”। User, Shell, और COM फ़ंक्शन अन्य घटक लोड करते हैं और एक्सेस वायलेशन पैदा करते हैं।2
- CRT से लिंक DLL में, वही प्रतिबंध ग्लोबल के कंस्ट्रक्टर और डेस्ट्रक्टर पर लागू होते हैं। वे
DllMainके वास्तविक भाग के रूप में चलते हैं।2 - सही डिज़ाइन है “टालना”। जो आरंभीकरण कंपाइल समय (स्टैटिक) हो सके वह करें; जो न हो वह पहली बार उपयोग तक टालें। यही आधिकारिक सर्वोत्तम अभ्यास है।1
- C++/CLI मिश्रित DLL विशेष रूप से खतरनाक हैं। लोडर लॉक के अधीन MSIL चलने से बचने के लिए,
DllMainऔर उसका कॉल ट्री नेटिव कंपाइल होना चाहिए।4
2. DllMain कब और कैसे बुलाया जाता है
DllMain वह एंट्री पॉइंट है जिसे OS लोडर तब बुलाता है जब DLL प्रोसेस या थ्रेड में प्रवेश करता या छोड़ता है। चार सूचनाएँ हैं।
| सूचना | समय |
|---|---|
| DLL_PROCESS_ATTACH | जब DLL प्रोसेस में लोड हो |
| DLL_THREAD_ATTACH | जब प्रोसेस में नया थ्रेड शुरू हो |
| DLL_THREAD_DETACH | जब थ्रेड सामान्य रूप से निकले |
| DLL_PROCESS_DETACH | जब DLL अनलोड हो, या प्रोसेस निकले |
दो तथ्य आसानी से छूट जाते हैं। पहला, हर बार एक थ्रेड बनने पर, पहले से लोड हर DLL का DllMain DLL_THREAD_ATTACH से बुलाया जाता है। अर्थात् DllMain “मेरा DLL लोड होने पर एक बार चलने वाली चीज़” नहीं; यह कोड प्रोसेस की थ्रेड गतिविधि पर बार-बार बुलाया जाता रहता है। यदि वह न चाहिए, तो DLL_PROCESS_ATTACH के भीतर DisableThreadLibraryCalls कॉल कर रोक सकते हैं (स्टैटिक CRT से लिंक DLL से न बुलाएँ)।5
दूसरा, CRT (C/C++ रनटाइम) से लिंक DLL में, ग्लोबल और स्टैटिक C++ ऑब्जेक्ट के कंस्ट्रक्टर और डेस्ट्रक्टर CRT के एंट्री पॉइंट से DllMain के भाग के रूप में चलते हैं।2 भले आप सोचें “हमारा DllMain खाली है, इसलिए हम सुरक्षित हैं”, विस्तृत आरंभीकरण वाला ग्लोबल ऑब्जेक्ट वही काम DllMain में चलाने जैसा है।
flowchart TB
accTitle: वे चार समय जब DllMain बुलाया जाता है
accDescr: DLL लोड पर DLL_PROCESS_ATTACH चलता है; प्रोसेस में हर थ्रेड शुरू और निकास पर पहले से लोड हर DLL पर DLL_THREAD_ATTACH और DETACH चलते हैं; अनलोड या प्रोसेस निकास पर DLL_PROCESS_DETACH चलता है; और स्टैटिक ऑब्जेक्ट के कंस्ट्रक्टर भी CRT से इसके भीतर चलते हैं
load["DLL लोड"] --> pa["DLL_PROCESS_ATTACH"]
pa --> ta["DLL_THREAD_ATTACH(हर थ्रेड शुरू पर)"]
ta --> td["DLL_THREAD_DETACH(हर थ्रेड निकास पर)"]
td --> pd["DLL_PROCESS_DETACH(अनलोड या निकास पर)"]
pa -.-> crt["स्टैटिक ऑब्जेक्ट का निर्माण भी यहाँ चलता है"]
चित्र 1: DllMain केवल लोड समय नहीं, हर थ्रेड शुरू और निकास पर भी बुलाया जाता है, और स्टैटिक ऑब्जेक्ट का आरंभीकरण भी उसके भाग के रूप में चलता है।
3. लोडर लॉक — एक लॉक जो हर सूचना को क्रमबद्ध करता है
DllMain पर प्रतिबंध अकेले इतने कठोर क्यों हैं? उत्तर लोडर की संरचना में है।
DLL लोड, अनलोड, और विभिन्न सूचनाओं की श्रृंखला को सुसंगत रखने के लिए, OS लोडर काम को प्रति प्रोसेस एक लोडर लॉक से क्रमबद्ध करता है। और महत्त्वपूर्ण बिंदु यह है कि DllMain इस लोडर लॉक पकड़े बुलाया जाता है।1 जब तक आप DllMain के भीतर हैं, उस प्रोसेस का हर अन्य DLL लोड, और हर थ्रेड-शुरू सूचना, इस लॉक के छूटने की प्रतीक्षा करता है।
उस संरचना से निषेधों के कारण एक के बाद एक निकलते हैं।
LoadLibraryन बुलाएँ क्योंकि वह लोडर-लॉक पुनःप्रवेश, या वृत्ताकार लोड-क्रम निर्भरता बनाता है। वह ऐसे DLL पर फ़ंक्शन कॉल भी करा सकता है जिसका आरंभीकरण अभी पूरा नहीं।2- दूसरे थ्रेड से सिंक्रोनाइज़ करना खतरनाक है क्योंकि जिस थ्रेड की प्रतीक्षा कर रहे हैं उसके ऐसे क्षण होते हैं जब उसे लोडर लॉक चाहिए (शुरू और निकास की सूचनाएँ,
GetModuleHandle-परिवार API की कॉल, आदि)। आप लोडर लॉक पकड़े दूसरी ओर की प्रतीक्षा करते हैं; दूसरी ओर लोडर लॉक की प्रतीक्षा करती है — क्लासिक लॉक-क्रम उलटाव।6 - User, Shell, और COM फ़ंक्शन खतरनाक हैं क्योंकि वे आंतरिक रूप से अन्य सिस्टम घटक लोड करते हैं। आरंभीकरण से पहले, या तोड़ने के बाद, घटक छूते हैं और एक्सेस वायलेशन मिलता है।2
sequenceDiagram
accTitle: DllMain के भीतर थ्रेड की प्रतीक्षा डेडलॉक क्यों करती है
accDescr: लोडर लॉक पकड़े DllMain वर्कर थ्रेड के निकलने की प्रतीक्षा करता है, पर निकलने की कोशिश कर रहा वर्कर DLL_THREAD_DETACH पाने के लिए लोडर लॉक के छूटने की प्रतीक्षा करता है, इसलिए वे एक-दूसरे की प्रतीक्षा कर डेडलॉक करते हैं
participant L as लोडर(लॉक पकड़े)
participant D as DllMain
participant W as Worker
L->>D: DLL_PROCESS_DETACH
D->>W: निकास माँगें और प्रतीक्षा करें
W->>W: काम खत्म, फिर निकलें
Note over W: निकास सूचना को लॉक चाहिए
Note over D,W: DllMain लॉक पकड़े, W प्रतीक्षा करता है
चित्र 2: “DllMain थ्रेड के निकलने की प्रतीक्षा करे” संरचनात्मक डेडलॉक है, क्योंकि थ्रेड निकास को स्वयं लोडर लॉक चाहिए।
बिंदु यह है कि यह उस तरह की बात नहीं जो “भाग्य खराब हो तो होती है”; यह संरचना से तय है कि टिकेगी। दस्तावेज़ कहता है कि लोडर लॉक को ऐप द्वारा परिभाषित लॉक पदानुक्रम के शीर्ष (पहले लिया जाने वाला) मानें। DllMain के भीतर आप पहले से वह शीर्ष-स्तरीय लॉक पकड़े हैं, इसलिए वहाँ से आगे किसी और चीज़ की प्रतीक्षा करना खतरनाक है — याद रखने का उपयोगी तरीका।6
flowchart TB
accTitle: लोडर लॉक और निजी लॉक के बीच लॉक-क्रम उलटाव
accDescr: लोडर लॉक पकड़े DllMain निजी लॉक लेने जाता है, जबकि वह निजी लॉक पकड़े वर्कर GetModuleHandle आदि के लिए लोडर लॉक लेने जाता है, इसलिए अधिग्रहण क्रम उलटा होता है और डेडलॉक होता है
d["DllMain: लोडर लॉक पकड़े"] --> dg["निजी लॉक G लेने जाता है"]
w["वर्कर: निजी लॉक G पकड़े"] --> wl["लोडर लॉक लेने जाता है"]
dg -.-> dead["उल्टे अधिग्रहण क्रम से डेडलॉक"]
wl -.-> dead
wl -.-> api["GetModuleHandle आदि को आंतरिक रूप से चाहिए"]
चित्र 3: GetModuleHandle जैसी निर्दोष API भी आंतरिक रूप से लोडर लॉक माँगती है, इसलिए निजी लॉक से क्रम उलटाव टिक सकता है।
साथ ही, DllMain के भीतर से CreateThread स्वयं अनुशंसित नहीं। बनाए थ्रेड को DLL_THREAD_ATTACH सूचना संसाधित करने के लिए लोडर लॉक चाहिए, इसलिए वह तब तक चलना शुरू नहीं कर सकता जब तक वर्तमान DllMain लौटे और लॉक छोड़े। इसलिए DllMain के भीतर उस थ्रेड के शुरू या खत्म होने की प्रतीक्षा तत्काल डेडलॉक है। जीवनकाल समस्या भी है — यदि DllMain लौटने के बाद, अभी न चला थ्रेड पीछे रहते DLL अनलोड हो, तो थ्रेड का स्टार्ट एड्रेस पहले से मुक्त कोड की ओर इशारा करता है और क्रैश होता है।3
4. दो बारूदी सुरंगें जिन पर C++ डेवलपर आसानी से चलते हैं
बारूदी सुरंग 1: ग्लोबल ऑब्जेक्ट का डाइनैमिक आरंभीकरण। अध्याय 2 में कहा, स्टैटिक ऑब्जेक्ट के कंस्ट्रक्टर DllMain प्रतिबंधों के अधीन चलते हैं। कॉन्फ़िगरेशन फ़ाइल पढ़ना, लॉगिंग सुविधा खड़ी करना, COM आरंभीकरण, थ्रेड शुरू करना — जिस क्षण आप DLL में ऐसा ग्लोबल रखते हैं जिसका कंस्ट्रक्टर वह काम करे, आप “DllMain में जो नहीं करना चाहिए” निष्पादित कर रहे होते हैं। कंपाइल समय तय स्थिरांक आरंभीकरण (जो constexpr बना सकें) सुरक्षित है; फ़ंक्शन कॉल वाला आरंभीकरण टालना चाहिए।
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
चित्र 4: भले “DllMain खाली है, इसलिए हम सुरक्षित हैं”, विस्तृत आरंभीकरण वाला ग्लोबल आते ही वही खतरा जीवित हो जाता है।
बारूदी सुरंग 2: C++/CLI (मिश्रित असेंबली)। नेटिव DLL को C++/CLI से लपेटने वाले कॉन्फ़िगरेशन में (रैपर लेख में वर्णित रूप), लोडर लॉक के अधीन MSIL (प्रबंधित कोड) चलने का खतरा है। MSIL चलाना CLR आरंभीकरण या दूसरी असेंबली का लोड ट्रिगर कर सकता है। कंपाइलर उन कोड पर चेतावनी C4747 देता है जहाँ DllMain सीधे MSIL चलाए, पर दूसरे मॉड्यूल की फ़ंक्शन से अप्रत्यक्ष निष्पादन नहीं पकड़ सकता। DllMain और उससे बुलाई फ़ंक्शन को #pragma unmanaged से नेटिव कंपाइल करें, या ऐसा कॉन्फ़िगरेशन उपयोग करें जिसमें DllMain हो ही नहीं।4
flowchart TB
accTitle: लोडर लॉक के अधीन MSIL निष्पादन पकड़ा जा सकता है या नहीं
accDescr: जहाँ DllMain सीधे MSIL चलाए वह कोड कंपाइलर चेतावनी C4747 से पकड़ सकता है, पर दूसरे मॉड्यूल की फ़ंक्शन से अप्रत्यक्ष निष्पादन नहीं, इसलिए कॉल ट्री की समीक्षा और नेटिव कंपाइल पर ज़ोर से रोकना पड़ता है
d2["DllMain से कॉल"] --> dir["सीधे MSIL चलाएँ"]
d2 --> ind["दूसरे मॉड्यूल से चलाएँ"]
dir --> c47["चेतावनी C4747 से पकड़ने योग्य"]
ind --> nc["कंपाइलर नहीं पकड़ सकता"]
nc -.-> rv["समीक्षा और pragma unmanaged से रोकें"]
चित्र 5: C4747 केवल प्रत्यक्ष निष्पादन से बचाता है। अप्रत्यक्ष पथ केवल समीक्षा से पकड़े जाते हैं।
5. सही डिज़ाइन — “टालना” को डिफ़ॉल्ट नीति बनाएँ
आधिकारिक सर्वोत्तम-अभ्यास अनुशंसा स्पष्ट है।1
- जो आरंभीकरण कंपाइल समय (स्टैटिक) हो सके वह पूरा करें। पहले पूछें कि क्या डाइनैमिक आरंभीकरण स्टैटिक से बदला जा सकता है।
- बाकी पहली बार उपयोग तक टालें। जब तक पहली बार उपयोग DLL लोड पूरा होने के बाद बुलाई साधारण API से हो, आरंभीकरण लोडर लॉक के बाहर चलता है और लगभग पूरी Windows API सुरक्षित उपयोग कर सकते हैं। पहली पहुँच पर बहिष्करण के लिए
INIT_ONCE(one-time initialization) या C++ magic statics (फ़ंक्शन-लोकल स्टैटिक) उपयोग कर सकते हैं। टालना रामबाण नहीं — यदि वह पहली पहुँच स्वयंDllMainया स्टैटिक इनिशियलाइज़र से हो, तो इनिशियलाइज़र फिर लोडर लॉक के अधीन चलता है और वही प्रतिबंध लौटते हैं। - केवल उन विफलताओं के लिए अपवाद करें जिन्हें जल्दी पकड़ना हो। यह आवश्यकता हो सकती है कि टूटी कॉन्फ़िगरेशन फ़ाइल से लोड स्वयं विफल हो। तब भी, “कोशिश करें और तुरंत विफल हों” के न्यूनतम तक रखें।
- DLL_PROCESS_ATTACH में
DisableThreadLibraryCallsपर विचार करें। यदि DLL थ्रेड सूचनाएँ उपयोग न करे, तो सूचना लागत स्वयं हटा सकते हैं (स्टैटिक CRT या स्टैटिक TLS उपयोग को छोड़कर)।5 - Application Verifier से जाँचें।
DllMainके भीतर कई खतरनाक कॉल वे हैं जिन्हें Application Verifier रन टाइम पर पकड़ता है।1
flowchart TB
accTitle: DLL आरंभीकरण के लिए डिज़ाइन मार्गदर्शन
accDescr: पहले सोचें कि आरंभीकरण कंपाइल-समय स्टैटिक हो सकता है या नहीं; यदि नहीं, तो डिफ़ॉल्ट पहली बार उपयोग तक टालना है, और DllMain में केवल वह न्यूनतम छोड़ें जिसे लोड विफलता के रूप में जल्दी पकड़ना हो
q1{"कंपाइल समय तय हो सकता है?"} -->|"हाँ"| s["स्टैटिक आरंभीकरण बनाएँ"]
q1 -->|"नहीं"| q2{"विफलता लोड समय पकड़नी है?"}
q2 -->|"नहीं"| lazy["पहली बार उपयोग तक टालें(डिफ़ॉल्ट)"]
q2 -->|"हाँ"| min["DllMain में केवल न्यूनतम करें"]
lazy -.-> once["INIT_ONCE या फ़ंक्शन-लोकल स्टैटिक से बहिष्कृत करें"]
चित्र 6: निर्णय क्रम है “क्या स्टैटिक हो सकता है → क्या टाला जा सकता है”, और DllMain में जो छोड़ते हैं वह केवल वह न्यूनतम है जिसे जल्दी पकड़ना हो।
DisableThreadLibraryCalls लागू करें या नहीं, निम्नलिखित शाखा से यंत्रवत तय हो सकता है।
flowchart TB
accTitle: DisableThreadLibraryCalls कॉल करें या नहीं
accDescr: स्टैटिक CRT से लिंक DLL से न बुलाएँ; यदि स्टैटिक TLS प्रभावी हो तो कॉल स्वयं विफल होती है इसलिए न बुलाएँ; यदि दोनों न हों और DLL थ्रेड सूचनाएँ उपयोग न करे, तो DLL_PROCESS_ATTACH में रिटर्न वैल्यू जाँच कर बुलाएँ ताकि सूचना लागत कटे
q1{"स्टैटिक CRT से लिंक?"} -->|"हाँ"| no2["नहीं बुलाना चाहिए"]
q1 -->|"नहीं"| q2{"स्टैटिक TLS उपयोग?"}
q2 -->|"हाँ"| eff["कॉल वैसे भी विफल(FALSE)"]
q2 -->|"नहीं"| q3{"थ्रेड सूचनाएँ चाहिए?"}
q3 -->|"नहीं"| yes["ATTACH में बुलाएँ(रिटर्न जाँचें)"]
q3 -->|"हाँ"| keep["न बुलाएँ; सूचनाएँ सँभालें"]
चित्र 7: स्टैटिक CRT, स्टैटिक TLS, और सूचनाएँ चाहिए या नहीं — ये तीन शर्तें अद्वितीय तय करती हैं कि बुलाना चाहिए या नहीं।
अनलोड पर थ्रेड रोकने के लिए आधिकारिक दस्तावेज़ ठोस प्रोटोकॉल देता है। DLL_PROCESS_DETACH में वर्कर थ्रेड के निकलने की “प्रतीक्षा” करने के बजाय (FreeLibrary से अनलोड पर), रूप है (1) इवेंट से निकास संकेत दें, (2) थ्रेड पक्ष काम को सुसंगत अवस्था तक समेटे, वापस संकेत दे, और अनंत प्रतीक्षा में प्रवेश करे, (3) DllMain पक्ष सुसंगत अवस्था पुष्टि कर फिर TerminateThread से थ्रेड समेटे।3 यह कठोर लगता है, पर “आप DllMain के भीतर थ्रेड के स्वाभाविक निकास की प्रतीक्षा नहीं कर सकते” की बाधा के भीतर यथार्थवादी उत्तर के रूप में दस्तावेज़ीकृत है।
sequenceDiagram
accTitle: अनलोड पर थ्रेड रोकने का प्रोटोकॉल
accDescr: DllMain वर्कर थ्रेड को इवेंट से निकलने का संकेत देता है; वर्कर काम को सुसंगत अवस्था तक समेटता है, वापस संकेत देता है, और अनंत प्रतीक्षा में जाता है; DllMain सुसंगत अवस्था पुष्टि कर फिर थ्रेड समाप्त करता है
participant D as DllMain(DETACH सँभाल)
participant W as वर्कर थ्रेड
D->>W: इवेंट से निकास संकेत
W->>W: काम को सुसंगत अवस्था तक समेटें
W->>D: सुसंगति पूर्ण संकेत दें और हमेशा प्रतीक्षा
D->>W: TerminateThread से समाप्त करें
Note over D,W: स्वाभाविक निकास की प्रतीक्षा नहीं, इसलिए डेडलॉक नहीं
चित्र 8: “स्वाभाविक निकास की प्रतीक्षा” के बजाय, “सुसंगति संकेत की प्रतीक्षा कर फिर काटें” लोडर लॉक से टकराव बचाता है।
पहले सिद्धांतों के रूप में, सबसे सुरक्षित डिज़ाइन है अनलोड हो सकने वाले DLL में थ्रेड का स्वामित्व न रखना, और थ्रेड स्वामित्व EXE पक्ष पर रखना।
प्रोसेस निकास पर DLL_PROCESS_DETACH उलटा है: कुछ न कर लौटना आदर्श है। इस बिंदु तक हर अन्य थ्रेड पहले ही जबरन समाप्त हो चुका होता है, और निर्भर DLL या रनटाइम की अवस्था पर भी भरोसा नहीं कर सकते। यहाँ विस्तृत काम केवल डेडलॉक और क्रैश पैदा करता है। जो डेटा टिकना चाहिए उसे ऐप के अपने शटडाउन पथ में लिखें; इस सूचना पर निर्भर न हों।3
6. जब इसमें फँसें तो जाँच कैसे करें
लोडर-लॉक हैंग की पहचानने योग्य अंगुली-छाप होती है।
हैंग डंप में स्टैक देखें। जमे क्षण का डंप लें और हर थ्रेड का स्टैक जाँचें। यदि आपको ntdll.dll लोडर फ़ंक्शनों (जिनके नाम Ldr से शुरू) के भीतर लॉक की प्रतीक्षा कर रहा थ्रेड, और DllMain या स्टैटिक इनिशियलाइज़र (dynamic initializer) के भीतर किसी और चीज़ की प्रतीक्षा कर रहा थ्रेड — यह जोड़ा मिले, तो लगभग निश्चित हैं। LoadLibrary कॉल के बीच रुका थ्रेड दूसरा विशिष्ट चरित्र है।
flowchart TB
accTitle: लोडर-लॉक हैंग की अंगुली-छाप
accDescr: हैंग डंप में, यदि ntdll लोडर फ़ंक्शनों के भीतर लॉक की प्रतीक्षा कर रहा थ्रेड और DllMain या स्टैटिक इनिशियलाइज़र के भीतर किसी और की प्रतीक्षा कर रहा थ्रेड दोनों मिलें, तो लगभग निश्चित रूप से लोडर-लॉक डेडलॉक मान सकते हैं
dump["हैंग डंप"] --> t1["Ldr-परिवार फ़ंक्शन में लॉक की प्रतीक्षा"]
dump --> t2["DllMain या स्टैटिक इनिशियलाइज़र में प्रतीक्षा"]
t1 --> pair{"दोनों मौजूद?"}
t2 --> pair
pair -->|"हाँ"| conf["लगभग निश्चित लोडर-लॉक डेडलॉक"]
pair -->|"नहीं"| other["दूसरे प्रकार के हैंग के रूप में जाँचें"]
चित्र 9: लोडर-लॉक हैंग की पहचानने योग्य अंगुली-छाप है “Ldr में प्रतीक्षा + DllMain के भीतर प्रतीक्षा”।
“समय-निर्भर” चरित्र पर संदेह करें। लोडर-लॉक डेडलॉक केवल उस क्षण टिकता है जब DLL लोड थ्रेड शुरू या निकास से मेल खाए। “स्टार्टअप पर कभी-कभी”, “केवल विशेष मशीन पर”, और “केवल सेवा के रूप में चलाने पर” जैसी पुनरुत्पादन स्थितियाँ इस तरह की समस्या के संकेत हैं।
निवारक जाँच चलाएँ। Application Verifier सक्षम कर परीक्षण चलाएँ, और DllMain के भीतर खतरनाक कॉल रन टाइम पर पकड़ सकते हैं।1 C++/CLI के लिए, चेतावनी C4747 अनदेखी न करें; DllMain से पहुँचने योग्य फ़ंक्शनों की समीक्षा में “अप्रत्यक्ष LoadLibrary कॉल करने वाली फ़ंक्शन” (COM आरंभीकरण, कुछ CRT सुविधाएँ, delay-loaded आयात, आदि) का कोण चेकलिस्ट में जोड़ें, तो शिप से पहले दुर्घटनाएँ पकड़ेंगे। delay-loaded आयात की पहली कॉल का आंतरिक रूप से LoadLibrary बन जाना आसानी से छूटने वाला बिंदु है।
7. सारांश
DllMainलोडर लॉक (प्रति प्रोसेस एक, हर DLL सूचना क्रमबद्ध करने वाला लॉक) पकड़े बुलाया जाता है। हर प्रतिबंध उसी से निकलता है।- निषेधों का केंद्र है “
LoadLibrary/FreeLibraryन बुलाएँ”, “दूसरे थ्रेड से सिंक्रोनाइज़ न करें”, और “Kernel32 के अलावा DLL पर निर्भर फ़ंक्शन न बुलाएँ”। CRT से चलने वाले स्टैटिक ऑब्जेक्ट के कंस्ट्रक्टर और डेस्ट्रक्टर उन्हीं प्रतिबंधों के अधीन हैं। - बुनियादी डिज़ाइन नीति टालना है। जो स्टैटिक बना सकें वह स्टैटिक करें; बाकी पहली बार उपयोग तक टालें।
DisableThreadLibraryCallsऔर Application Verifier उपयोग करें। - अनलोड पर थ्रेड रोकना आधिकारिक प्रोटोकॉल मानता है (संकेत → सुसंगति पुष्टि → समाप्त)। प्रोसेस निकास पर DLL_PROCESS_DETACH आदर्शतः खाली है।
- C++/CLI में, लोडर लॉक के अधीन MSIL चलाना अपनी बारूदी सुरंग है।
DllMainकॉल ट्री के नेटिव कंपाइल पर ज़ोर दें।
DllMain प्रतिबंध पहले देखने में निषेधों की अनुचित सूची लगते हैं। पर एक बार यह एक बिंदु पकड़ लें कि “यह शीर्ष-स्तरीय लॉक, लोडर लॉक, पकड़े बुलाया जाता है”, हर निषेध उसी सिद्धांत का पुनर्कथन है। इसे सिद्धांत के रूप में याद रखें, और जब दस्तावेज़ में न हो ऐसा किनारा मिले, तब भी सही प्रश्न पूछ सकेंगे: “क्या यह काम मैं लॉक पकड़े करने का अधिकार रखता हूँ?”
संबंधित लेख
- Windows DLL नाम समाधान कैसे काम करता है — खोज क्रम और SxS
- C# से नेटिव DLL कॉल करना: C++/CLI रैपर बनाम P/Invoke
- व्यावहारिक मल्टीथ्रेडिंग सर्वोत्तम अभ्यास: C++ संस्करण — RAII और jthread से संरचना द्वारा दुर्घटनाएँ हटाना
- स्प्यूरियस वेकअप — Condition Variable “बिना सूचना के” क्यों जागती है और Windows पर सही प्रतीक्षा
- WinDbg + SOS से क्रैश डंप पढ़ना — संग्रह के बाद विश्लेषण की व्यावहारिक मार्गदर्शिका
- COM STA/MTA की बुनियाद — थ्रेडिंग मॉडल और हैंग से बचना
संबंधित परामर्श क्षेत्र
KomuraSoft LLC स्टार्टअप या DLL लोड समय हैंग और डेडलॉक की मूल-कारण जाँच (डंप विश्लेषण), DllMain और स्टैटिक आरंभीकरण के आसपास डिज़ाइन समीक्षा, तथा C++/CLI रैपर और प्लग-इन DLL को सुरक्षित आरंभीकरण डिज़ाइन की ओर सुधारना सँभालता है। “केवल विशेष वातावरण में स्टार्टअप पर हैंग होता है” के कठिन-पुनरुत्पादन चरण पर भी परामर्श कर सकते हैं।
संदर्भ लिंक
-
Microsoft Learn, Dynamic-Link Library Best Practices. DllMain के लोडर लॉक पकड़े बुलाए जाने पर, जिससे कॉल कर सकने वाली फ़ंक्शन कठोर सीमित हैं; आदर्श DllMain के खाली stub होने और आरंभीकरण यथासंभव टाले जाने पर; कंपाइल-समय स्टैटिक आरंभीकरण की अनुशंसा पर; जल्दी पकड़नी वाली विफलताओं के लिए केवल न्यूनतम करने पर; और Application Verifier से विशिष्ट DllMain गलतियाँ पकड़ने पर। ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
Microsoft Learn, DllMain entry point. एंट्री पॉइंट में केवल सरल आरंभीकरण और समाप्ति करने पर; LoadLibrary / FreeLibrary न बुलाने के कारण (वृत्ताकार लोड क्रम और आरंभीकरण से पहले या समाप्ति के बाद DLL उपयोग); Kernel32.dll के पहले से लोड गारंटीशुदा होने पर, ताकि अन्य DLL लोड न करने की सीमा में कॉल कर सकें; सुरक्षित फ़ंक्शनों की पूर्ण सूची न होने पर; User, Shell, और COM फ़ंक्शन के एक्सेस वायलेशन पैदा करने पर; DLL सूचनाओं के क्रमबद्ध होने पर, जिससे अन्य थ्रेड या प्रोसेस से संचार डेडलॉक करता है; और CRT लिंक होने पर स्टैटिक ऑब्जेक्ट के कंस्ट्रक्टर और डेस्ट्रक्टर पर वही प्रतिबंध लागू होने पर। ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7
-
Microsoft Learn, Dynamic-Link Library Best Practices - Best Practices for Synchronization. DllMain के भीतर थ्रेड के निकलने की प्रतीक्षा करने पर डेडलॉक करने वाली संरचना पर (थ्रेड निकास की DLL_THREAD_DETACH सूचना को लोडर लॉक चाहिए); अनलोड पर थ्रेड रोकने के प्रोटोकॉल पर (इवेंट से संकेत, सुसंगत अवस्था पुष्टि, फिर समाप्त); प्रोसेस निकास पर DLL_PROCESS_DETACH के अन्य थ्रेड पहले से जबरन समाप्त और एड्रेस-स्पेस सुसंगति की गारंटी न होने पर, जिससे आदर्श हैंडलर खाली है; और DllMain में थ्रेड बनाने के अधूरे आरंभीकरण के साथ सूचनाएँ कतार में छोड़ने और समस्या पैदा करने पर। ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Initialization of Mixed Assemblies. लोडर लॉक के अधीन MSIL न चलाने पर; DllMain और उसके कॉल ट्री को MSIL में न कंपाइल करने और #pragma unmanaged से निपटने पर; DllMain के सीधे MSIL चलाने की कोशिश पर चेतावनी C4747 दिए जाने, पर दूसरे मॉड्यूल से अप्रत्यक्ष निष्पादन न पकड़े जाने पर; और स्टैटिक ऑब्जेक्ट के डाइनैमिक इनिशियलाइज़र के वही समस्या पैदा कर सकने पर। ↩ ↩2
-
Microsoft Learn, DisableThreadLibraryCalls function (libloaderapi.h). थ्रेड निर्माण और नाश पर ओवरहेड घटाने के लिए DLL_THREAD_ATTACH / DLL_THREAD_DETACH सूचनाएँ अक्षम करने पर; स्टैटिक CRT से लिंक DLL से न बुलाने पर; और स्टैटिक TLS (thread_local या __declspec(thread)) प्रभावी होने पर अनुकूलन न किए जाने पर। ↩ ↩2
-
Microsoft Learn, Dynamic-Link Library Best Practices - Deadlocks Caused by Lock Order Inversion. लॉक पदानुक्रम परिभाषित कर हमेशा उसी क्रम में अधिग्रहण करने पर; लोडर के DllMain बुलाने से पहले लोडर लॉक लेने पर, जिससे लोडर लॉक लॉक पदानुक्रम के शीर्ष पर बैठना चाहिए; GetModuleFileName जैसी अप्रत्यक्ष लोडर लॉक लेने वाली API और निजी लॉक के बीच अधिग्रहण क्रम देखने पर; और लॉक-क्रम उलटाव से डेडलॉक के ठोस उदाहरण पर। ↩ ↩2
संबंधित लेख
निकटवर्ती विषयों में गहराई से जाने के लिए समान टैग वाले नवीनतम लेख।
Win32 Thread Pool API — CreateThreadpoolWork से थ्रेड बनाए बिना समवर्तिता
क्या आपके नेटिव कोड में CreateThread कॉल बिखरी पड़ी हैं? यह लेख Vista में पुनर्डिज़ाइन की गई Win32 थ्रेड पूल API — work, timer, wait और i...
"Not Responding" वास्तव में क्या है — Windows ऐप के हैंग का निर्णय कैसे करता है, और न हैंग करने वाले ऐप कैसे डिज़ाइन करें
Windows का "Not Responding" वह तंत्र है जिसमें OS तय करता है कि विंडो ने 5 सेकंड संदेश नहीं लिया और उसे घोस्ट विंडो से बदल देता है। यह ले...
स्प्यूरियस वेकअप — Condition Variable "बिना सूचना के" क्यों जागती है और Windows पर सही प्रतीक्षा कैसे करें
कंडीशन वेरिएबल की प्रतीक्षा सूचना आए बिना भी लौट सकती है (स्प्यूरियस वेकअप)। यह लेख Windows इम्प्लीमेंटेशन से समझाता है कि विनिर्देश इसे ...
Named Pipes व्यवहार में — डिज़ाइन से सुरक्षा तक, Windows की मानक IPC
Named pipes — Windows की मानक इंटर-प्रोसेस कम्युनिकेशन — की व्यावहारिक मार्गदर्शिका। यह लेख प्राथमिक स्रोतों से बाइट और मैसेज मोड का चुना...
स्लीप से रिज़्यूम पर टूटने वाले ऐप — Windows पावर इवेंट कैसे काम करते हैं और उनसे बचने वाले व्यावसायिक ऐप कैसे बनाएँ
लैपटॉप खोला और व्यावसायिक ऐप के कनेक्शन मर चुके थे — कारण वह डिज़ाइन है जिसने स्लीप का हिसाब ही नहीं रखा। यह लेख WM_POWERBROADCAST सूचना ...
संबंधित विषय
ये पृष्ठ विषय को सेवाओं और निर्णयों के व्यापक संदर्भ में रखते हैं।
Windows के तकनीकी विषय
Windows विकास, बग जाँच और मौजूदा संपत्तियों के उपयोग का प्रवेश-द्वार।
इस विषय से जुड़ी सेवाएँ
यह लेख निम्नलिखित सेवाओं से सीधे जुड़ा है।
Windows ऐप विकास
व्यावसायिक ऐप, डिवाइस एकीकरण और संचार उपकरण, आवश्यकताओं से विकास तक।
अक्सर पूछे जाने वाले प्रश्न
इस लेख के विषय पर परामर्श में अक्सर पूछे जाने वाले प्रश्न।
- क्या DllMain में सचमुच कुछ भी नहीं कर सकते?
- "कुछ न करें" अतिशयोक्ति नहीं; यह आधिकारिक डिज़ाइन रुख है, और Microsoft स्वयं कहता है कि आदर्श DllMain लगभग खाली stub है। सुरक्षित है Kernel32.dll फ़ंक्शनों का वह उपसमुच्चय — DllMain चलने तक Kernel32 लोड होना गारंटीशुदा है — जो अन्य DLL लोड न करे। क्रिटिकल सेक्शन या 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 सूचनाएँ न चाहिए हों, तो DLL_PROCESS_ATTACH में DisableThreadLibraryCalls कॉल करने से प्रति-थ्रेड-निर्माण और प्रति-थ्रेड-निकास सूचनाएँ रुकती हैं और बार-बार थ्रेड बनाने वाले प्रोसेस में ओवरहेड घटता है। दो अपवाद हैं। स्टैटिक CRT से लिंक DLL से इसे न बुलाएँ (स्टैटिक CRT को थ्रेड सूचनाएँ चाहिए)। और यदि thread_local या __declspec(thread) से स्टैटिक TLS प्रभावी हो, तो कॉल स्वयं विफल होकर FALSE लौटाती है, इसलिए रिटर्न वैल्यू जाँचने की आदत बनाएँ। डाइनैमिकली लिंक CRT उपयोग करने वाले विशिष्ट DLL पर, यह पुष्टि कर कि थ्रेड सूचनाओं पर कुछ निर्भर नहीं, इसे उपयोग करें।
- C++/CLI (मिश्रित प्रबंधित) DLL स्टार्टअप पर क्यों हैंग होता है?
- विशिष्ट कारण है लोडर लॉक पकड़े रहते MSIL (प्रबंधित कोड) चलाने की कोशिश। C++/CLI मिश्रित असेंबली में, यदि DllMain, उससे बुलाई फ़ंक्शन, या ग्लोबल के डाइनैमिक इनिशियलाइज़र MSIL में कंपाइल हों, तो लोडर लॉक के अधीन CLR आरंभीकरण या दूसरी असेंबली का लोड चाहिए पड़ सकता है, और वह डेडलॉक कर सकता है। जब DllMain स्वयं सीधे MSIL चलाने की कोशिश करे तो कंपाइलर चेतावनी C4747 देता है, पर दूसरे मॉड्यूल से अप्रत्यक्ष निष्पादन नहीं पकड़ सकता। उपाय है DllMain और उसके कॉल ट्री को #pragma unmanaged से नेटिव कंपाइल करना — या DllMain रखना ही नहीं।
- क्या DLL_PROCESS_DETACH में संसाधन साफ़ करूँ?
- उत्तर "प्रोसेस निकास" और "FreeLibrary से अनलोड" के बीच बदलता है। प्रोसेस निकास पर DLL_PROCESS_DETACH में अन्य थ्रेड पहले ही समाप्त हो चुके होते हैं, और एड्रेस स्पेस अब भी सुसंगत हो इसकी गारंटी नहीं, इसलिए मेमोरी मुक्त करने जैसी सफ़ाई वास्तव में खतरनाक है; आधिकारिक मार्गदर्शन है कि "आदर्श हैंडलर खाली है"। जो डेटा टिकना चाहिए उसे ऐप के अपने शटडाउन पथ में लिखें, और यहाँ मूलतः कुछ न करें और लौटें। FreeLibrary से अनलोड पर प्रोसेस चलता रहता है, इसलिए पूर्ण सफ़ाई चाहिए — थ्रेड रोकना, हैंडल बंद करना, आदि। पर DllMain के भीतर थ्रेड के निकलने की प्रतीक्षा डेडलॉक करती है, इसलिए आधिकारिक प्रोटोकॉल मानें: संकेत दें, सुसंगत अवस्था तक प्रतीक्षा करें, और काम DllMain के बाहर पूरा करें।