الفبای معماری حافظه (قسمت دوم – بخش های مختلف حافظه)

0
436
الفبای معماری حافظه (قسمت دوم – بخش های مختلف حافظه)

تو قسمت قبل راجع به کلیات حافظه ها صحبت کردیم. تو این قسمت میخوایم به خورده دقیق تر حافظه ها و قسمت های مختلفشون مثل Heap و Stack رو بررسی کنیم. بذارید قبل این کار دو اصطلاح رو تعریفشونو با هم ببینیم:

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

نکته مهمی که وجود داره اینه که ما برای کامپایل کدهامون نیازی به دونستن اینکه چه پلتفرمی قراره این کدها رو اجرا کنه نداریم و صرفا با اطلاع از معماری هسته پردازشی میشه کدها رو کامپایل کرد ولی برای مرحله Linking & Locating چون میخوایم کدهای کامپایل شده رو روی حافظه پیاده کنیم لازمه بدونیم دقیقا چه نوع حافظه ای با چه سایزی در کنار CPU قرار گرفته پس این فرایند وابستگی زیادی به پلتفرم پیدا میکنه (که میشه این وابستگی رو تو Linking file ای که در اختیار ابزار Linking قرار میدیم به خوبی دید!)

اصطلاح مهم دیگه Memory map هستش به این معنی که تو این فرایند به بخش های مختلف مثه پریفرال ها و یا محل ذخیره کد و … آدرس ها و رنج هایی توی حافظه اختصاص داده میشه که برای کارکردن با اون قسمت لازمه از همون آدرس ها و رنج های مشخص شده استفاده کنیم. تصویر زیر میتونه تو درک این موضوع کمک کنه:

Memory map

باید توجه داشت که تو بعضی پردازنده ها مثه ARM، رجیسترها در این فضای آدرس (Address Space) قرار نمیگیرن و دسترسی بهشون با شیوه متفاوتی انجام میشه. برای دسترسی به رجیسترها تو این حالت از Keyword های خاصی در دستورات اسمبلی استفاده میشه.

خب حالا باید ببینیم داده ها چطور داخل حافظه ها قرار میگیرن. تصویر زیر رو ببینید:

Memory models

همونطور که مشخصه داده ها به صورت Byte هایی در خونه های مختلف حافظه قرار میگیرن، به عبارتی کوچکترین واحدی که ما تو بحث حافظه باهاش کار میکنیم یک Byte هستش.

داده هایی که ما ذخیره میکنیم از دو قسمت اصلی code و data تشکیل شده که هر کدوم از این دو بخش هم زیر قسمت هایی داره که در ادامه باهاشون بیشتر آشنا خواهیم شد:

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

تو بخش Data ما چهار زیر بخش مهم داریم که اندازه هرکدوم با توجه به نحوه برنامه نویسی ما میتونه کم یا زیاد شه:

  • بخش Stack که داده های موقتی مثه متغیرهای محلی رو تو خودش نگه داری میکنه
  • بخش Heap که داده های دینامیک که زمان اجرا ایجاد میشن و نیاز به ذخیره سازی دارن رو نگه داری میکنه
  • بخش data متغیر های Global که با صفر مقدار دهی نشدند یا متغیرهایی که به صورت استاتیک تعریف شدند و با صفر مقداردهی نشدند رو نگهداری میکنه
  • بخش BSS هم متغیر های Global که با صفر مقدار دهی شدند یا کلا مقدار دهی نشدند و همچنین متغیرهایی که به صورت استاتیک تعریف شدند و با صفر مقداردهی شدند یا کلا مقدار دهی نشدند رو نگهداری میکنه

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

خب حالا که با محل ذخیره داده های مختلف آشناشدیم بد نیست راجع به Life time داده های در برنامه هم صحبت کنیم.به طور کلی داده های یک برنامه سه نوع Life time دارند:

  • تعداد متغیرهای محلی
  • تعداد ورودی های تابع
  • سایز و نوع متغیرهای محلی
  • سایز و نوع پارامترهای ورودی
  • سایز و نوع داده خروجی
  • تعداد سابروتین های تودرتو که در تابع فراخوانی میشن
  • وقفه یا وقفه های تودرتو

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

یه راهکار استفاده از حافظه Flash هستش که خب چون برای استفاده ازش نیاز به یه سری دسترسی ها هست و همچنین تاخیر زیادی هم داره چندان این روش توصیه نمیشه.

روش دوم و عملی که استفاده میشه، استفاده از یه حافظه غیرفرار جانبی به اسم EEPROM هستش که در کنار میکروها (و بعضا داخل میکرو) قرار میگیره و میتونه این داده ها رو برامون نگهداری کنه. داده هایی که قراره تو این حافظه قرار بگیرن تو یه فایل با پسوند eeprom.* ذخیره میشن.

در ادامه حافظه مهم Stack رو باهم بیشتر بررسی میکنیم. این حافظه در زمان کامپایل اختصاص پیدا میکنه ولی در زمان اجرای برنامه مقادیرش دستخوش تغییر میشه. استک شامل چندین فریم هستش که در هر فریم مواردی که ذخیره میشن رو تصویر زیر به خوبی نشون میده:

همونطور که از این تصویر مشخصه عملا بدون وجود استک ما قادر نیستیم از قابلیت های مهمی مثه وقفه ها استفاده کنیم. به همین خاطر هم نیازه که حواسمون به Stack overflow باشه همیشه تا اطلاعات مهمی مثه آدرس ادامه برنامه یا رجیسترهای ذخیره شده تو استک از دست نرند.

موارد زیر میتونن در اندازه هر فریم تاثیر گذار باشند :

  • تعداد متغیرهای محلی
  • تعداد ورودی های تابع
  • سایز و نوع متغیرهای محلی
  • سایز و نوع پارامترهای ورودی
  • سایز و نوع داده خروجی
  • تعداد سابروتین های تودرتو که در تابع فراخوانی میشن
  • وقفه یا وقفه های تودرتو

تصویر زیر هم مفهوم وجود چند فریم در یک استک رو نشون میده:

Stack Frames

برای کار با استک از دو دستور مهم استفاده میشه. دستور Push که داده ها رو از رجیسترها داخل استک کپی میکنه و دستور Pop که داده ها رو از استک خارج کرده و درون رجیسترها ذخیره میکنه. این دستورات این قابلیت رو دارند که قطعات مختلفی از داده ها رو با یک دستور انتقال بدن.

بخش مهم دیگه ای که میخوایم بررسی کنیم حافظه Heap هستش که به حافظه دینامیک هم شهرت داره. مشابه استک انتخاب هیپ در زمان کامپایل صورت میگیره و مقادیرش در زمان اجرای برنامه میتونه تغییر کنه. در هر بار اختصاص حافظه برنامه نویس میتونه بسته به نیاز هر مقدار بایتی رو که میخواد به متغیرش اختصاص بده و همچنین این امکان رو داره که این مقداری رو که اختصاص میده در طول برنامه در صورت نیاز تغییر بده.

برای کار با حافظه Heap چهار نوع دستور وجود داره که میتونیم تو برنامه ازشون استفاده کنیم. اولین دستور malloc هستش که به عنوان ورودی سایز حافظه ای رو که میخوایم اختصاص بدیم دریافت میکنه.

دستور بعدی calloc هستش که به عنوان ورودی تعداد المان های حافظه و اندازه هر المان برحسب بایت رو دریافت میکنه و به اندازه حاصلضرب این دو عدد تو حافظه فضا بهمون اختصاص میده. این دستور همچنین فضاهایی رو که بهمون اختصاص میده با مقدار صفر مقداردهی میکنه.

دو دستور بالا یک حافظه پیوسته رو در محلی از Heap بهمون اختصاص میدن به نحوی که حافظه دچار overflow نشه.

دستور سوم realloc هست که برای تغییر سایز حافظه ای که قبلا اختصاص داده شده استفاده میشه. به عنوان ورودی هم این دستور پوینتر به حافظه از پیش اختصاص داده شده و مقدار جدیدی که برای حافظه در نظر داریم رو دریافت میکنه. این دستور فضای جدیدی رو تو حافظه ایجاد میکنه و فضای قبلی رو هم به صورت اتوماتیک آزاد میکنه.

هرکدوم از سه دستور بالا اگه با موفقیت اجرا نشن (بنا به دلایل مختلف مثلا اینکه حافظه گنجایش نداشته باشه) یه NULL pointer بر میگردونن.

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

یه نکته جالب راجع به این حافظه اینه که برای اختصاص بخشی ازش لازمه تمام اون بخش به صورت یکپارچه اختصاص داده بشه. بذارید با یه مثال توضیح بدم.فرض کنید شما یه حافظه Heap با 80 بایت گنجایش دارید که از قبل یه حافظه 32 بایتی درست وسط هیپ اختصاص داده شده. حالا شما میخواید 40 بایت دیگه به یه قسمت دیگه اختصاص بدید ولی درکمال تعجب میبینید این کار با شکست مواجه میشه! دلیلش سادست چون شما 40 بایت به صورت یکپارچه ندارید بلکه دو تیکه 16 بایتی و 32 بایتی در بالا وپایین هیپ دارید که هیچ کدوم نمیتونه 40 بایت به صورت یکپارچه در اختیارتون بذاره.

به طور کلی برنامه نویس ها استفاده از حافظه Heap رو بنا به دلایل مختلف که اینجا مجال توضیحش نیست توصیه نمی کنند.

 

منبع:سیسوگ

مطلب قبلیماجرای اولین هک سخت افزار!
مطلب بعدیآموزش STM32 با توابع LL قسمت پنجم: GPIO-Output

پاسخ دهید

لطفا نظر خود را وارد کنید!
لطفا نام خود را در اینجا وارد کنید