مقدمه

در درس قبل، با زبان میانی مشترک (CIL) به عنوان زبان اسمبلی دنیای .NET آشنا شدیم. دیدیم که کد C# ما به این زبان میانی ترجمه می‌شود. یکی از ویژگی‌های جالب و قدرتمند اکوسیستم .NET، قابلیت انجام مهندسی رفت و برگشت (Round-trip Engineering) است. این مفهوم به این معناست که ما می‌توانیم یک اسمبلی کامپایل‌شده (.dll یا .exe) را به کد منبع CIL آن "دیس‌اسمبل" (disassemble) کنیم، در صورت تمایل آن کد CIL را تغییر دهیم، و سپس آن را مجدداً به یک اسمبلی کاملاً کاربردی "اسمبل" (assemble) کنیم.

این قابلیت نه تنها یک تمرین آکادمیک جالب است، بلکه یک ابزار قدرتمند برای درک عمیق‌تر نحوه‌ی کار کامپایلر و CLR، و همچنین برای سناریوهای پیشرفته مانند بهینه‌سازی سطح پایین و تولید کد دینامیک محسوب می‌شود. در این درس، این فرآیند رفت و برگشتی را به صورت گام به گام بررسی خواهیم کرد.

ابزارهای مورد نیاز: ildasm و ilasm

برای انجام مهندسی رفت و برگشت، ما از دو ابزار خط فرمان استاندارد که به همراه .NET SDK نصب می‌شوند، استفاده می‌کنیم:

  • ildasm.exe (Intermediate Language Disassembler): این ابزار یک فایل اسمبلی را به عنوان ورودی گرفته و کد CIL و فراداده‌ی آن را در قالب یک فایل متنی با پسوند .il استخراج می‌کند.
  • ilasm.exe (Intermediate Language Assembler): این ابزار یک فایل .il را به عنوان ورودی گرفته و آن را به یک فایل اسمبلی .dll یا .exe کامپایل می‌کند.

این دو ابزار به ما اجازه می‌دهند تا چرخه‌ی کامل C# → CIL → Assembly را مشاهده و دستکاری کنیم.

یک مثال گام به گام

بیایید این فرآیند را با یک مثال ساده از ابتدا تا انتها انجام دهیم.

قدم اول: نوشتن و کامپایل کد C#

ابتدا یک کلاس ساده در یک فایل به نام MyLibrary.cs می‌نویسیم.

Copy Icon MyLibrary.cs
namespace MyCode
{
    public class SimpleCalc
    {
        public int Add(int x, int y)
        {
            return x + y;
        }
    }
}

سپس، با استفاده از کامپایلر C# (csc.exe) از طریق خط فرمان، این کد را به یک کتابخانه‌ی کلاس (.dll) کامپایل می‌کنیم.

Copy Icon Command Prompt
csc /target:library MyLibrary.cs

پس از اجرای این دستور، یک فایل به نام MyLibrary.dll در پوشه‌ی فعلی ایجاد می‌شود.

قدم دوم: دیس‌اسمبل کردن به CIL

حالا از ابزار ildasm برای استخراج کد CIL از اسمبلی خود استفاده می‌کنیم. دستور زیر، محتوای اسمبلی را در یک فایل متنی به نام MyLibrary.il می‌ریزد.

Copy Icon Command Prompt
ildasm /out=MyLibrary.il MyLibrary.dll

اگر فایل MyLibrary.il را باز کنیم، کدی شبیه به زیر را خواهیم دید که شامل تعریف اسمبلی، فضای نام، کلاس، و کد CIL برای متد Add است.

Copy Icon MyLibrary.il
.namespace MyCode
{
  .class public auto ansi beforefieldinit SimpleCalc
         extends [System.Runtime]System.Object
  {
    .method public hidebysig instance int32 
            Add(int32 x, int32 y) cil managed
    {
      // Code size       7 (0x7)
      .maxstack  2
      IL_0000:  ldarg.1
      IL_0001:  ldarg.2
      IL_0002:  add
      IL_0003:  stloc.0
      IL_0004:  ldloc.0
      IL_0006:  ret
    } // end of method SimpleCalc::Add

    .method public hidebysig specialname rtspecialname 
            instance void  .ctor() cil managed
    {
      // ... constructor CIL ...
    } // end of method SimpleCalc::.ctor

  } // end of class SimpleCalc
} // end of namespace MyCode

قدم سوم: اسمبل کردن مجدد CIL

در این مرحله، ما می‌توانیم فایل .il را مستقیماً ویرایش کنیم (هرچند این کار نیازمند تسلط کامل بر CIL است). برای مثال، می‌توانیم یک دستور Console.WriteLine را به متد Add اضافه کنیم. اما برای این درس، ما فایل را بدون تغییر رها کرده و آن را مستقیماً به یک اسمبلی جدید کامپایل می‌کنیم.

Copy Icon Command Prompt
ilasm /dll /output=NewMyLibrary.dll MyLibrary.il

دستور بالا، فایل MyLibrary.il را خوانده و یک اسمبلی جدید به نام NewMyLibrary.dll ایجاد می‌کند. این اسمبلی جدید از نظر عملکردی کاملاً با MyLibrary.dll اصلی یکسان است و می‌تواند در هر پروژه‌ی دیگری مورد ارجاع قرار گیرد. ما با موفقیت یک چرخه‌ی کامل رفت و برگشت را انجام دادیم!

چرا مهندسی Round-trip مهم است؟

  • ابزار یادگیری بی‌نظیر: بهترین راه برای فهمیدن اینکه کامپایلر C# با ویژگی‌های سطح بالایی مانند async/await، yield return یا عبارات لامبدا چه می‌کند، این است که کد CIL تولید شده برای آن‌ها را مشاهده کنید. این کار به شما درک عمیقی از اتفاقات پشت صحنه می‌دهد.
  • بهینه‌سازی سطح پایین: در موارد بسیار نادر، ممکن است یک برنامه‌نویس حرفه‌ای تشخیص دهد که می‌تواند با ویرایش مستقیم کد CIL، بهینه‌سازی‌هایی را انجام دهد که کامپایلر C# قادر به انجام آن نبوده است.
  • پایه‌ای برای تولید کد دینامیک: کتابخانه‌هایی که در زمان اجرا کد تولید می‌کنند (مانند برخی از فریم‌ورک‌های ORM یا AOP)، از APIهایی مانند System.Reflection.Emit برای تولید مستقیم بایت‌کدهای CIL در حافظه و کامپایل آن‌ها به یک اسمبلی دینامیک استفاده می‌کنند. درک CIL برای کار با این APIها ضروری است.