مقدمه
در درسهای گذشته، با ساختار کلی زبان میانی مشترک (CIL) و نحوهی تعریف ساختار یک
اسمبلی با استفاده از دایرکتیوها آشنا شدیم. دیدیم که دایرکتیوها اسکلت منطقی کد را میسازند. اما
گوشت و خون واقعی برنامه، یعنی منطق اجرایی آن، توسط دستورات (Instructions) یا
کدهای عملیاتی (Opcodes) پیادهسازی میشود. این دستورات، عملیات
پایهای هستند که بر روی پشتهی ارزیابی (Evaluation Stack) انجام میشوند و در نهایت، توسط
کامپایلر JIT به کد ماشین بومی ترجمه میگردند.
زبان CIL دارای بیش از ۲۰۰ دستور مختلف است که هر کدام وظیفهی مشخصی را بر عهده دارند.
بررسی تمام آنها خارج از حوصلهی این دوره است، اما در این درس به بررسی دستههای اصلی و
پرکاربردترین دستورات خواهیم پرداخت. درک این دستورات به ما نشان میدهد که چگونه ساختارهای سطح
بالای زبان C# مانند حلقهها، دستورات شرطی، فراخوانی متدها و کار با اشیاء، در سطح
پایین پیادهسازی میشوند.
دستورات بارگذاری و ذخیرهسازی (Loading and Storing)
این دسته از دستورات، مسئول جابجایی دادهها بین حافظههای مختلف (آرگومانهای متد، متغیرهای محلی،
فیلدهای کلاس) و پشتهی ارزیابی هستند. هر عملیاتی در CIL، ابتدا نیازمند بارگذاری
عملوندهای خود بر روی پشته است.
بارگذاری آرگومانها و متغیرهای محلی
دستورات ldarg (load argument) و ldloc
(load local) برای بارگذاری آرگومانهای متد و متغیرهای محلی بر روی پشته به کار میروند. دستورات
stloc (store local) نیز مقدار بالای پشته را در یک متغیر محلی ذخیره
میکنند.
C# Code
public void SimpleAssignment(int p)
{
int x = p;
}
کد CIL معادل برای این متد به شکل زیر خواهد بود:
CIL Code
.method public instance void SimpleAssignment(int32 p) cil managed
{
.locals init ([0] int32 x)
IL_0000: ldarg.1
IL_0001: stloc.0
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 به بیرون بپر."
C# Code
if (a > b) { Console.WriteLine("a is greater"); }
else { Console.WriteLine("a is not greater"); }
کد CIL آن به صورت زیر خواهد بود:
CIL Code
ldarg.1
ldarg.2
cgt
brfalse.s ELSE_BLOCK
ldstr "a is greater"
call void [System.Console]::WriteLine(string)
br.s END_IF
ELSE_BLOCK:
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) را از پشته
برداشته، نوع آن را بررسی کرده و مقدار نوع مقداری داخل آن را استخراج و روی پشته قرار میدهد.
C# Code
int i = 123;
object o = i;
int j = (int)o;
کد CIL برای این عملیات به شکل زیر خواهد بود:
CIL Code
ldc.i4 123
stloc.0
ldloc.0
box [System.Runtime]System.Int32
stloc.1
ldloc.1
unbox.any [System.Runtime]System.Int32
stloc.2