آموزش کامل لینکر و مدل حافظه در C برای امبدد سیستم ها - قسمت 27 امبدد C

embedded C 27
12 بازدید
۱۴۰۴-۰۷-۱۲
10 دقیقه
  • نویسنده: Alireza Abbasi
  • درباره نویسنده: ---
در فرآیند ساخت هر برنامه، مرحلهٔ «لینک کردن» نقش کلیدی ایفا می‌کند. لینکر (Linker) وظیفه دارد فایل‌های شیء تولید شده توسط کامپایلر را ترکیب کرده، بخش‌های مختلف حافظه را با دقت در آدرس‌های صحیح قرار دهد و ارجاع‌های بین ماژول‌های برنامه را برقرار کند. این کار در سیستم‌های نهفته (Embedded Systems) که منابع بسیار محدودند – مانند میکروکنترلرهای کوچک با چند کیلوبایت حافظه – اهمیت دوچندان پیدا می‌کند.

شناخت دقیق عملکرد لینکر نه تنها به ما کمک می‌کند تا برنامه‌ای بهینه از نظر مصرف حافظه بسازیم، بلکه برای اشکال‌زدایی (Debugging)، مدیریت ارتقاء Firmware بدون آسیب به دستگاه و تعریف بخش‌های حافظه خاص ضروری است. لینکر با استفاده از مدل حافظهٔ زبان C، داده‌ها و کد را در بخش‌هایی مانند text, data, bss, stack و heap سازماندهی می‌کند و در نهایت یک فایل نقشه (Map File) تولید می‌کند که موقعیت دقیق هر تابع و متغیر را در حافظه نشان می‌دهد.

  • (object files) تشکیل دهنده یک برنامه و قرار دادن آن‌ها کنار هم است. لینکر باید بداند که چیدمان حافظه دستگاه شما دقیقاً چگونه است تا بتواند برنامه را در حافظه جای‌دهد. همچنین مسئولیت اتصال نمادهای خارجی
  • (external symbols) در یک فایل با تعاریف واقعی آنها در فایل دیگر را بر عهده دارد. این فرآیند، لینک کردن نمادها (linking symbols) نامیده می‌شود.

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

بیایید نگاهی به یک خطای معمولی بیندازیم که نشان می‌دهد درک بهتر لینکر چگونه می‌تواند مفید باشد. فرض کنید سیستمی در حال اجرا دارید که دائم خراب می‌شود. وقتی سیستم کرش می‌کند، یک Stack Trace چاپ می‌کند که در آن پشته‌ی فراخوانی‌ها (Call Stack) نمایش داده می‌شود. این پشته توضیح می‌دهد که چه توالی‌ای از خطاها منجر به بروز مشکل شده است (به لیست 11-1 مراجعه کنید).

یک نمونه Stack Trace

این متن به شما می‌گوید که خطا در تابعی با آدرس x00000000000011360 رخ داده است. از آنجایی که شما برنامه خود را با استفاده از آدرس‌های مطلق (absolute addresses) ننوشته‌اید، دانستن نام تابع برایتان مفیدتر خواهد بود. در اینجا کارایی نقشه‌ی لینکر (linker map) پررنگ می‌شود.

لیست زیر بخشی از نقشه‌ی این برنامه را نشان می‌دهد.

بخشی از نقشه‌ی برنامه‌ی لیست قبلی

ما در لیست اول در آدرس x11360 با خطا مواجه شدیم. در لیست دوم تابع « three » از آدرس 0x1129 شروع می‌شود و تا تابع بعدی در آدرس 0x113e ادامه می‌یابد. درواقع، ما ۱۳ بایت بعد از شروع تابع « three » قرار داریم، بنابراین جایی نزدیک به ابتدای تابع هستیم.

لیست اول نشان می‌دهد که تابع « three » توسط کسی در آدرس 0x1150 فراخوانی شده است. لیست دوم نشان می‌دهد که تابع « two » از آدرس 0x113e تا 0x1153 قرار دارد، پس این تابع، تابع « three » را فراخوانی کرده است. با تحلیل مشابهی، می‌توانیم بفهمیم که تابع « two » توسط تابع « one » و تابع « one » توسط تابع اصلی (main) فراخوانی شده است.

کار لینکر چیست؟

لینکر وظیفه دارد فایل‌های شیء (object files) که برنامه را تشکیل می‌دهند، با هم ترکیب کند تا یک فایل برنامه‌ی واحد ایجاد شود. یک فایل شیء ( object) حاوی کد و داده است که در بخش‌های نامگذاری شده سازماندهی شده‌اند. (نام واقعی بخش‌ها به کامپایلر بستگی دارد. البته برنامه‌نویسان حرفه‌ای می‌توانند نام‌های خاص خود را برای این بخش‌ها انتخاب کنند.)

بخش‌های موجود در فایل شیء، آدرس ثابتی ندارند. به آن‌ها قابل جابجایی (relocatable) گفته می‌شود، یعنی می‌توان آن‌ها را تقریباً در هر جایی قرار داد، اما لینکر آن‌ها را در محل خاصی از حافظه قرار می‌دهد.

چیپ ARM حاوی دو نوع حافظه است: حافظه‌ی دسترسی تصادفی (RAM) و حافظه‌ی فلش (flash memory). متغیرها در حافظه‌ی رم قرار می‌گیرند. یکی از مشکلات این نوع حافظه، از بین رفتن تمام داده‌ها با قطعی برق است. حافظه‌ی فلش، از نظر کاربردی، حافظه‌ی فقط خواندنی (read-only memory) است. (نوشتن در حافظه‌ی فلش، به طور معمول غیرممکن است. البته، با استفاده از روش‌های خاص و پیچیده‌ی سیستم  I/O، افراد متخصص می‌توانند این کار را انجام دهند.) داده‌های موجود در حافظه‌ی فلش با قطع برق سیستم پاک نمی‌شوند.

لینکر داده‌ها را از تمام فایل‌های شیء می‌گیرد و آن‌ها را در حافظه‌ی رم جای می‌دهد. سپس باقیمانده‌ی رم را به دو بخش پشته (stack) و توده (heap) تقسیم می‌کند. داده‌های فقط خواندنی و کد در حافظه‌ی فلش قرار می‌گیرند. این توضیح کمی ساده شده است، اما جزئیات آن را برای بخش‌های بعدی این فصل نگه می‌داریم.

شاید برای شما مفید باشد:
()long در آردوینو

آخرین کاری که لینکر انجام می‌دهد، نوشتن یک فایل نقشه‌ (map file) است که به شما می‌گوید کجا هر بخش از برنامه را در حافظه قرار داده. شاید با خودتان فکر کنید که چرا باید بدانیم لینکر هر قسمت را کجا گذاشته است؛ بالاخره مهم این است که برنامه به درستی در حافظه بارگذاری شود.

با این حال، دلایل مهمی برای اهمیت این موضوع وجود دارد:

  • دیباگ‌کردن (Debugging): هنگام دیباگ برنامه در محیط عملیاتی (field debugging)، دانستن اینکه هر بخش از کد یا داده در کجای حافظه قرار دارد، بسیار ضروری است. این اطلاعات به ما کمک می‌کند تا بفهمیم چه اتفاقی در حال رخ دادن است و مشکلات را راحت‌تر پیدا کنیم.
  • تعریف بخش‌های حافظه‌ی خاص (Specialized Memory Sections): در برخی موارد، ممکن است بخواهیم بخش‌های خاصی از حافظه را با ویژگی‌های منحصر به فرد تعریف کنیم. برای مثال، ممکن است بخواهیم بخشی از حافظه را فقط خواندنی (read-only) یا با سرعت دسترسی بالاتر تعریف کنیم. نقشه‌ی لینکر به ما کمک می‌کند تا ببینیم چه بخش‌هایی از حافظه در حال حاضر استفاده می‌شوند و ما می‌توانیم بخش‌های جدید را در مکان‌های مناسب اختصاص دهیم.
  • الحاق تراشه‌های حافظه‌ی اضافی (Attaching Additional Memory Chips): اگر سیستم ما قابلیت ارتقاء حافظه را داشته باشد، ممکن است بخواهیم تراشه‌های حافظه‌ی اضافی به آن متصل کنیم. نقشه‌ی لینکر به ما کمک می‌کند تا بفهمیم برنامه در حال حاضر از کدام بخش‌های حافظه استفاده می‌کند و چگونه می‌توانیم حافظه‌ی جدید را به درستی ادغام کنیم.

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

حالا به مهم‌ترین دلیل اهمیت لینکر می‌رسیم:

ارتقاء سیستم‌عامل (firmware upgrade)

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

اما چطور می‌توانیم از نرم‌افزاری که در حال اجراست، برای جایگزینی خودِ آن نرم‌افزار استفاده کنیم؟ و مهم‌تر از همه، چطور این کار را بدون آجر کردن (bricking) سیستم انجام دهیم؟ (آجر کردن به وضعیتی گفته می‌شود که فرآیند ارتقاء با شکست مواجه شده و سیستم شما عملاً غیرقابل استفاده می‌شود، درست مثل یک آجر!)

این کار به برنامه‌نویسی ماهرانه‌ای نیاز دارد و در انتهای این فصل به توضیح آن خواهم پرداخت.

مدل‌های حافظه در کامپایل و لینکر

مدل حافظه (Memory Model) نحوهٔ تعریف و مشخص‌سازی حافظه در یک سیستم را توصیف می‌کند.

به‌طور کلی، حافظه به بخش‌هایی با نام‌های مشخص تقسیم می‌شود. استاندارد زبان C، فایل‌های شیء (Object Files)، و تراشهٔ ARM هر یک برای توصیف بخش‌های حافظه از نام‌های متفاوتی استفاده می‌کنند.

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

مدل ایده‌آل حافظه در برنامه‌نویسی C

در حالت ایده‌آل، تمام اجزای یک برنامه‌ی C باید در یکی از بخش‌های استاندارد حافظه قرار بگیرند: text، data یا bss.

دستورالعمل‌های فقط خواندنی (read-only) و داده‌های فقط خواندنی در بخش text قرار می‌گیرند. در اینجا، کد تابع اصلی (main) و هم رشته‌ی متنی (که فقط خواندنی است) در بخش text قرار می‌گیرند:

داده‌های مقداردهی‌شده (متغیرهای سراسری مقداردهی‌شده) در بخش data قرار می‌گیرند:

داده‌های مقداردهی‌نشده (متغیرهای سراسری مقداردهی‌نشده) در بخش bss قرار می‌گیرند:

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

حافظه‌ی این بخش‌ها در زمان کامپایل تخصیص داده می‌شود. کامپایلر C یک فایل شیء (object file) تولید می‌کند که در آن می‌گوید: “به این مقدار فضای text نیاز دارم و محتوای آن این است. به این مقدار فضای data نیاز دارم و محتوای آن این است. به این مقدار فضای bss نیاز دارم، اما محتوایی برای آن مشخص نشده است.”

دستور size میزان فضایی را که برنامه‌ی شما در هر بخش استفاده می‌کند نشان می‌دهد.

فایل شیء از ۴۸۱ بایت حافظه‌ی text ، ۴ بایت حافظه‌ی data و ۴ بایت دیگر از حافظه‌ی bss استفاده می‌کند. کل تعداد بایت‌ها برای هر سه بخش ۴۸۹ یا معادل با 1e9 در مبنای شانزده است.

مدل ایده‌آل حافظه در C دو بخش دیگر نیز دارد. با این حال، این بخش‌ها توسط کامپایلر تخصیص داده نمی‌شوند، بلکه توسط لینکر تخصیص داده می‌شوند. این بخش‌ها پشته (stack) و توده (heap) هستند. پشته برای متغیرهای محلی مورد استفاده قرار می‌گیرند و با فراخوانی رویه‌ها به صورت پویا تخصیص داده می‌شود. heap مجموعه‌ای از حافظه است که می‌تواند به صورت پویا تخصیص داده شده و آزاد شود (توضیحات بیشتر در مورد توده در فصل ۱۳).

شاید برای شما مفید باشد:
‌راه‌اندازی ماژول Laser Emit با آردوینو

کامپایلر بعد از گرفتن تعاریف متغیرها، آن‌ها را به بخش‌های حافظه اختصاص می‌دهد. این بخش‌ها از فضای نامی (namespace) متفاوتی نسبت به نام‌های بخش‌های حافظه‌ی ایده‌آل C استفاده می‌کنند. در برخی موارد نام‌ها مشابه هستند و در برخی موارد کاملاً متفاوت‌اند. کامپایلرهای مختلف و حتی نسخه‌های مختلف یک کامپایلر خاص ممکن است از نام‌های متفاوتی برای این بخش‌ها استفاده کنند.

لیست 11-3 برنامه‌ای را نشان می‌دهد که شامل تمام انواع داده‌های بحث شده تا اینجای کار است.

لیست سوم: نمونه‌هایی از انواع داده

 

بیایید ببینیم کامپایلر GNU GCC با برنامه نمونه ما از لیست سوم چه می‌کند – به طور خاص، چگونه حافظه را برای انواع مختلف متغیرها و داده‌ها اختصاص می‌دهد.

ابتدا، نگاهی به متغیر سراسری مقداردهی‌شده initializedGlobal از لیست سوم می‌اندازیم:

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

دستورالعمل .align به اسمبلر می‌گوید که داده‌های بعدی باید روی یک مرز 4 بایتی تراز شوند. (دو بیت آخر/کم ارزش آدرس باید صفر باشند تا طول متغیر ضریبی از چهار باشد، از این رو .align 2). در نهایت، برچسب initializedGlobal و داده .word 1 وجود دارد.

 

هنگامی که یک متغیر به صفر مقداردهی اولیه می‌شود (initializedToZero در لیست سوم)، کد هم کمی تغییر می‌کند:

در اینجا، کامپایلر از دستور .bss برای قرار دادن متغیر در بخش .bss استفاده می‌کند. همچنین به جای .word از دستور .space استفاده می‌کند که به اسمبلر می‌گوید این متغیر 4 بایت فضای حافظه اشغال می‌کند و باید آن بایت‌ها را با صفر مقداردهی اولیه کند.

حال به سراغ یک متغیر سراسری بدون مقدار اولیه (uninitializedGlobal از فهرست ۱۱-۳) می‌رویم:

بخش .comm به اسمبلر می‌گوید که یک نماد به طول 4 بایت تعریف کند که باید با مرز 4 بایت هم‌تراز باشد. این نماد در بخشی از حافظه به نام ‘COMMON’ قرار می‌گیرد. در این مورد، نام این بخش از حافظه با الگوی نام‌گذاری مدل حافظه ایده‌آل C مطابقت ندارد.

شاید برای شما مفید باشد:
لایه‌های PCB و طراحی برد چند لایه | قسمت 16 آموزش آلتیوم دیزاینر

دستوری که aString را در لیست سوم تعریف می‌کند، یک ثابت رشته‌ای (“A string.”) را نیز تعریف می‌کند. ثابت رشته‌ای فقط خواندنی است، در حالی که اشاره گر (aString) خواندنی/نوشتنی است. در اینجا کد تولید شده آمده است:

ابتدا، کامپایلر باید ثابت “A string.” را تولید کند. برای این کار، یک نام داخلی (.LC0) برای این ثابت ایجاد می‌کند و محتوای این ثابت با دستور اسمبلر .ascii تولید می‌شود. دستور .section .rodata ثابت را در یک بخش پیوند دهنده به نام .rodata قرار می‌دهد. (مدل حافظه ایده‌آل C این بخش را text می‌نامد.)

حال به تعریف خود متغیر، aString می‌رسیم. دستور .data آن را در بخش داده قرار می‌دهد. از آنجایی که این متغیر یک اشاره‌گر است، با آدرس رشته (.LC0) مقداردهی اولیه می‌شود.

آخرین بخش اصلی، بخشی است که حاوی کد برنامه است. مدل حافظه ایده‌آل C این بخش را text می‌نامد. در اینجا لیست اسمبلر برای ابتدای تابع main آمده است:

نام این بخش text.main است. در این مورد، کامپایلر تصمیم گرفته پیشوند text را بردارد و نام ماژول (main) را به آن اضافه کند تا نام بخش را تشکیل دهد.

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

کلمه کلیدی static که خارج از هر رویه‌ای استفاده می‌شود، متغیری را نشان می‌دهد که فقط می‌توان از آن در داخل ماژول فعلی استفاده کرد.

 

این کد متغیر initializedModule را از لیست سوم ایجاد می‌کند:

به نظر می‌رسد این کد خیلی شبیه به initializedGlobal است، با این تفاوت که دستور .global در آن وجود ندارد.

به طور مشابه، متغیر uninitializedModule از لیست سوم نیز شباهت زیادی به uninitializedGlobal دارد، با این تفاوت که باز هم دستور .global در آن وجود ندارد:

حال به سراغ متغیرهایی می‌رویم که با استفاده از کلمه کلیدی static در داخل یک رویه تعریف شده‌اند. این متغیرها زمان کامپایل در حافظه اصلی تخصیص داده می‌شوند، اما دامنه آنها به رویه‌ای که در آن تعریف شده‌اند محدود می‌شود.

بیایید با متغیر uninitializedStatic از لیست سوم شروع کنیم:

به نظر می‌رسد این کد شبیه به یک متغیر محلی بدون مقدار اولیه است، با این تفاوت که کامپایلر نام متغیر را از uninitializedStatic به uninitializedStatic.4108 تغییر داده است. چرا؟

هر بلوک یا بخشی از کد که درون آکولاد ({}) قرار گرفته باشد، می‌تواند متغیر uninitializedStatic خودش را داشته باشد. دامنه نام متغیر C در بلاکی که در آن تعریف شده است، محلی است. اما دامنه زبان اسمبلی کل فایل است؛ بنابراین، کامپایلر با اضافه کردن یک عدد تصادفی منحصربه‌فرد به انتهای اعلان متغیر، نام آن را منحصربه‌فرد می‌کند.

به طور مشابه، متغیر initializedStatic بسیار شبیه به همتای سراسری خود به نظر می‌رسد:

در این مورد، دستور .global وجود ندارد و نام با اضافه شدن یک پسوند تغییر یافته است.

اطلاعات
12
0
0
لینک و اشتراک
profile

نویسنده: Alireza Abbasi

متخصص الکترونیک

ویراستار: MasoudHD
مقالات بیشتر
slide

پالت | بازار خرید و فروش قطعات الکترونیک

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

آیسی | موتور جستجوی قطعات الکترونیک

سامانه آی سی سیسوگ (Isee) قابلیتی جدید و کاربردی از سیسوگ است. در این سامانه سعی شده است که جستجو، انتخاب و خرید مناسب تر قطعات برای کاربران تسهیل شود. جستجو در آیسی
family

سیسوگ‌شاپ | فروشگاه محصولات Quectel

فروشگاه سیسوگ مجموعه ای متمرکز بر تکنولوژی های مبتنی بر IOT و ماژول های M2M نظیر GSM، GPS، LTE، NB-IOT، WiFi، BT و ... جایی که با تعامل فنی و سازنده، بهترین راهکارها انتخاب می شوند. برو به فروشگاه سیسوگ
family

سیسوگ فروم | محلی برای پاسخ پرسش‌های شما

دغدغه همیشگی فعالان تخصصی هر حوزه وجود بستری برای گفتگو و پرسش و پاسخ است. سیسوگ فروم یک انجمن آنلاین است که بصورت تخصصی امکان بحث، گفتگو و پرسش و پاسخ در حوزه الکترونیک را فراهم می‌کند. پرسش در سیسوگ فرم
family

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

بررسی و ارائه اطلاعات مربوط به ECU (واحد کنترل الکترونیکی) و نرم‌افزارهای متن باز مرتبط با آن برو به سیکار
become a writer

نویسنده شو !

سیسوگ با افتخار فضایی برای اشتراک گذاری دانش شماست. برای ما مقاله بنویسید.

ارسال مقاله
become a writer

نویسنده شو !

سیسوگ با افتخار فضایی برای اشتراک گذاری دانش شماست. برای ما مقاله بنویسید.

ارسال مقاله
خانواده سیسوگ
سیسوگ‌شاپ

فروشگاه محصولات Quectel

پالت
سیسوگ فروم

محلی برای پاسخ پرسش‌های شما

سیسوگ جابز
سیسوگ
سیسوگ فروم
سی‌کار

اولین مرجع متن باز ECU در ایران

سیسوگ مگ
آی‌سی

موتور جستجوی قطعات الکترونیکی

سیسوگ آکادمی
پالت

بازار خرید و فروش قطعات الکترونیک

دیدگاه ها

become a writer

نویسنده شو !

سیسوگ با افتخار فضایی برای اشتراک گذاری دانش شماست. برای ما مقاله بنویسید.

ارسال مقاله
become a writer

نویسنده شو !

سیسوگ با افتخار فضایی برای اشتراک گذاری دانش شماست. برای ما مقاله بنویسید.

ارسال مقاله