در قسمت قبل در بررسی پشت پرده فرایند کامپایل به نقش پیشپردازشگر، کامپایلر و اسمبلر پرداختیم در ادامه به بررسی عملکرد لینکر میپردازیم.
لینکر
برای ساخت برنامه، لازم است آبجکت فایلی (object file) که از برنامه خود ایجاد کردیم و برخی کامپوننتهای کتابخانه C (libc) با هم ترکیب شوند. وظیفهی لینکر این است که فایلهای موردنیاز برای ساخت برنامه را بگیرد، آنها را ترکیب کند و به هر کدام از آنها یک آدرس حافظهی واقعی اختصاص دهد.
همانند اسمبلر (assembler) میتوانیم با این کامند به gcc بگوییم که فلگهایی (flags) را به لینکر منتقل کند:
1 | $ gcc -Wall -Wextra -static -Wl,-Map=hello.map -o hello hello.o |
گزینه ‘-Wl ‘به GCC میگوید که گزینهای که به دنبال آن میآید (-Map=hello.map) را به لینکر پاس دهد.
map به ما میگوید که لینکر هر بخش برنامه را در کجای حافظه قرار داده است. (بعداً در این مورد بیشتر بحث میکنیم) همچنین، گزینه static– را اضافه کردهایم که فایل اجرایی را از لینک شده بهصورت پویا (dynamic linked) به لینک شده بهصورت ایستا (statically linked) تغییر میدهد تا نقشه حافظه (memory map) بیشتر شبیه چیزی باشد که در سیستم امبدد خود خواهیم دید. بهاینترتیب میتوانیم از بحث پیچیدگیهای لینک متغیر اجتناب کنیم.
آبجکت فایلها مانند hello.o میتوانند جابهجا شوند. یعنی میتوانند در هر جایی از حافظه قرار بگیرند. این وظیفهی لینکر است که تصمیم بگیرد دقیقاً در کجای حافظه قرار بگیرند. همچنین وظیفهی لینکر است که از کتابخانههای استفادهشده توسط برنامه، آبجکت فایلهای موردنیاز را استخراج کند و آنها را در برنامهی نهایی بگنجاند. نقشهی لینکر (linker map) به ما میگوید که آبجکتها کجا قرار گرفتهاند و چه بخشهایی از چه کتابخانهای در برنامهی گنجانده (include) شدهاند. بهعنوانمثال، یک ورودی معمولی لینکر ممکن است شبیه به این باشد:
1 2 3 4 5 6 7 8 9 10 11 | .text 0x000000000040fa90 0x1c8 /usr/lib/gcc/x86_64-linux-gnu/5/../../../ x86_64-linux-gnu/libc.a(ioputs.o) 0x000000000040fa90 puts 0x000000000040fa90 _IO_puts *fill* 0x000000000040fc58 0x8 |
اگر از قسمت قبل بهخاطر داشته باشید ما put را در برنامه خود استفاده نکردیم و در جهت بهینهسازی در فرایند کامپایل این تابع جایگزین printf ای که ما استفاده کردهایم شده است همانگونه که قبلاً هم اشاره کردهایم، این تابع از فایل استاندارد کتابخانهی C (libc.a) آمده است. ما میتوانیم اینجا ببینیم که کد این تابع در آدرس “0x000000000040fa90” قرار دارد. شاید برایتان سؤال باشد که دانستن این آدرسها چه کمکی به ما میکند؛ مثلاً اگر برنامه ما در محدودهی آدرس بین 0x40fa90 تا 0x40fc58 خراب شود اطلاع از مکان قرارگیری puts به ما کمک میکند که بدانیم puts باعث خرابی شده است.
همچنین میدانیم که 0x1c8 ،puts بایت (40fc58-40fa90) را به خود اختصاص داده است. این ۴۵۶ بایت (یا کمی کمتر از .5K) دسیمال است. زمانی که برنامهنویسی با میکروکنترلری که حافظه محدودی دارد را آغاز کنیم، باید بر روی این اعداد بیشتر حساس باشیم و میزان حافظه استفاده شده برای ما مهم خواهد بود.
اکنون شما باید دید خوبی از هر المان یک برنامهی C و اینکه این بخشهای مختلف چهکارهایی انجام میدهند داشته باشید. بیشتر مواقع، میتوانید به کامپایلر اجازه دهید این جزئیات را بدون نگرانی در پشت پرده انجام دهد. اما وقتی که شما دارید برنامهنویسی را برای تراشههای کوچک با منابع محدود انجام میدهید، باید نگران آنچه درونش رخ میدهد باشید.
افزودن به Makefile
فایل makefile توسط برنامه make برای ساختن و اجرای پروژهها استفاده میشود. با استفاده از makefile که در قسمت دوم آشنا شدیم.
با ویژگیها و عملکردهای مختلف کامپایلر، اسمبلر، و لینکر در GCC آشنا شوید. برای بررسی جنبههای مختلف کامپایلر GCC، اسمبلر و لینکر، فایل Makefile خود را بهگونهای تغییر میدهید که تمام فایلهای توصیفشده در بخش قبلی را تولید کند:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | CFLAGS=-Wall -Wextra -ggdb all: hello hello.i hello.s hello.o: hello.c gcc $(CFLAGS) -Wa,-a=hello.lst -c hello.c hello: hello.o gcc $(CFLAGS) -static -Wl,-Map=hello.map -o hello hello.o hello.i: hello.c gcc -E hello.c >hello.i hello.s: hello.c gcc -S hello.c # Type "make verbose" to see the whole command line verbose: gcc -v $(CFLAGS) -Wextra -c hello.c clean: rm -f hello hello.i hello.s hello.o |
همانطور که قبلاً توضیح داده شد، خط اول غیر خالی، یک ماکرو را تعریف میکند که به make میگوید (CFLAGS) $ را در سراسر فایل با Wall -Wextra -ggdb– جایگزین کند. سپس، یک تارگت (یک آیتم که باید ساخته شود) به نام all تعریف میکنیم. ازآنجاییکه این اولین آبجکت در فایل است و همچنین پیشفرض میباشد، میتوانید آن را بهسادگی، با واردکردن کامند زیر بسازید:
1 | $ make |
این تارگت که به آن phony target میگوییم، منجر به ایجاد یک فایل با نام ” all“ نمیشود. بلکه، هر بار که کامند “make all“ را اجرا میکنید، make بررسی میکند که آیا نیاز است بخشهای لازم را دوباره بسازد یا نه. این بخشها را میتوانید در فایل makefile پس از کلمه کلیدی”all :” مشاهده کنید. برای ساخت تارگت “all“، باید تارگتهای “hello“، “hello.i” و “hello.s” را بسازیم.
1 | gcc -E hello.c > hello.i |
بنابراین، اگر فایل hello.c را ویرایش کنید و سپس کامند make hello.i را اجرا کنید، خواهید دید که make وظیفه خود را انجام میدهد.
1 2 3 4 5 | $ (Change hello.c) $ make hello.i gcc =E hello.c > hello.i |
تارگت دیگری در Makefile ما، clean است که تمام فایلهای تولید شده را حذف میکند. برای خلاصشدن از شر فایلهای تولید شده، کامند زیر را اجرا کنید:
1 | $ make clean |
GNU make یک برنامه بسیار پیچیده است که دارای یک راهنمای بیش از ۳۰۰ صفحهای میباشد. البته خبر خوب این است که برای کارآمد بودن کافی است با یک زیرمجموعه بسیار کوچک از کامندهای آن سروکار داشته باشید.
خلاصه این فصل ساختن یک برنامه “Hello World” یکی از سادهترین کارهایی است که یک برنامهنویس C میتواند انجام دهد. بااینحال، درک تمام جزئیاتی که در پشت پرده برای ایجاد و اجرای آن برنامه C اتفاق میافتد، کمی دشوارتر است. خوشبختانه، شما نیازی به تسلط کامل بر همه بخشهای زبان اسمبلی تولید شده توسط برنامه ندارید.
اما هر برنامهنویس امبدد باید بهاندازه کافی بفهمد تا بتواند مشکلات یا رفتارهای غیرمعمولی مانند ظاهرشدن puts در برنامهای که printf در آن نوشته شده است، را تشخیص دهد. توجه به این نکات کوچک به ما کمک میکند از دستگاههای کوچکمان بهترین استفاده را کنیم.
در پایان این بخش شما قادر خواهید بود به سؤالات زیر پاسخ دهید:
- اسناد GNU make کجا قرار دارند؟
- آیا کد C بین سختافزارهای مختلف قابلانتقال است؟
- آیا کد زبان اسمبلی بین دستگاهها با انواع مختلف قابلانتقال است؟
- چرا یک کامند در زبان اسمبلی فقط یک دستور ماشین تولید میکند، درحالیکه یک کامند در زبان C میتواند چندین دستور ماشین تولید کند؟