مسیر کامل از کد منبع تا اجرا
یک برنامه مستقیماً از کد منبع به اجرا روی سختافزار نمیرود. چندین ابزار مجزا را طی میکند، که هرکدام برنامه را به یک شکل میانی متفاوت تبدیل میکنند پیش از آنکه در نهایت بتواند اجرا شود.
Compilerکد منبع سطح بالا را به زبان اسمبلی مخصوص پردازنده هدف ترجمه میکند.Assemblerآن زبان اسمبلی را به یکObject Fileترجمه میکند، فایلی باینری حاوی دستورات ماشین بههمراه اطلاعات دفترداری اضافی که هنوز کاملاً نهایی نشده است.Linkerچندین فایل object کامپایلشده جداگانه، بههمراه هر کد کتابخانهای مورد نیاز، را در یکExecutable Fileکامل واحد ترکیب میکند، و ارجاعات بین فایلهای مختلف را رفع میکند تا فراخوانی یک تابع که در یک فایل انجام شده، درست به موقعیت واقعی آن تابع در فایل دیگر اشاره کند.Loaderفایل اجرایی نهاییشده را برمیدارد، دستورات و دادههای آن را در حافظه قرار میدهد، و پردازنده را برای شروع اجرای آن آماده میکند.
هرکدام از این ابزارها یک مسئله مجزا را حل میکنند: کامپایلر ترجمه زبان را مدیریت میکند، اسمبلر کدگذاری دستورات تکی را مدیریت میکند، لینکر ترکیب قطعات مستقلاً توسعهیافته یک برنامه را مدیریت میکند، و لودر انتقال از یک فایل استاتیک روی دیسک به یک فرآیند فعالانه در حال اجرا در حافظه را مدیریت میکند.
چرا کامپایل جداگانه اهمیت دارد
پروژههای نرمافزاری بزرگ بهندرت بهصورت یک فایل منبع واحد نوشته میشوند. تقسیم یک برنامه بین فایلهای زیاد، کامپایل کردن هرکدام بهطور مستقل، و ترکیب آنها فقط در مرحله linking، اجازه میدهد بخشهای مختلف یک سیستم بزرگ بهطور جداگانه توسعه، تست، و بازکامپایل شوند — یک برنامه نیازی ندارد بهطور کامل از صفر بازسازی شود فقط چون یک فایل کوچک تغییر کرده است.
جمعبندی عملی: ترجمه یک روال مرتبسازی C
برای دیدن کاربرد عملی این ایدهها، یک تابع مرتبسازی سادهشده به زبان C را در نظر بگیرید که یک آرایه از اعداد صحیح را با استفاده از یک رویکرد مبتنی بر تبادل ساده مرتب میکند.
void sort(long v[], long n) {
for (long i = 0; i < n; i += 1) {
for (long j = i; j > 0 && v[j-1] > v[j]; j -= 1) {
swap(v, j);
}
}
}ترجمه این کد به اسمبلی RISC-V نیازمند ترکیب تقریباً هر مفهومی است که تاکنون در این فصل پوشش داده شده:
- حلقههای بیرونی و درونی
forبه دستوراتConditional Branchتبدیل میشوند که محدوده حلقه را بررسی میکنند، ترکیبشده باUnconditional Jumpsبرای بازگشت به ابتدای هر حلقه. - ایندکسگذاری آرایه مانند
v[j]نیازمند محاسبه یک آدرس حافظه با ترکیب یک آدرس پایه با یک آفست، سپس صادر کردن یکData Transfer Instructionبرای بارگذاری یا ذخیره مقدار است. - فراخوانی
swapبهعنوان یکProcedure Callپیادهسازی میشود، با پیروی از قراردادهای ذخیره رجیستر که پیشتر پوشش داده شد، چونsortباید رجیسترهای شمارنده حلقه خودش را در طول فراخوانی حفظ کند. - مقایسههایی مانند
v[j-1] > v[j]از دستورات تفریق و شاخهزنی ساخته میشوند، چون RISC-V برای هر مورد یک شاخه اختصاصی "بزرگتر از" ندارد.
یک قطعه سادهشده که منطق مقایسه درونیترین و بازگشت به حلقه را نشان میدهد:
Loop:
bge x0, j, Exit
ld t0, -8(v_addr)
ld t1, 0(v_addr)
ble t0, t1, Exit
jal ra, swap
addi j, j, -1
jal x0, Loop
Exit:این مثال نشان میدهد حتی یک قطعه کد اپلیکیشن کوتاه و معمولی نیز به یک توالی قابلتوجه از دستورات ساده کامپایل میشود، که هرکدام دقیقاً یک کار کوچک انجام میدهند.
چرا مرور یک مثال کامل اهمیت دارد
مطالعه انواع دستورات بهطور مجزا مفید است، اما یک برنامه واقعی حلقهها، دسترسی به آرایه، فراخوانی تابع، و مقایسهها را همه همزمان ترکیب میکند. دیدن اینکه یک الگوریتم آشنا مانند مرتبسازی چگونه روی دستورات RISC-V نگاشت میشود، روشن میکند این مجموعه کوچک از عناصر پایه که در سراسر این فصل پوشش داده شد، چگونه در کنار هم هر آنچه نرمافزار واقعی نیاز دارد را میسازند.