مقدمه

در درس‌های گذشته، با ساختار کلی زبان میانی مشترک (CIL) و نحوه‌ی تعریف ساختار یک اسمبلی با استفاده از دایرکتیوها آشنا شدیم. دیدیم که دایرکتیوها اسکلت منطقی کد را می‌سازند. اما گوشت و خون واقعی برنامه، یعنی منطق اجرایی آن، توسط دستورات (Instructions) یا کدهای عملیاتی (Opcodes) پیاده‌سازی می‌شود. این دستورات، عملیات پایه‌ای هستند که بر روی پشته‌ی ارزیابی (Evaluation Stack) انجام می‌شوند و در نهایت، توسط کامپایلر JIT به کد ماشین بومی ترجمه می‌گردند.

زبان CIL دارای بیش از ۲۰۰ دستور مختلف است که هر کدام وظیفه‌ی مشخصی را بر عهده دارند. بررسی تمام آن‌ها خارج از حوصله‌ی این دوره است، اما در این درس به بررسی دسته‌های اصلی و پرکاربردترین دستورات خواهیم پرداخت. درک این دستورات به ما نشان می‌دهد که چگونه ساختارهای سطح بالای زبان C# مانند حلقه‌ها، دستورات شرطی، فراخوانی متدها و کار با اشیاء، در سطح پایین پیاده‌سازی می‌شوند.

دستورات بارگذاری و ذخیره‌سازی (Loading and Storing)

این دسته از دستورات، مسئول جابجایی داده‌ها بین حافظه‌های مختلف (آرگومان‌های متد، متغیرهای محلی، فیلدهای کلاس) و پشته‌ی ارزیابی هستند. هر عملیاتی در CIL، ابتدا نیازمند بارگذاری عملوندهای خود بر روی پشته است.

بارگذاری آرگومان‌ها و متغیرهای محلی

دستورات ldarg (load argument) و ldloc (load local) برای بارگذاری آرگومان‌های متد و متغیرهای محلی بر روی پشته به کار می‌روند. دستورات stloc (store local) نیز مقدار بالای پشته را در یک متغیر محلی ذخیره می‌کنند.

Copy Icon C# Code
public void SimpleAssignment(int p)
{
    int x = p;
}

کد CIL معادل برای این متد به شکل زیر خواهد بود:

Copy Icon CIL Code
.method public instance void SimpleAssignment(int32 p) cil managed
{
  .locals init ([0] int32 x)
  
  IL_0000: ldarg.1   // Load parameter 'p' onto the stack.
  IL_0001: stloc.0   // Pop the value from stack and store in local 'x'.
  IL_0002: ret
}

در اینجا، ldarg.1 به اولین پارامتر متد (یعنی p) اشاره دارد (به یاد داشته باشید که ldarg.0 در متدهای نمونه به this اشاره دارد). این دستور مقدار p را روی پشته قرار می‌دهد. سپس stloc.0 آن مقدار را از پشته برداشته و در اولین متغیر محلی (x) ذخیره می‌کند.

دستورات کنترلی و انشعاب (Branching)

این دسته از دستورات، جریان اجرای برنامه را کنترل می‌کنند و اساس پیاده‌سازی ساختارهای شرطی و حلقه‌ها هستند. آن‌ها با استفاده از برچسب‌ها (labels) مانند IL_XXXX به JIT می‌گویند که اجرای کد را به کدام بخش منتقل کند.

پیاده‌سازی if-else

یک دستور if (condition) { A } else { B } در CIL به این صورت ترجمه می‌شود: "شرط را ارزیابی کن. اگر نتیجه false بود، به ابتدای بلوک B بپر. در غیر این صورت، بلوک A را اجرا کن و سپس از روی بلوک B به بیرون بپر."

Copy Icon C# Code
if (a > b) { Console.WriteLine("a is greater"); }
else { Console.WriteLine("a is not greater"); }

کد CIL آن به صورت زیر خواهد بود:

Copy Icon CIL Code
  ldarg.1      // Load 'a'
  ldarg.2      // Load 'b'
  cgt          // Compare them (a > b). Pushes 1 if true, 0 if false.
  brfalse.s  ELSE_BLOCK // If the result is false (0), jump to ELSE_BLOCK
  
  // 'IF' block code
  ldstr      "a is greater"
  call       void [System.Console]::WriteLine(string)
  br.s       END_IF // Jump over the ELSE block

ELSE_BLOCK:
  // 'ELSE' block code
  ldstr      "a is not greater"
  call       void [System.Console]::WriteLine(string)

END_IF:
  ret

دستور cgt (compare greater than) دو مقدار را از پشته برداشته و مقایسه می‌کند. دستور brfalse (branch if false) در صورت false بودن شرط، به برچسب مشخص شده پرش می‌کند. دستور br (branch) نیز یک پرش بدون قید و شرط است.

دستورات فراخوانی متد (call و callvirt)

برای فراخوانی متدها از دو دستور اصلی استفاده می‌شود که تفاوت مهمی با هم دارند.

  • call: این دستور برای اتصال زودهنگام (Early Binding) به کار می‌رود. از این دستور برای فراخوانی متدهای استاتیک و متدهای نمونه‌ی غیرمجازی (non-virtual) استفاده می‌شود. کامپایلر در زمان کامپایل دقیقاً می‌داند کدام متد باید فراخوانی شود.
  • callvirt: این دستور برای اتصال دیرهنگام (Late Binding) و پیاده‌سازی چندریختی به کار می‌رود. از این دستور برای فراخوانی متدهای مجازی (virtual) و متدهای اینترفیس استفاده می‌شود. در زمان اجرا، CLR به جدول متدهای مجازی (v-table) شیء نگاه کرده و نسخه‌ی صحیح و بازنویسی شده‌ی متد را برای اجرا پیدا می‌کند. به دلایل فنی و امنیتی، کامپایلر C# معمولاً برای تمام متدهای نمونه‌ی عمومی، حتی اگر virtual نباشند، از callvirt استفاده می‌کند.

دستورات مربوط به مدل شیءگرا

CIL مجموعه‌ای غنی از دستورات برای کار با اشیاء، فیلدها و انواع مقداری دارد.

دستورات newobj، ldfld و stfld

  • newobj: این دستور یک نمونه‌ی جدید از یک شیء را ایجاد می‌کند. این دستور ابتدا حافظه‌ی لازم را بر روی Heap تخصیص داده و سپس سازنده‌ی مشخص شده را فراخوانی می‌کند.
  • ldfld (load field): مقدار یک فیلد نمونه را می‌خواند و روی پشته قرار می‌دهد.
  • stfld (store field): مقداری را از پشته برداشته و در یک فیلد نمونه ذخیره می‌کند.

دستورات box و unbox

این دو دستور برای تبدیل بین انواع مقداری و ارجاعی به کار می‌روند.

  • box: یک نوع مقداری را از پشته برداشته، یک شیء جدید بر روی Heap برای آن ایجاد کرده، مقدار را در آن کپی می‌کند و در نهایت، ارجاع به آن شیء جدید را روی پشته قرار می‌دهد.
  • unbox.any: یک ارجاع به یک شیء بسته‌بندی شده (boxed) را از پشته برداشته، نوع آن را بررسی کرده و مقدار نوع مقداری داخل آن را استخراج و روی پشته قرار می‌دهد.
Copy Icon C# Code
int i = 123;
object o = i; // Boxing
int j = (int)o; // Unboxing

کد CIL برای این عملیات به شکل زیر خواهد بود:

Copy Icon CIL Code
ldc.i4    123
stloc.0      // i = 123
ldloc.0      // Load i
box       [System.Runtime]System.Int32 // Box the int to an object
stloc.1      // o = boxed i
ldloc.1      // Load o
unbox.any [System.Runtime]System.Int32 // Unbox the object to an int
stloc.2      // j = unboxed o