DllMain ও লোডার লক — DLL ইনিশিয়ালাইজেশনে "কিছুই করবেন না" বলা হয় তার আসল কারণ

· · Windows, DLL, Windows ডেভেলপমেন্ট, C++, সমস্যা সমাধান, মাল্টিথ্রেডিং, Win32 API

“অ্যাপ স্টার্টআপে হ্যাং করে, কিন্তু শুধু নির্দিষ্ট পরিবেশে।” “নিজের DLL লোড করলে LoadLibrary মাঝে মাঝে ফেরেই না।” “শুধু সার্ভিস শুরুর সময়ে ডেডলক হয়।” — এমন অনুসন্ধান যথেষ্ট দূর পর্যন্ত ধরলে, প্রায়ই একই জায়গায় পৌঁছান। DLL-এর ইনিশিয়ালাইজেশন কোড — অর্থাৎ DllMain

Microsoft-এর ডকুমেন্টেশন DllMain নিয়ে অস্বাভাবিক তীব্র সুরে সতর্ক করে। LoadLibrary ডাকবেন না। অন্য থ্রেডের সাথে সিঙ্ক্রোনাইজ করবেন না। User, Shell বা COM ফাংশন ডাকবেন না। আদর্শ DllMain খালি স্টাব — ভাষা এত তীব্র কেন? কারণ এক অভ্যন্তরীণ যন্ত্রে কেন্দ্রীভূত, লোডার লক। Windows-এ DLL, প্লাগ-ইন ও C++/CLI র‍্যাপার লেখা ডেভেলপারদের লক্ষ্য করে এই নিবন্ধ প্রাথমিক উৎস থেকে ব্যাখ্যা করে লোডার লক কীভাবে কাজ করে, যে কাঠামো ডেডলক ধরে রাখে, আর নিরাপদ ডিজাইন ও অনুসন্ধান পদ্ধতি।

১. আগে উপসংহার

  • DllMain লোডার লক ধরে ডাকা হয়, প্রতি প্রক্রিয়ায় ঠিক একটি এমন শেয়ার্ড লক। তাই DllMain থেকে এমন কাজ ডাকা যা লোডার লক নিতে চায় (সরাসরি বা পরোক্ষে) ডেডলকের সম্ভাবনা তৈরি করে, অথবা এখনও ইনিশিয়ালাইজ না হওয়া DLL ছুঁয়ে ক্র্যাশ।1
  • LoadLibrary / FreeLibrary ডাকা নিষিদ্ধ। বৃত্তাকার লোড-অর্ডার নির্ভরতা তৈরি করে এবং যে DLL-এর নিজের ইনিশিয়ালাইজেশন এখনও চলেনি তার বিপরীতে ইনিশিয়ালাইজেশন কোড চালাতে পারে।2
  • অন্য থ্রেডের সাথে সিঙ্ক্রোনাইজও নিষিদ্ধ। DLL নোটিফিকেশন সিরিয়ালাইজড, তাই DllMain-এর ভেতরে থ্রেড শুরু বা এক্সিটের অপেক্ষা সেই থ্রেডকেই লোডার লকের অপেক্ষায় থামিয়ে রাখে, আর ডেডলক হয়।23
  • নিরাপদে যা ডাকা যায় তা কার্যত শুধু Kernel32.dll-এর একটি উপসেট। আর অফিসিয়াল ডকুমেন্টেশন স্পষ্ট বলে “নিরাপদ ফাংশনের সম্পূর্ণ তালিকা নেই”। User, Shell ও COM ফাংশন অন্য কম্পোনেন্ট লোড করে অ্যাক্সেস ভায়োলেশন ঘটায়।2
  • CRT-এর সাথে লিংক করা DLL-এ গ্লোবালের কনস্ট্রাক্টর ও ডেস্ট্রাক্টরে একই সীমাবদ্ধতা প্রযোজ্য। সেগুলো DllMain-এর কার্যত অংশ হিসেবে চলে।2
  • সঠিক ডিজাইন “স্থগিত রাখা”। যে ইনিশিয়ালাইজেশন কম্পাইল সময়ে (স্ট্যাটিকভাবে) করা যায় করুন; যা যায় না তা প্রথম ব্যবহার পর্যন্ত স্থগিত রাখুন। সেটাই অফিসিয়াল বেস্ট প্র্যাকটিস।1
  • C++/CLI মিক্সড DLL বিশেষ বিপজ্জনক। লোডার লকের নিচে MSIL না চালাতে DllMain ও তার কল ট্রি নেটিভ কম্পাইল করতে হয়।4

২. 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-এ চালানোর সমান।

DllMain যে চার সময়ে ডাকা হয়DLL লোডে DLL_PROCESS_ATTACH চলে; প্রক্রিয়ায় প্রতি থ্রেড শুরু ও এক্সিটে ইতিমধ্যে-লোড প্রতিটি DLL-এ DLL_THREAD_ATTACH ও DETACH চলে; আনলোড বা প্রক্রিয়া এক্সিটে DLL_PROCESS_DETACH চলে; আর স্ট্যাটিক অবজেক্টের কনস্ট্রাক্টরও CRT দিয়ে এর ভেতরে চলেDLL লোডDLL_PROCESS_ATTACHDLL_THREAD_ATTACH (প্রতি থ্রেড শুরুতে)DLL_THREAD_DETACH (প্রতি থ্রেড এক্সিটে)DLL_PROCESS_DETACH (আনলোড বা এক্সিটে)স্ট্যাটিক অবজেক্টের কনস্ট্রাকশনও এখানে চলে

চিত্র ১: DllMain শুধু লোড সময়ে নয়, প্রতি থ্রেড শুরু ও এক্সিটেও ডাকা হয়, আর স্ট্যাটিক অবজেক্টের ইনিশিয়ালাইজেশনও তার অংশ হিসেবে চলে।

৩. লোডার লক — যে একটি লক প্রতিটি নোটিফিকেশন সিরিয়ালাইজ করে

DllMain-এর সীমাবদ্ধতা একা এত কঠোর কেন? উত্তর লোডারের কাঠামোতে।

DLL লোড, আনলোড ও বিভিন্ন নোটিফিকেশনের এক ধারাবাহিক অপারেশন সঙ্গতিপূর্ণ রাখতে OS লোডার প্রতি প্রক্রিয়ায় একটি লোডার লক দিয়ে কাজ সিরিয়ালাইজ করে। আর গুরুত্বপূর্ণ বিষয়: DllMain এই লোডার লক ধরে ডাকা হয়1 আপনি DllMain-এর ভেতরে থাকা পর্যন্ত সেই প্রক্রিয়ার অন্য প্রতিটি DLL লোড, আর প্রতিটি থ্রেড-শুরুর নোটিফিকেশন, এই লক ছাড়ার অপেক্ষা করে।

সেই কাঠামো থেকে নিষেধাজ্ঞার কারণ একের পর এক আসে।

  • LoadLibrary ডাকবেন না কারণ তা লোডার-লক রিএন্ট্রান্সি, অথবা বৃত্তাকার লোড-অর্ডার নির্ভরতা তৈরি করে। ইনিশিয়ালাইজেশন এখনও শেষ না হওয়া DLL-এর ফাংশন ডাকার ফলও হতে পারে।2
  • অন্য থ্রেডের সাথে সিঙ্ক্রোনাইজ বিপজ্জনক কারণ আপনি যে থ্রেডের অপেক্ষা করছেন তার এমন মুহূর্ত আছে যখন তার লোডার লক দরকার (শুরু ও এক্সিটের নোটিফিকেশন, GetModuleHandle-পরিবারের API ডাকা ইত্যাদি)। আপনি লোডার লক ধরে পক্ষের অপেক্ষা করেন; পক্ষ লোডার লকের অপেক্ষা করে — ক্লাসিক লক-অর্ডার ইনভার্সন।6
  • User, Shell ও COM ফাংশন বিপজ্জনক কারণ সেগুলো অভ্যন্তরে অন্য সিস্টেম কম্পোনেন্ট লোড করে। ইনিশিয়ালাইজেশনের আগে, অথবা ভেঙে ফেলার পরে, কম্পোনেন্ট ছুঁয়ে অ্যাক্সেস ভায়োলেশন পান।2
DllMain-এর ভেতরে থ্রেডের অপেক্ষা কেন ডেডলক করেলোডার লক ধরে DllMain ওয়ার্কার থ্রেড এক্সিটের অপেক্ষা করে, কিন্তু যে ওয়ার্কার বেরোতে চাইছে সে DLL_THREAD_DETACH পেতে লোডার লক ছাড়ার অপেক্ষা করে, তাই পরস্পর অপেক্ষা করে ডেডলক হয়ওয়ার্কারDllMainলোডার(লক ধরে)ওয়ার্কারDllMainলোডার(লক ধরে)এক্সিট নোটিফাই-এর লক দরকারDllMain লক ধরে, W অপেক্ষা করেDLL_PROCESS_DETACHএক্সিট অনুরোধ ও অপেক্ষাকাজ শেষ, তারপর এক্সিট

চিত্র ২: “DllMain থ্রেড এক্সিটের অপেক্ষা করে” কাঠামোগত ডেডলক, কারণ থ্রেড এক্সিট নিজেই লোডার লক চায়।

বিষয়টি “দুর্ভাগ্য হলে ঘটে” ধরনের নয়; কাঠামোগতভাবে ধরে রাখা নিশ্চিত। ডকুমেন্টেশন বলে লোডার লককে অ্যাপ যে লক হায়ারার্কি সংজ্ঞায়িত করে তার শীর্ষ (যা আগে নেওয়া হয়) হিসেবে ধরুন। DllMain-এর ভেতরে আপনি ইতিমধ্যে সেই শীর্ষ-স্তরের লক ধরে আছেন, তাই সেখান থেকে অন্য কিছুর অপেক্ষা করা যেকোনো কাজ বিপজ্জনক — এভাবে মনে রাখা উপকারী।6

লোডার লক ও প্রাইভেট লকের মধ্যে লক-অর্ডার ইনভার্সনলোডার লক ধরে DllMain প্রাইভেট লক নিতে যায়, আর সেই প্রাইভেট লক ধরে ওয়ার্কার GetModuleHandle ইত্যাদির জন্য লোডার লক নিতে যায়, তাই অর্জনের ক্রম উল্টে ডেডলক হয়DllMain: লোডার লক ধরেপ্রাইভেট লক G নিতে যায়ওয়ার্কার: প্রাইভেট লক G ধরেলোডার লক নিতে যায়উল্টানো অর্জন-ক্রমে ডেডলকGetModuleHandle ইত্যাদি অভ্যন্তরে চায়

চিত্র ৩: GetModuleHandle-এর মতো নিরীহ APIও অভ্যন্তরে লোডার লক চায়, তাই প্রাইভেট লকের সাথে ক্রম উল্টে ধরে যেতে পারে।

আর DllMain-এর ভেতর থেকে CreateThread ডাকা নিজেই সুপারিশকৃত নয়। তৈরি থ্রেডের DLL_THREAD_ATTACH নোটিফিকেশন প্রক্রিয়া করতে লোডার লক দরকার, তাই বর্তমানে চলা DllMain ফিরে লক না ছাড়া পর্যন্ত সে চলা শুরু করতে পারে না। তাই DllMain-এর ভেতরে সেই থ্রেড শুরু বা শেষের অপেক্ষা তৎক্ষণাৎ ডেডলক। জীবনকাল সমস্যাও আছে — DllMain ফেরার পর, এখনও চলা শুরু না করা থ্রেড পেছনে থেকে গেলে DLL আনলোড হলে থ্রেডের স্টার্ট অ্যাড্রেস ইতিমধ্যে-মুক্ত কোডেই নির্দেশ করে আর ক্র্যাশ হয়।3

৪. C++ ডেভেলপার সহজে যে দুই মাইন পায়ে দেয়

মাইন ১: গ্লোবাল অবজেক্টের ডায়নামিক ইনিশিয়ালাইজেশন। অধ্যায় ২ যেমন বলেছে, স্ট্যাটিক অবজেক্টের কনস্ট্রাক্টর DllMain সীমাবদ্ধতার নিচে চলে। কনফিগারেশন ফাইল পড়া, লগিং সুবিধা দাঁড় করানো, COM ইনিশিয়ালাইজ, থ্রেড শুরু — যে গ্লোবালের কনস্ট্রাক্টর সেই ধরনের কাজ করে তাকে DLL-এ রাখার মুহূর্তে আপনি “DllMain-এ যা করা যায় না” চালাচ্ছেন। কম্পাইল সময়ে স্থির কনস্ট্যান্ট ইনিশিয়ালাইজেশন (constexpr করা যায় এমন কিছু) নিরাপদ; ফাংশন কল জড়িত ইনিশিয়ালাইজেশন স্থগিত রাখুন।

গ্লোবাল অবজেক্ট ইনিশিয়ালাইজ যে পথে মাইন হয়DLL লোডে লোডার লক নেওয়া হয়, আর গ্লোবাল অবজেক্টের কনস্ট্রাক্টর CRT এন্ট্রি পয়েন্ট দিয়ে চলে, তাই সেই কনস্ট্রাক্টরের ভেতরের LoadLibrary, থ্রেড সিঙ্ক্রোনাইজেশন ও COM ইনিশিয়ালাইজেশন DllMain নিষেধাজ্ঞার চালনাDLL লোড (লোডার লক অর্জিত)CRT এন্ট্রি পয়েন্টগ্লোবাল অবজেক্টের কনস্ট্রাক্টরLoadLibrary-সমান কাজথ্রেড শুরু ও শেষের অপেক্ষাCOM বা User32 ব্যবহারএগুলো সব DllMain নিষেধাজ্ঞার নিচে পড়ে

চিত্র ৪: “DllMain খালি, তাই নিরাপদ”ও জটিল ইনিশিয়ালাইজেশনযুক্ত গ্লোবাল থাকলেই একই বিপদ ফিরিয়ে আনে।

মাইন ২: C++/CLI (মিক্সড অ্যাসেম্বলি)। নেটিভ DLL-কে C++/CLI দিয়ে মোড়ানো কনফিগারেশনে (র‍্যাপার নিবন্ধে যে আকৃতি) লোডার লকের নিচে MSIL (ম্যানেজড কোড) চালানোর বিপদ আছে। MSIL চালানো CLR ইনিশিয়ালাইজেশন বা অন্য অ্যাসেম্বলি লোড ট্রিগার করতে পারে। কম্পাইলার DllMain সরাসরি MSIL চালায় এমন কোডে C4747 সতর্কতা দেয়, কিন্তু অন্য মডিউলের ফাংশন দিয়ে পরোক্ষ চালনা ধরতে পারে নাDllMain ও তা থেকে ডাকা ফাংশন #pragma unmanaged দিয়ে নেটিভ কম্পাইল করুন, অথবা DllMain না থাকা কনফিগারেশন ব্যবহার করুন।4

লোডার লকের নিচে MSIL চালনা ধরা যায় কি নাDllMain সরাসরি MSIL চালায় এমন কোড কম্পাইলার C4747 সতর্কতায় ধরতে পারে, কিন্তু অন্য মডিউলের ফাংশন দিয়ে পরোক্ষ চালনা পারে না, তাই কল ট্রি রিভিউ ও নেটিভ কম্পাইল জোর করে আটকাতে হয়DllMain থেকে কলসরাসরি MSIL চালানঅন্য মডিউল দিয়ে চালানC4747 সতর্কতায় ধরা যায়কম্পাইলার ধরতে পারে নারিভিউ ও #pragma unmanaged দিয়ে আটকান

চিত্র ৫: C4747 শুধু সরাসরি চালনার বিরুদ্ধে রক্ষা করে। পরোক্ষ পথ শুধু রিভিউতে ধরা যায়।

৫. সঠিক ডিজাইন — “স্থগিত রাখা” ডিফল্ট নীতি করুন

অফিসিয়াল বেস্ট-প্র্যাকটিস সুপারিশ স্পষ্ট।1

  1. যে ইনিশিয়ালাইজেশন কম্পাইল সময়ে (স্ট্যাটিকভাবে) শেষ করা যায় শেষ করুন। আগে জিজ্ঞাসা করুন ডায়নামিক ইনিশিয়ালাইজেশন স্ট্যাটিক দিয়ে বদলানো যায় কি না।
  2. বাকিটা প্রথম ব্যবহার পর্যন্ত স্থগিত রাখুন। প্রথম ব্যবহার DLL লোড শেষ হওয়ার পর ডাকা সাধারণ API থেকে হলে ইনিশিয়ালাইজেশন লোডার লকের বাইরে চলে আর প্রায় পুরো Windows API নিরাপদে ব্যবহার করা যায়। প্রথম অ্যাক্সেসে এক্সক্লুশনের জন্য INIT_ONCE (ওয়ান-টাইম ইনিশিয়ালাইজেশন) অথবা C++ ম্যাজিক স্ট্যাটিক (ফাংশন-লোকাল স্ট্যাটিক) ব্যবহার করা যায়। স্থগিতকরণ সর্বরোগহর নয় — সেই প্রথম অ্যাক্সেস নিজেই DllMain বা স্ট্যাটিক ইনিশিয়ালাইজার থেকে হলে ইনিশিয়ালাইজার এখনও লোডার লকের নিচে চলে আর একই সীমাবদ্ধতায় ফিরে যান।
  3. শুধু আগে ধরতেই হয় এমন ব্যর্থতার জন্য ব্যতিক্রম করুন। ভাঙা কনফিগারেশন ফাইলে লোড নিজেই ব্যর্থ হোক এমন শর্ত থাকতে পারে। তবু “চেষ্টা করে সঙ্গে সঙ্গে ব্যর্থ” ন্যূনতমে রাখুন।
  4. DLL_PROCESS_ATTACH-এ DisableThreadLibraryCalls ভাবা। DLL থ্রেড নোটিফিকেশন ব্যবহার না করলে নোটিফিকেশন খরচই সরানো যায় (স্ট্যাটিক CRT বা স্ট্যাটিক TLS ব্যবহার ছাড়া)।5
  5. Application Verifier দিয়ে পরিদর্শন। DllMain-এর ভেতরের অনেক বিপজ্জনক কল Application Verifier রান টাইমে ধরে।1
DLL ইনিশিয়ালাইজেশনের ডিজাইন নির্দেশনাআগে দেখুন ইনিশিয়ালাইজেশন কম্পাইল-সময় স্ট্যাটিক ইনিশিয়ালাইজেশন করা যায় কি না; না গেলে ডিফল্ট প্রথম ব্যবহার পর্যন্ত স্থগিত রাখা, আর DllMain-এ শুধু লোড ব্যর্থতা হিসেবে আগে ধরতেই হয় এমন ন্যূনতম রাখুনহ্যাঁনানাহ্যাঁকম্পাইল সময়ে ঠিক করা যায়?স্ট্যাটিক ইনিশিয়ালাইজেশন করুনব্যর্থতা লোড সময়ে ধরতেই হবে?প্রথম ব্যবহার পর্যন্ত স্থগিত (ডিফল্ট)DllMain-এ শুধু ন্যূনতম করুনINIT_ONCE বা ফাংশন-লোকাল স্ট্যাটিক দিয়ে এক্সক্লুড

চিত্র ৬: সিদ্ধান্তের ক্রম “স্ট্যাটিক করা যায় → স্থগিত রাখা যায়”, আর DllMain-এ যা রাখেন তা শুধু আগে ধরতেই হয় এমন ন্যূনতম।

DisableThreadLibraryCalls প্রয়োগ করবেন কি না নিচের শাখায় যান্ত্রিকভাবে ঠিক করা যায়।

DisableThreadLibraryCalls ডাকবেন কি নাস্ট্যাটিক CRT-এর সাথে লিংক করা DLL থেকে ডাকবেন না; স্ট্যাটিক TLS কার্যকর থাকলে কল নিজেই ব্যর্থ তাই ডাকবেন না; কোনোটাই না মিললে ও DLL থ্রেড নোটিফিকেশন ব্যবহার না করলে DLL_PROCESS_ATTACH-এ রিটার্ন মান যাচাই করে ডাকুন নোটিফিকেশন খরচ কাটতেহ্যাঁনাহ্যাঁনানাহ্যাঁস্ট্যাটিক CRT-এর সাথে লিংক?ডাকা যাবে নাস্ট্যাটিক TLS ব্যবহার?কল যাই হোক ব্যর্থ (FALSE)থ্রেড নোটিফিকেশন দরকার?ATTACH-এ ডাকুন (রিটার্ন মান যাচাই)ডাকবেন না; নোটিফিকেশন সামলান

চিত্র ৭: স্ট্যাটিক CRT, স্ট্যাটিক TLS ও নোটিফিকেশন দরকার কি না — এই তিন শর্ত অনন্যভাবে ঠিক করে ডাকা উচিত কি না।

আনলোডে থ্রেড থামানোর জন্য অফিসিয়াল ডকুমেন্টেশন কংক্রিট প্রোটোকল দেয়। DLL_PROCESS_DETACH-এ (FreeLibrary দিয়ে আনলোডে) ওয়ার্কার থ্রেড এক্সিটের “অপেক্ষা” না করে আকৃতি হলো (১) ইভেন্ট দিয়ে এক্সিট সিগন্যাল, (২) থ্রেড পাশ কাজ সঙ্গতিপূর্ণ অবস্থায় ভাঁজ করে, ফিরে সিগন্যাল দেয়, আর অসীম ওয়েটে ঢোকে, (৩) DllMain পাশ সঙ্গতিপূর্ণ অবস্থা নিশ্চিত করে তারপর TerminateThread দিয়ে থ্রেড ভাঁজ করে।3 রূঢ় দেখায়, কিন্তু “DllMain-এর ভেতরে থ্রেডের স্বাভাবিক এক্সিটের অপেক্ষা করা যায় না” সীমাবদ্ধতার ভেতরে বাস্তবসম্মত উত্তর হিসেবে নথিভুক্ত।

আনলোডে থ্রেড থামানোর প্রোটোকলDllMain ইভেন্ট দিয়ে ওয়ার্কার থ্রেডকে এক্সিট সিগন্যাল করে; ওয়ার্কার কাজ সঙ্গতিপূর্ণ অবস্থায় ভাঁজ করে, ফিরে সিগন্যাল দেয় ও অসীম ওয়েটে ঢোকে; DllMain সঙ্গতিপূর্ণ অবস্থা নিশ্চিত করে তারপর থ্রেড টার্মিনেট করেওয়ার্কার থ্রেডDllMain (DETACH হ্যান্ডলিং)ওয়ার্কার থ্রেডDllMain (DETACH হ্যান্ডলিং)স্বাভাবিক এক্সিটের অপেক্ষা নেই, তাই ডেডলক নেইইভেন্ট দিয়ে এক্সিট সিগন্যালকাজ সঙ্গতিপূর্ণ অবস্থায় ভাঁজসঙ্গতি সম্পূর্ণ সিগন্যাল ও চিরকাল অপেক্ষাTerminateThread দিয়ে টার্মিনেট

চিত্র ৮: “স্বাভাবিক এক্সিটের অপেক্ষা”র বদলে “সঙ্গতি সিগন্যালের অপেক্ষা করে তারপর কেটে দেওয়া” লোডার লকের সাথে সংঘর্ষ এড়ায়।

প্রথম নীতি হিসেবে সবচেয়ে নিরাপদ ডিজাইন আনলোডযোগ্য DLL-এ থ্রেড মালিকানা এড়ানো, আর থ্রেড মালিকানা EXE পাশে রাখা।

প্রক্রিয়া এক্সিটে DLL_PROCESS_DETACH উল্টো: কিছু না করে ফেরাই আদর্শ। এই বিন্দুতে অন্য প্রতিটি থ্রেড ইতিমধ্যে জোর করে শেষ, আর নির্ভরশীল DLL বা রানটাইমের অবস্থার উপরও নির্ভর করা যায় না। এখানে জটিল কাজ শুধু ডেডলক ও ক্র্যাশ ঘটায়। যে ডেটা টিকে থাকতেই হবে তা অ্যাপের নিজস্ব শাটডাউন পথে লিখে রাখুন; এই নোটিফিকেশনের উপর নির্ভর করবেন না।3

৬. ধরলে কীভাবে অনুসন্ধান করবেন

লোডার-লক হ্যাঙের চেনা আঙুলের ছাপ আছে।

হ্যাং ডাম্পে স্ট্যাক দেখুন। জমে যাওয়া মুহূর্তের ডাম্প নিয়ে প্রতিটি থ্রেডের স্ট্যাক পরিদর্শন করুন। ntdll.dll লোডার ফাংশনের (Ldr দিয়ে নাম শুরু হওয়া পরিবার) ভেতরে লকের অপেক্ষায় থাকা থ্রেড ও DllMain বা স্ট্যাটিক ইনিশিয়ালাইজারের (dynamic initializer) ভেতরে অন্য কিছুর অপেক্ষায় থাকা থ্রেডের জোড়া পেলে প্রায় নিশ্চিত। LoadLibrary কলের মাঝপথে থেমে থাকা থ্রেড আরেকটি ক্লাসিক চরিত্র।

লোডার-লক হ্যাঙের আঙুলের ছাপহ্যাং ডাম্পে ntdll লোডার ফাংশনের ভেতরে লকের অপেক্ষায় থাকা থ্রেড ও DllMain বা স্ট্যাটিক ইনিশিয়ালাইজারের ভেতরে অন্য কিছুর অপেক্ষায় থাকা থ্রেড দুটোই থাকলে লোডার-লক ডেডলক ধরা প্রায় নিশ্চিতহ্যাঁনাহ্যাং ডাম্পLdr-পরিবার ফাংশনে লকের অপেক্ষায় থ্রেডDllMain বা স্ট্যাটিক ইনিশিয়ালাইজারের ভেতরে অপেক্ষায় থ্রেডদুটোই আছে?প্রায় নিশ্চিত লোডার-লক ডেডলকঅন্য ধরনের হ্যাং হিসেবে অনুসন্ধান

চিত্র ৯: লোডার-লক হ্যাঙের চেনা আঙুলের ছাপ “Ldr-এ অপেক্ষা + DllMain-এর ভেতরে অপেক্ষা”।

“সময়নির্ভর” চরিত্র সন্দেহ করুন। লোডার-লক ডেডলক শুধু সেই মুহূর্তে ধরে যখন DLL লোড থ্রেড শুরু বা এক্সিটের সাথে মিলে যায়। “স্টার্টআপে মাঝে মাঝে”, “শুধু নির্দিষ্ট মেশিনে”, “শুধু সার্ভিস হিসেবে চালালে” — এমন পুনরুৎপাদন শর্ত এই ধরনের সমস্যার চিহ্ন।

প্রতিরোধমূলক পরিদর্শন চালান। Application Verifier চালু করে টেস্ট চালালে DllMain-এর ভেতরের বিপজ্জনক কল রান টাইমে ধরা যায়।1 C++/CLI-এ C4747 সতর্কতা উপেক্ষা করবেন না; DllMain থেকে পৌঁছানো ফাংশনের রিভিউতে “পরোক্ষে LoadLibrary ডাকে এমন ফাংশন” (COM ইনিশিয়ালাইজেশন, কিছু CRT ফিচার, ডিলে-লোডেড ইমপোর্ট ইত্যাদি) কোণ রিভিউ চেকলিস্টে যোগ করলে শিপ করার আগে দুর্ঘটনা ধরা যায়। ডিলে-লোডেড ইমপোর্টের প্রথম কল অভ্যন্তরে LoadLibrary হয়ে যাওয়া সহজে মিস হয়।

৭. সারসংক্ষেপ

  • DllMain লোডার লক ধরে ডাকা হয় (প্রতি প্রক্রিয়ায় একটি, যে লক প্রতিটি DLL নোটিফিকেশন সিরিয়ালাইজ করে)। প্রতিটি সীমাবদ্ধতা সেখান থেকে আসে।
  • নিষেধাজ্ঞার কেন্দ্র “LoadLibrary / FreeLibrary ডাকবেন না”, “অন্য থ্রেডের সাথে সিঙ্ক্রোনাইজ করবেন না”, আর “Kernel32 ছাড়া অন্য DLL-এর উপর নির্ভর করা ফাংশন ডাকবেন না”। CRT দিয়ে চলা স্ট্যাটিক অবজেক্টের কনস্ট্রাক্টর ও ডেস্ট্রাক্টর একই সীমাবদ্ধতার নিচে পড়ে।
  • মৌলিক ডিজাইন নীতি স্থগিতকরণ। যে ইনিশিয়ালাইজেশন স্ট্যাটিক করা যায় স্ট্যাটিক করুন; বাকিটা প্রথম ব্যবহারে স্থগিত রাখুন। DisableThreadLibraryCalls ও Application Verifier ব্যবহার করুন।
  • আনলোডে থ্রেড থামানো অফিসিয়াল প্রোটোকল মানে (সিগন্যাল → সঙ্গতি নিশ্চিত → টার্মিনেট)। প্রক্রিয়া এক্সিটে DLL_PROCESS_DETACH আদর্শত খালি।
  • C++/CLI-এ লোডার লকের নিচে MSIL চালানো আলাদা মাইন। DllMain কল ট্রির নেটিভ কম্পাইল জোর করুন।

DllMain সীমাবদ্ধতা প্রথমে অযৌক্তিক নিষেধাজ্ঞার তালিকা মনে হয়। কিন্তু “শীর্ষ-স্তরের লক, লোডার লক ধরে ডাকা হয়” এই একটি বিন্দু ধরলে প্রতিটি নিষেধাজ্ঞা একই নীতির পুনর্কথন। নীতি হিসেবে মনে রাখুন, আর ডকুমেন্টেশনে নেই এমন এজ কেস পেলেও সঠিক প্রশ্ন করতে পারা উচিত: “লক ধরে এই কাজ করার অনুমতি আছে কি?”

সম্পর্কিত নিবন্ধ

সম্পর্কিত পরামর্শ ক্ষেত্র

KomuraSoft LLC স্টার্টআপ বা DLL লোড সময়ে হ্যাং ও ডেডলকের মূল কারণ অনুসন্ধান (ডাম্প বিশ্লেষণ), DllMain ও স্ট্যাটিক ইনিশিয়ালাইজেশন ঘিরে ডিজাইন রিভিউ, আর C++/CLI র‍্যাপার ও প্লাগ-ইন DLL নিরাপদ ইনিশিয়ালাইজেশন ডিজাইনের দিকে সংশোধন সামলায়। “শুধু নির্দিষ্ট পরিবেশে স্টার্টআপে হ্যাং করে” এই কঠিন-পুনরুৎপাদন পর্যায়েও পরামর্শ করা যায়।

তথ্যসূত্র

  1. Microsoft Learn, Dynamic-Link Library Best Practices। DllMain লোডার লক ধরে ডাকা হয় বলে যে ফাংশন ডাকা যায় তা কঠোরভাবে সীমিত; আদর্শ DllMain খালি স্টাব ও ইনিশিয়ালাইজেশন যতদূর সম্ভব স্থগিত; কম্পাইল-সময় স্ট্যাটিক ইনিশিয়ালাইজেশনের সুপারিশ; আগে ধরতেই হয় এমন ব্যর্থতার জন্য শুধু ন্যূনতম করা; এবং Application Verifier দিয়ে ক্লাসিক DllMain ভুল ধরা সম্পর্কে।  2 3 4 5 6

  2. Microsoft Learn, DllMain entry point। এন্ট্রি পয়েন্টে শুধু সরল ইনিশিয়ালাইজেশন ও টার্মিনেশন করা; LoadLibrary / FreeLibrary ডাকা যায় না কেন (বৃত্তাকার লোড অর্ডার ও ইনিশিয়ালাইজেশনের আগে বা টার্মিনেশনের পরে DLL ব্যবহার); Kernel32.dll ইতিমধ্যে লোড নিশ্চিত, তাই অন্য DLL লোড করে না এমন পরিসরে ডাকা যায়; নিরাপদ ফাংশনের সম্পূর্ণ তালিকা নেই; 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 দিয়ে সামলানো; DllMain সরাসরি MSIL চালাতে চাইলে C4747 সতর্কতা দেওয়া, কিন্তু অন্য মডিউল দিয়ে পরোক্ষ চালনা ধরা না যাওয়া; এবং স্ট্যাটিক অবজেক্টের ডায়নামিক ইনিশিয়ালাইজার একই সমস্যা ঘটাতে পারা সম্পর্কে।  2

  5. Microsoft Learn, DisableThreadLibraryCalls function (libloaderapi.h)। থ্রেড তৈরি ও ধ্বংসের ওভারহেড কমাতে DLL_THREAD_ATTACH / DLL_THREAD_DETACH নোটিফিকেশন নিষ্ক্রিয় করা; স্ট্যাটিক CRT-এর সাথে লিংক করা DLL থেকে না ডাকা; এবং স্ট্যাটিক TLS (thread_local বা __declspec(thread)) কার্যকর থাকলে অপটিমাইজেশন না হওয়া সম্পর্কে।  2

  6. Microsoft Learn, Dynamic-Link Library Best Practices - Deadlocks Caused by Lock Order Inversion। লক হায়ারার্কি সংজ্ঞায়িত করে সবসময় একই ক্রমে অর্জন; লোডার DllMain ডাকার আগে লোডার লক অর্জন করে, তাই লোডার লক হায়ারার্কির শীর্ষে থাকা উচিত; GetModuleFileName-এর মতো পরোক্ষে লোডার লক নেওয়া API ও প্রাইভেট লকের মধ্যে অর্জন-ক্রম দেখা; এবং লক-অর্ডার ইনভার্সন থেকে ডেডলকের কংক্রিট উদাহরণ সম্পর্কে।  2

কাছাকাছি বিষয়ে গভীরে যেতে একই ট্যাগযুক্ত সাম্প্রতিক নিবন্ধ।

"Not Responding" আসলে কী — Windows কীভাবে অ্যাপ হ্যাং ধরে, আর যে ডিজাইন হ্যাং করে না

Windows-এর "Not Responding" এমন যন্ত্র যেখানে OS বিচার করে উইন্ডো ৫ সেকেন্ড মেসেজ তোলেনি আর তাকে ঘোস্ট উইন্ডো দিয়ে বদলায়। এই নিবন্ধ সেই...

Spurious Wakeup — কন্ডিশন ভেরিয়েবল কেন "নোটিফাই না হয়েও" জাগে এবং Windows-এ সঠিকভাবে কীভাবে অপেক্ষা করবেন

কন্ডিশন ভেরিয়েবলের ওয়েট নোটিফিকেশন না এলেও ফিরতে পারে (spurious wakeup)। এই নিবন্ধ Windows বাস্তবায়ন থেকে ব্যাখ্যা করে স্পেসিফিকেশন কে...

নেমড পাইপ ব্যবহারিকভাবে — ডিজাইন থেকে নিরাপত্তা পর্যন্ত Windows-এর মানক IPC

Windows-এর মানক আন্তঃপ্রক্রিয়া যোগাযোগ নেমড পাইপের ব্যবহারিক গাইড। প্রাথমিক উৎস থেকে এই নিবন্ধ বাইট ও মেসেজ মোডের পছন্দ, একাধিক ক্লায়েন...

ঘুম থেকে জাগলে ভাঙে যে অ্যাপ — Windows পাওয়ার ইভেন্টের কাজ ও যে ব্যবসায়িক অ্যাপ তা সহ্য করে

ল্যাপটপ খুললেন আর ব্যবসায়িক অ্যাপের সংযোগ মৃত — কারণ ঘুম ধরে না নেওয়া ডিজাইন। এই নিবন্ধ WM_POWERBROADCAST নোটিফিকেশন প্রবাহ, Modern Sta...

এই পৃষ্ঠাগুলো বিষয়টিকে সেবা ও সিদ্ধান্তের বৃহত্তর প্রেক্ষাপটে স্থাপন করে।

নিবন্ধটি নিচের সেবাগুলোর সাথে সরাসরি সম্পর্কিত।

প্রায়শ জিজ্ঞাসিত প্রশ্ন

এই নিবন্ধের বিষয়ে পরামর্শে প্রায়ই আসা প্রশ্ন।

DllMain-এ সত্যিই কিছুই করা যায় না?
"কিছুই করবেন না" অতিরঞ্জন নয়; এটি অফিসিয়াল ডিজাইন অবস্থান, আর Microsoft নিজেই বলে আদর্শ DllMain প্রায়-খালি স্টাব। যা নিরাপদ তা Kernel32.dll ফাংশনের একটি উপসেট — DllMain চলার সময় Kernel32 লোড হয়ে থাকবে নিশ্চিত — সেই পরিসরে যা অন্য DLL লোড করে না। ক্রিটিক্যাল সেকশন বা মিউটেক্স তৈরি, আর 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-এর বাইরে শেষ।

লেখকের প্রোফাইল

নিবন্ধের লেখকের পরিচিতি পৃষ্ঠা।

Go Komura

KomuraSoft LLC-এর প্রতিনিধি

Windows সফটওয়্যার ডেভেলপমেন্ট, প্রযুক্তিগত পরামর্শ ও বাগ তদন্তে বিশেষজ্ঞ, বিশেষ করে বিদ্যমান সিস্টেমযুক্ত প্রকল্প ও পুনরুৎপাদন করা কঠিন বাগে।

পাবলিক লিঙ্ক

ব্লগে ফিরে যান