کامپایلر، طراحی و معماری آن – به زبان ساده


کامپایلر کد نوشته شده در یک زبان برنامهنویسی را به زبانهای دیگری ترجمه میکند به طوری که در طی این فرایند معنی برنامه تغییر نمییابد. همچنین کامپایلر کد مقصد را برحسب زمان و فضا بهینه و کارآمد میسازد.
مفاهیم طراحی کامپایلر نیازمند بینش عمیقی از فرایندهای ترجمه و بهینهسازی است. طراحی کامپایلر مستلزم مکانیسمهای ترجمه و همچنین کشف و بازیابی خطا است. این فرایند شامل تحلیل واژهای، نحوی و معنایی کد در وهله اول و در مرحله دوم تولید و بهینهسازی کد در پسزمینه است.
سلسله مطالبی که با عنوان طراحی کامپایلر به خدمت شما تقدیم میشوند، مناسب همه کسانی است که به یادگیری عمیق زبانهای برنامهنویسی و علوم رایانه علاقهمند هستند. در نهایت شما با مطالعه کامل این دوره با اغلب مفاهیم طراحی کامپایلر آشنا میشوید و حتی میتوانید خودتان یک کامپایلر طراحی کنید.
برای مطالعه این سلسله مطالب داشتن دانشی از کامپایلرها ضرورتی ندارد؛ اما دستکم باید درکی از یک زبان برنامهنویسی مانند C، جاوا و غیره داشته باشد. در صورتی که از قبل با زبان اسمبلی آشنا باشید، این یک مزیت بزرگ محسوب میشود.
مروری بر کامپایلرها
رایانهها ترکیب متعادلی از نرمافزار و سختافزار هستند. سختافزار تنها یک قطعه مکانیکی است و کارکردهای آن از سوی نرمافزارهای مناسب کنترل میشود. سختافزار دستورالعملها را به شکل بار الکتریکی درک میکند که معادل زبان باینری در برنامهنویسی رایانه است. الفبای زبان باینری تنها دو حرف دارد: صفر و یک. برای این که دستورالعملهایی برای سختافزار بنویسیم باید کدی به فرمت باینری بنویسیم که در واقع یک سری از 1 و 0 ها است. نوشتن چنین کدی برای برنامهنویسها وظیفه دشوار و طاقتفرسایی است. از این رو نرمافزارهای واسطی به نام کامپایلر طراحی شدهاند که این کدها را بنویسند.
سیستم پردازش زبان
تا این جا دانستیم که سیستم رایانهای متشکل از سختافزار و نرمافزار است. سختافزار زبانی را میفهمد که برای انسان نامفهوم است. از این رو ما برنامهها را در زبانی سطح بالا که درک و بهخاطرسپاری آن برای ما آسانتر است مینویسیم. سپس این برنامهها به یک سری ابزارها و همتایان سیستمعامل داده میشوند تا کد مطلوبی که ماشین میتواند بفهمد به دست آید. این ابزارها سیستم پردازش زبان نامیده میشوند.
زبان سطح بالا در چندین فاز به زبان باینری تبدیل میشود. کامپایلر برنامهای است که زبان سطح بالا را به زبان اسمبلی تبدیل میکند. به طور مشابه assembler برنامهای است که زبان اسمبلی را به زبان سطح ماشین تبدیل میکند.
ابتدا اجازه بدهید ببینیم یک برنامه با استفاده از کامپایلر C چگونه روی ماشین میزبان اجرا میشود:
- کاربر برنامهای را به زبان C مینویسد (زبان سطح بالا)
- کامپایلر C برنامه را کامپایل میکند و آن را به زبان اسمبلی ترجمه میکند (زبان سطح پایین)
- سپس یک اسمبلر برنامه اسمبلی را به کد ماشین (object) ترجمه میکند.
- یک ابزار linker برای پیوند دادن همه اجزای برنامه به هم و اجرای آن استفاده میشود (کد قابل اجرای ماشین)
- یک loader همه این اجزا را در حافظه بارگذاری میکند و سپس برنامه اجرا میشود.
پیش از آن که مستقیماً به بررسی مفاهیم کامپایلرها بپردازیم باید از چند ابزار دیگر که ارتباط نزدیکی با کامپایلرها دارند داشته باشیم.
پیش پردازنده (Preprocessor)
یک پیش پردازنده به طور کلی به عنوان بخشی از کامپایلر در نظر گرفته میشود و ابزاری است که ورودی کامپایلرها را تهیه میکند. این ابزار به وظایفی از قبیل ریز پردازش (macro-processing)، تقویت (augmentation)، گنجایش فایل (file inclusion)، بسط زبان (language extension) و غیره میپردازد.
مفسر (Interpreter)
مفسر مانند کامپایلر، زبان سطح بالا را به زبان سطح ماشین ترجمه میکند. تفاوت در روش خواندن کدهای منبع یا ورودی است. کامپایلر کل کد منبع را به یکباره میخواند، توکنها را ایجاد میکند، معنا را بررسی میکند و کد واسط را تولید میکند، سپس کل برنامه را اجرا میکند و این فرایند ممکن است شامل مراحل فراوانی باشد. به طور عکس مفسرها یک عبارت را از ورودی میخوانند، آن را به یک کد میانجی تبدیل میکنند، اجرا میکنند و سپس به ترتیب عبارت بعدی را انتخاب میکنند. اگر خطایی رخ دهد یک مفسر اجرا را متوقف کرده و گزارش خطا میکند؛ در حالی که کامپایلر کل یک برنامه را میخواند و حتی در صورتی که با چند خطا مواجه شود نیز به این کار ادامه میدهد.
اسمبلر (Assembler)
اسمبلر وظیفه ترجمه زبان اسمبلی به کد ماشین را بر عهده دارد. خروجی اسمبلر فایل آبجت (Object) نامیده میشود که شامل ترکیبی از دستورالعملهای ماشین به عنوان داده مورد نیاز برای قرار دادن این دستورالعملها در حافظه است.
لینکر (Linker)
لینکر برنامهای رایانهای است که فایلهای آبجکت مختلف را با هم پیوند داده و ادغام میکند تا یک فایل اجرایی ایجاد کند. همه این فایلها را میتوان به وسیله اسمبلرهای مختلف کامپایل کرد. وظیفه اصلی یک لینکر جستجو و یافتن ماژولها/روتینهای ارجاع دار در یک برنامه و تعیین موقعیت حافظهای است که کدها باید در آن بارگذاری شوند تا دستورالعملهای برنامه ارجاعهای صحیحی داشته باشند.
لودر (Loader)
لودر بخشی از سیستمعامل است که مسئولیت بارگذاری فایلهای اجرایی درون حافظه و اجرا کردن آنها را بر عهده دارد. لودر اندازه یک برنامه (دستورالعملها و دادهها) را محاسبه میکند و فضای حافظه مورد نیاز آن را ایجاد میکند. این جزء رجیسترهای مختلف را برای آغاز اجرا مقداردهی اولیه میکند.
کامپایلر متقابل (Cross-compiler)
این نوع از کامپایلر بر روی یک پلتفرم (الف) اجرا میشود و توانایی ایجاد کدهای اجرایی برای پلتفرم دیگر (ب) را دارد و از این رو کامپایلر متقابل نامیده میشود.
کامپایلر سورس به سورس (Source-to-source Compiler)
کامپایلری است که یک سورس کد را در یک زبان برنامهنویسی دریافت میکند و آن را به سورس کد در زبان برنامهنویسی دیگری ترجمه میکند.
معماری کامپایلر
معماری کامپایلر را میتوان بر اساس روش کامپایل کردن به طور عمده به دو فاز تقسیم کرد: فاز تحلیل و فاز سنتز.
فاز تحلیل
کامپایلر در این فاز که به نام فرانتاند (Front End) نیز نامیده میشود، اقدام به خواندن برنامه منبع میکند و آن را به اجزای اصلی تقسیم میکند و سپس به دنبال خطاهای واژهای، نحوی و ساختاری میگردد. در فاز تحلیل، یک بازنمایی میانجی از برنامه منبع و جدول نماد ایجاد میشود که در ادامه به عنوان ورودی به فاز سنتز وارد خواهد شد.
فاز سنتز
در این فاز که بک-اند نیز نامیده میشود، برنامه هدف با کمک بازنمایی کد میانجی و جدول نمادها ایجاد میشود.
یک کامپایلر ممکن است فازها و گذرهای مختلفی داشته باشد:
- گذر (pass): منظور از گذر، پیمایش کل برنامه از سوی یک کامپایلر است.
- فاز (phase): منظور از فاز در کامپایلر یک مرحله قابل تمییز است که در آن ورودی از فاز قبلی تحویل گرفته میشود، مورد پردازش قرار میگیرد و خروجی به عنوان ورودی فاز بعدی تحویل داده میشود. هر گذر میتواند شامل یک یا چند فاز باشد.
فازهای کامپایلر
فرایند کامپایل کردن یک توالی از فازهای مختلف است. هر فاز، ورودی را از مرحله قبلی میگیرد و بازنمایی خاص خود از برنامه منبع را دارد که آن را به عنوان خروجی، به فاز بعدی کامپایلر ارائه میکند. فازهای کامپایلر را در تصویر زیر بهتر میتوانید ببینید:
تحلیل واژهای (Lexical Analysis)
فاز نخست اسکنر به عنوان اسکنر متنی عمل میکند.
در این فاز کد منبع به عنوان رشتهای از کاراکترها اسکن میشود و به صورت تکواژههای معنادار تبدیل میشود. تحلیلگر واژگانی این تکواژهها را به شکل توکنهایی مانند زیر نمایش میدهد:
<token-name, attribute-value>
تحلیل نحوی (Syntax Analysis)
فاز بعدی به نام تحلیل نحوی یا تجزیه (parsing) نامیده میشود. در این فاز توکنهای ایجاد شده در مرحله تحلیل واژهای به عنوان ورودی استفاده میشوند و یک درخت تجزیه (یا درخت نحوی) ایجاد میشود. در این فاز چیدمانهای توکن با گرامر کد منبع مقایسه میشوند، یعنی تجزیهکننده بررسی میکند آیا عبارتی که توسط توکنها ایجاد شده از نظر معناشناختی صحیح است یا نه.
تحلیل معناشناسی (Semantic Analysis)
در فاز تحلیل معنایی بررسی میشود که آیا درخت تجزیه برحسب قواعد زبانشناسی ایجاد شده است یا نه. برای نمونه آیا بین انواع مختلف داده تبدیل نوع صحیحی صورت گرفته یا نه و یا رشته به یک عدد صحیح اضافه شده است. ضمناً تحلیلگر معنایی، شناسهها را ردگیری کرده و نوع و عبارتهای آنها را بررسی میکند. در این مرحله بررسی میشود که آیا شناسهای پیش از استفاده اعلان شده یا نه. تحلیلگر معناشناختی، یک درخت نحوی نمادگذاری شده به عنوان خروجی تولید میکند.
تولید کد میانجی (Intermediate Code Generation)
کامپایلر پس از تحلیل معناشناختی یک کد میانجی از کد منبع برای ماشین هدف تولید میکند. این کد نمایشدهنده برنامهای برای یک ماشین انتزاعی است. این کد میانجی چیزی بین زبان سطح بالا و زبان ماشین است. کد میانجی باید به طرزی ایجاد شده باشد که ترجمه آن به کد ماشین مقصد آسان باشد.
بهینهسازی کد (Code Optimization)
در فاز بعدی کد میانجی بهینهسازی میشود. بهینهسازی را میتوان چیزی مانند حذف خطوط کد غیر ضروری تصور کرد که با چیدمان توالی دستورات به ترتیبی که باعث سریعتر اجرا شدن برنامه و عدم هدر دادن منابع (پردازنده و حافظه) میشود، موجب عملکرد بهینهتر آن میشود.
تولید کد (Code Generation)
در این فاز، تولیدکننده کد، بازنمایی بهینه از کد میانجی را دریافت کرده و آن را به زبان ماشین مقصد نگاشت میکند. تولیدکننده کد، کد میانجی را به یک توالی از کدهای ماشین که عموماً قابل جایابی مجدد (re-locatable) هستند، ترجمه میکند. توالی دستورالعملهای کد ماشین همان وظیفهای را انجام میدهد که کد میانجی باید انجام میداد.
جدول نمادها (Symbol Table)
جدول نمادها نوعی از ساختمان داده است که در تمام فازهای کامپایلر حضور دارد. همه نامهای شناسه همراه با انواع آنها در این جدول ذخیره میشوند. جدول نمادها امکان جستجوی سریع رکورد شناسه و بازیابی آن را تسهیل میکند. جدول نمادها همچنین برای مدیریت حوزه تعریف (scope) استفاده میشود.
اگر این نوشته مورد توجه شما قرار گرفته است، احتمالاً به موارد زیر نیز علاقهمند خواهید بود:
- آموزش طراحی کامپایلر
- آموزش تجزیه انتقال (کاهش) در طراحی کامپایلر
- ابزارهای مهندسی کامپیوتر
- آموزش گرامرها در طراحی کامپایلر
- آموزش طراحی کامپایلر (مرور و حل تست های کنکور کارشناسی ارشد)
- مجموعه آموزشهای علوم کامپیوتر
- مجموعه آموزش های میکروکنترلر PIC با کامپایلر CCS
==
بسیاااار عاااالی
سپاس
فوق عالی ممنون بابت زحماتتون