هنگامیکه که یه اسمبلی برای اجرا به حافظه بارگذاری میشه، CLR کدهای موجود رو بر مبنای متد به متد اجرا میکنه. یعنی برای اجرای یه قطعه کد اونها رو عملا به صورت قطعاتی که کوچکترین واحدش متدها هستند درنظر میگیره و سپس برای اجرای متدها، کد IL موجود رو برای کامپایل JIT به حافظه بارگذاری میکنه و بعدش کار اجرای کدهای کامپایل شده به زبان ماشین رو بصورت خط به خط انجام میده.
اما این وسط تو اجرای این کدها ممکنه ارجاعی به اعضای نوع های دیگه وجود داشته باشه. خب حالا میشه پرسید که CLR دقیقا چیجوری این نوع ها رو شناسایی میکنه؟ چیجوری محل دقیق اونا رو تو اسمبلی هابی که اونا رو تعریف کرده پیدا میکنه؟ و چیجوری کدهای موردنیاز رو به حافظه بارگذاری میکنه؟
برای توضیح بیشتر مثالی زده میشه. قطعه کد زیر رو درنظر بگیرین:
public sealed class Program { public static void Main() { System.Console.WriteLine("Yusefnejad.blog.ir"); } }
پس از کامپایل این کد تو یه اسمبلی اجرایی و اجرای اون، ابتدا CLR تو حافظه بارگذاری و اجرا میشه. سپس CLR شروع به خوندن هدر clr اسمبلی میکنه و بدنبال توکن MethodDef متد ورودی برنامه (Entry Point) تو جداول متادیتای اسمبلی میگرده. این متد ورودی اینجا همون Main هستش. با استفاده از این داده ها آدرس کد IL این متد تو حافظه دقیقا مشخص میشه و پس از بازیابی اون، ابتدا عملیات بررسی و تایید کد انجام میشه و بعدش با استفاده از کامپایلر JIT کد IL اون به کد ماشین کامپایل شده و این کد native تولیدی اجرا میشه.
برای مشاهده کد IL متد Main تو اسمبلی کامپایل شده، میشه از ابزار ildasm.exe استفاده کرد. تو این ابزار ابتدا از متد View گزینه Show bytes رو انتخاب کنین و بعدش رو گره متد main دابل کلیک بکنین تا کد IL اون به همراه آدرس حافظه و شماره توکن اعضا نشون داده بشه:
.method public hidebysig static void Main() cil managed // SIG: 00 00 01 { .entrypoint // Method begins at RVA 0x2050 // Code size 13 (0xd) .maxstack 8 IL_0000: /* 00 | */ nop IL_0001: /* 72 | (70)000001 */ ldstr "Yusefnejad.blog.ir" IL_0006: /* 28 | (0A)000003 */ call void [mscorlib]System.Console::WriteLine(string) IL_000b: /* 00 | */ nop IL_000c: /* 2A | */ ret } // end of method Program::Main
هنگام کامپایل JIT این کد، CLR تمامی نوع ها و اعضای ریفرنس داده شده تو این کد رو شناسایی میکنه و اونا رو اگه تا حالا بارگذاری نشده باشن، به حافظه بارگذاری میکنه. تو کد بالا ابتدا رشته "Yusefnejad.blog.ir" با استفاده از دستور ldstr به stack بارگذاری شده و سپس تو خط بعدی، این رشته تو خروجی کنسول نمایش داده میشه. مشخصه که اینجا یه ریفرنسی به System.Console.WriteLine وجود داره. با استفاده از داده های اضافی نمایش داده شده (با فعال کردن گزینه Show bytes) مشخصه که دستور call داره توکن 0A00003 رو صدا میزنه. درواقع این دستور اینجا داره ورودی شماره 3 از جدول 0A رو فراخوانی میکنه. این جدول (0A) جدول متادیتای MemberRef هستش. CLR با بررسی داده های این ورودی متوجه میشه که این ورودی خودش متعلق به یه نوع دیگه (System.Console) با یه توکن تو جدول متادیتای TypeRef هستش. با مراجعه به ورودی مربوطه تو این جدول معلوم میشه که این نوع خودش تو یه اسمبلی دیگه تعریف شده که با یه توکن تو جدول متادیتای AssemblyRef معرفی شده. با رجوع به این جدول نهایی مشخص میشه که اسمبلی موردنظر مشخصاتی بصورت زیر داره:
از اینجا به بعد CLR میدونه که به چه اسمبلی ای نیاز داره. بنابراین سعی میکنه تا این اسمبلی رو تو حافظه بارگذاری کنه. البته درصورت نیاز به بارگذاری فایلی جداگانه، ابتدا عملیات یافتن یا کاوش اسمبلی انجام میشه.
برای بارگذاری یه نوع موردنیاز، CLR میتونه این نوع رو تو یکی از سه حالت زیر پیدا کنه:
- تو فایل و اسمبلی جاری: تشخیص این حالت تو زمان کامپایل انجام میشه (اصطلاحا بهش early bound هم میگن). بدون نیاز به بارگذرای فایلی اضافی، نوع موردنظر مستقیما از محتویات بارگذاری شده خونده میشه.
- تو فایلی جداگانه از اسمبلی جاری (اسمبلی جاری چند فایله باشه): تو این حالت، اول بررسی میشه که فایل موردنظر تو جدول ModuleRef تو مانیفست اسمبلی جاری معرفی شده باشه. سپس CLR مسیری که مانیفست اسمبلی از اونجا لود شده رو برای پیدا کردن فایل ماژول مربوطه میگرده. پس از یافتن این فایل اون رو به حافظه بارگذاری میکنه و پس از بررسی داده های اون برای اطمینان از درستی محتواش، نوع و عضو موردنظر رو یافته و اجرای برنامه ادامه پیدا میکنه.
- تو فایل و اسمبلی جداگانه: تو این حالت، CLR ابتدا فایلی که مانیفست اسمبلی مربوطه رو داره، یافته و بارگذاری میکنه. اگه این فایل بارگذاری شده، حاوی نوع موردنظر نباشه (برای اسمبلیهای چند فایله)، با استفاده از داده های مانیفست، فایل ماژولی که نوع موردنظر توش تعریف شده رو یافته و بارگذاری میکنه. سپس نوع و عضو موردنیاز رو پیدا کرده و اجرای کد ادامه پیدا میکنه.
برای آشنایی بیشتر با جداول اشاره شده، به مطالب متادیتا و مانیفست رجوع کنین.
اگه تو هر مرحله ای از عملیات یافتن و بارگذاری نوع، خطایی رخ بده (مثلا فایل مورد نظر پیدا نشه، فایل رو نشه بارگذاری کرد، مقدار هش فایل با چیزی که تو مانفیست اسمبلی وجود داره متفاوت باشه و ...)، استثنای مناسبی صادر میشه. همچنین درصورت نیاز میشه متدهایی برای رویدادهای رخ داده تو این فرایندها که تو کلاس System.AppDomain تعریف شدن، ثبت کرد. این رویدادها شامل AssemblyResolve, ReflectionOnlyAssemblyResolve, TypeResolve میشن. این سه رویداد وقتیکه هر مرحله ای از بارگذاری نوع به خطا بخوره صدا زده میشن. تو متدهایی که برای این رویدادها ثبت میشه، میشه خطای رخ داده رو بررسی کرد و با پیاده سازی کدهایی مشکل رخ داده رو به نوعی حل کرد و اجرای اپلیکیشن رو بدون صدور خطا ادامه داد.
تو مثال جاری، CLR تشخیص میده که نوع System.Console تو یه اسمبلی دیگه تعریف شده. اینجا CLR باید دنبال فایل اصلی اسمبلی (فایلی که مانیفست اسمبلی توشه) بگرده. سپس با گشتن تو جداول مانیفست اسمبلی مقصد، موقعیت فیزیکی پیاده سازی نوع مورنیاز بدست میاد. حالا اگه همون فایل اصلی اسمبلی این نوع رو پیاده کرده باشه که چه بهتر و همونجا بارگذاری نوع انجام میشه. اما اگه فایلی که نوع رو پیاده کرده باشه، یه فایل جدا باشه (تو یه اسمبلی چند فایله)، CLR ابتدا دنبال اون فایل میگرده و پس از یافتن و بارگذاریش و بررسی جداول متادیتا، نوع موردنظر رو پیدا کرده و بارگذاری میکنه. بعدش CLR ساختار داخلی موردنیازش رو با استفاده از داده های نوع بارگذاری شده میسازه، و در ادامه کامپایلر JIT برای کامپایل کد متد جاری (متد Main تو این مثال) دست به کار میشه. درنهایت اجرای متد در دستور کار قرار میگیره.
تمامی این فرایند نسبتا پیچیده تو تصویر زیر به نمایش در اومده:
تا اینجا فرایند پیش فرضی که CLR برای بارگذاری یه نوع انجام میده رو بررسی کردیم. اما مثل کاوش اسمبلی، بارگذاری اسمبلی و نوع رو هم میشه بصورت دستی مدیریت کرد و تغییراتی تو روند پیش فرض این فرایند ایجاد کرد. برای اینکار هم میشه از یه سری تنظیمات فایل کانفیگ برنامه استفاده کرد. برای روشنتر شدن این موضوع یه نمونه از این تنظیمات در زیر آورده شده:
<?xml version="1.0" ?> <configuration> <runtime> <assemblybinding xmlns="urn:schemas-microsoft-com:asm.v1"> <probing privatepath="libs;utils;data;bin\asm" /> <dependentassembly> <assemblyidentity name="MyAssembly" publickeytoken="32ab4ba45e0a69a1" culture="neutral" /> <bindingredirect oldversion="1.7.0.0" newversion="2.0.5.0" /> <codebase version="2.0.5.0" href="http://www.MySite.com/MyAssembly.dll" /> </dependentassembly> <dependentassembly> <assemblyidentity name="MyLib" publickeytoken="1f2e74e897abbcfe" culture="neutral" /> <bindingredirect oldversion="1.0.0.0-1.5.0.0" newversion="1.9.0.0" /> </dependentassembly> </assemblybinding> </runtime> </configuration>
قسمتهای مختلف این تنظیمات در ادامه شرح داده شدن:
probing: همونطور که تو مطلب مدیریت کاوش اسمبلی شرح داده شد، از این تنظیم برای تعیین مسیرهایی که CLR برای یافتن اسمبلی های موردنیاز باید کاوش کنه استفاده میشه. میبینیم که میشه چندین مسیر رو که با سمیکالن از هم جدا شدن تو این قسمت معرفی کرد.
dependentAssembly: با استفاده از این تنظیم میشه مدیریت بارگذاری اسمبلی رو بدست گرفت. مثلا تو اولین تنظیم، تعیین شده که برای اسمبلی MyAssembly با توکن کلید عمومی 32ab4ba45e0a69a1 و بدون فرهنگ، درصورت درخواست نسخه 1.7.0.0، از نسخه 2.0.5.0 باید به جاش استفاده بشه. یا تو تنظیم دوم، برای اسمبلی MyLib با توکن کلید عمومی 1f2e74e897abbcfe و بدون فرهنگ، درصورت نیاز به نسخه های 1.0.0.0 تا 1.5.0.0، از نسخه 1.9.0.0 باید به جاشون استفاده کرد.
با استفاده از تگ codeBase هم میشه کاوش اسمبلی رو تو این قسمت مدیریت کرد. مثلا تو این مثال تنظیم شده تا این اسمبلی از یه مسیر اینترنتی دانلود بشه. از این تگ بیشتر برای اسمبلی های با نام قوی استفاده میشه.
با استفاده از این تنظیمات میشه رفتار بارگذاری اسمبلی CLR رو تا حدودی به دلخواه تغییر داد. یکی از مثالهای عملی این تنظیات رو میشه تو تمپلیت پیش فرض پروژه های ASP.NET MVC تو ویژوال استودیو دید. مثلا فایل کانفیگ یه پروژه MVC 4 رو سیستم من، شامل تنظیمات زیره:
<runtime> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <dependentAssembly> <assemblyIdentity name="DotNetOpenAuth.Core" publicKeyToken="2780ccd10d57b246" /> <bindingRedirect oldVersion="1.0.0.0-4.0.0.0" newVersion="4.1.0.0" /> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="DotNetOpenAuth.AspNet" publicKeyToken="2780ccd10d57b246" /> <bindingRedirect oldVersion="1.0.0.0-4.0.0.0" newVersion="4.1.0.0" /> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="System.Web.Helpers" publicKeyToken="31bf3856ad364e35" /> <bindingRedirect oldVersion="1.0.0.0-2.0.0.0" newVersion="2.0.0.0" /> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="System.Web.Mvc" publicKeyToken="31bf3856ad364e35" /> <bindingRedirect oldVersion="1.0.0.0-4.0.0.0" newVersion="4.0.0.0" /> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="System.Web.WebPages" publicKeyToken="31bf3856ad364e35" /> <bindingRedirect oldVersion="1.0.0.0-2.0.0.0" newVersion="2.0.0.0" /> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="WebGrease" publicKeyToken="31bf3856ad364e35" /> <bindingRedirect oldVersion="1.0.0.0-1.3.0.0" newVersion="1.3.0.0" /> </dependentAssembly> </assemblyBinding> </runtime>میبینید که برای اطمینان از بارگذاری درست نوع ها تو این پروژه، بارگذاری اسمبلی ها با این تنظیمات خاص مدیریت شده.
.: . :.