از کد منبع تا یک فرآیند در حال اجرا: ترجمه و یک مثال کامل مرتب‌سازی

تبدیل یک برنامه C به چیزی که سیستم‌عامل واقعاً بتواند اجرا کند شامل چندین مرحله ترجمه مجزاست، که هرکدام یک فایل میانی متفاوت تولید می‌کنند. این مقاله کل این خط لوله را از کامپایلر تا loader مرور می‌کند، سپس مفاهیم این فصل را روی یک مثال کامل و واقعی به‌کار می‌برد: ترجمه گام‌به‌گام یک روال مرتب‌سازی C به اسمبلی RISC-V.

خط لوله کامپایل RISC-Vلینکر و لودرمثال مرتب‌سازی

~4 دقیقه مطالعه · آخرین به‌روزرسانی ۱۵ شهریور ۱۴۰۵

مسیر کامل از کد منبع تا اجرا

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

  • 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 نگاشت می‌شود، روشن می‌کند این مجموعه کوچک از عناصر پایه که در سراسر این فصل پوشش داده شد، چگونه در کنار هم هر آنچه نرم‌افزار واقعی نیاز دارد را می‌سازند.

نوشته و پژوهش‌شده توسط دکتر شاهین صیامی

مقالات مرتبط

سخت‌افزار چگونه تقسیم انجام می‌دهد: خارج‌قسمت، باقیمانده و موارد خاص

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

ادامه

سخت‌افزار چگونه اعداد را ضرب می‌کند: از منطق ساده تا مدارهای واقعی

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

ادامه

سخت‌افزار چگونه جمع و تفریق انجام می‌دهد و سرریز را تشخیص می‌دهد

سخت‌افزار چگونه جمع و تفریق انجام می‌دهد و سرریز را تشخیص می‌دهد

ادامه

آرایه‌ها در مقابل اشاره‌گرها در سطح سخت‌افزار

در C، آرایه‌ها و اشاره‌گرها اغلب قابل‌تعویض به‌نظر می‌رسند، و بسیاری از برنامه‌نویسان با آن‌ها به‌عنوان یک چیز واحد رفتار می‌کنند. با این حال در سطح سخت‌افزار، آن‌ها به توالی‌های دستوری قابل‌توجهی متفاوت با ویژگی‌های کارایی متفاوت کامپایل می‌شوند. این مقاله این دو رویکرد را با استفاده از اسمبلی RISC-V مقایسه می‌کند تا دقیقاً نشان دهد چرا کد مبتنی بر اشاره‌گر اغلب سریع‌تر است.

ادامه

مدیریت آدرس‌های بزرگ و همگام‌سازی در RISC-V

یک دستور ۳۲ بیتی نمی‌تواند یک ثابت بزرگ یا یک آدرس حافظه دور را مستقیماً درون خودش جا دهد، و چند پردازنده‌ای که حافظه را به اشتراک می‌گذارند نمی‌توانند بدون هماهنگی، یک داده یکسان را با امنیت به‌روزرسانی کنند. این مقاله توضیح می‌دهد RISC-V چگونه مقادیر فوری و آدرس‌های بزرگ را از قطعات کوچک‌تر می‌سازد، و دستورات اتمیک چگونه اجازه می‌دهند برنامه‌های موازی با امنیت همگام شوند.

ادامه

سخت‌افزار چگونه از فراخوانی توابع و داده‌های کاراکتری پشتیبانی می‌کند

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

ادامه