در مباحث قبلی، به بررسی بخشهای حافظه استاندارد که توسط زنجیره ابزار GNU تولید میشوند، پرداختیم. تراشههای STM32 از یک بخش سفارشی به نام .isr_vector استفاده میکنند. این بخش باید اولین دادهای باشد که در حافظه فلش برنامهریزی میشود، چرا که سختافزار ARM از این بخش حافظه برای مدیریت وقفهها و سایر عملکردهای مرتبط با سختافزار استفاده میکند. جدول 11-1 که از راهنمای STM32F030x4 آورده شده است، جزئیات مربوط به بردار وقفه را شرح میدهد.
| موقعیت | اولویت | نوع اولویت | نام اختصاری | توضیحات | آدرس |
| – | – | – | – | رزرو شده | 0x00000000 |
| – | -3 | ثابت | Reset | بازنشانی | 0x00000004 |
| – | -2 | ثابت | NMI | وقفه غیر قابل ماسک | 0x00000008 |
| – | -1 | ثابت | Hard | تمام کلاسهای خطا | 0x0000000c |
| – | 3 | قابلتنظیم | SVC | تماس سیستمی از طریق دستورالعمل SWI | 0x0000002c |
| – | 5 | قابلتنظیم | PendSV | درخواست قابل تعلیق برای خدمات سیستمی | 0x00000038 |
| – | 6 | قابلتنظیم | SysTick | تایمر تیک سیستم | 0x0000003c |
| 0 | 7 | قابلتنظیم | WWDG | وقفه window watchdog | 0x00000040 |
| 1 | – | رزرو شده | 0x00000044 | ||
| 2 | 9 | قابلتنظیم | RTC | وقفههای RTC (ترکیب خطوط EXTI 17، 19 و 20) | 0x00000048 |
مستندات بردار وقفه (خلاصه)
فایل فریمور STM با نام startup_stm32f030x8.s که یک فایل زبان اسمبلی است، کد مربوط به تعریف این جدول (جدول بردار وقفه) را در خود جایداده است. در اینجا بخشی از این فایل آمده است:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
131 .section .isr_vector,"a",%progbits 134 135 136 g_pfnVectors 137 0000 00000000 .word _estack 138 0004 00000000 .word Reset_Handler 139 0008 00000000 .word NMI_Handler 140 000c 00000000 .word HardFault_Handler |
خط اول به لینکر میگوید که جدول را در بخشی به نام .isr_vector قرار دهد. این بخش وابستگی شدیدی به سختافزار دارد، تعریف دقیقی دارد و باید به طور مطلق در جای درست قرار گیرد. در غیر این صورت سیستم کار نخواهد کرد.
کد بالا، یک آرایه به نام g_pfnVectors تعریف میکند که شامل موارد زیر است:
نحوه مدیریت این کد توسط لینکر را در بخش بعدی بررسی خواهیم کرد.
کامپایلر و اسمبلر مجموعهای از فایلهای آبجکت (object files) تولید میکنند که کد و داده را به بخشهای زیر تقسیم میکنند:
لینکر توسط اسکریپتی به نام LinkerScript.ld کنترل میشود که بخشی از هر پروژه STM32 Workbench است. این اسکریپت به لینکر میگوید که حافظه سیستم از دو بخش تشکیل شده است:
وظیفه لینکر گرفتن دادهها از فایلهای آبجکت و جایگذاری آنها با مراحل زیر در حافظه است:
بخش .data بخش دردسرساز ماجراست. این اعلان را در نظر بگیر:
int initializedGlobal = 1234;
لینکر برای initializedGlobal در RAM فضا تخصیص میدهد. مقدار اولیه (1234) در فلش قرار میگیرد. در زمان راهاندازی (startup)، مقادیر اولیه بهصورت یک بلوک از فلش به RAM کپی میشوند تا بخش .data مقداردهی اولیه شود.
پس از ریست، کدی که در فایل startup_stm32f030x8.S قرار دارد اجرا میشود و مراحل زیر را انجام میدهد:
هنگام راهاندازی مجدد، کد موجود در فایل startup_stm32f030x8.S اجرا میشود و مراحل زیر را انجام میدهد:
دو نوع فایل object وجود دارد: absolute و relocatable.
یک فایل absolute همه چیز را بر اساس یک آدرس ثابت (absolute) تعریف میکند. یعنی مثلاً سمبل main در آدرس 0x7B0 قرار دارد و لینکِر یا هیچ ابزار دیگری نمیتواند آن را به آدرس دیگری منتقل کند.
اما یک فایل object از نوع relocatable طوری طراحی شده که محل قرارگیری دادهها و کدهایش قابلجابهجایی (relocate) باشد. مثلاً فایل main.c وقتی کامپایل میشود، فایل main.o تولید میکند. اگر اسمبلی این فایل را نگاه کنیم، میبینیم سمبل main در آدرس 0000 تعریف شده است:
|
1 2 3 4 5 6 7 8 |
52 .section .text.main,"ax",%progbits --snip-- 60 main: 61 .LFB0: --snip-- 67 0000 80B5 push {r7, lr} |
این نماد به بخشی که در آن قرار دارد (یعنی text.main) وابسته است. ازآنجاییکه فایل شیء (object file) قابلجابهجایی است، text.main میتواند در هر جایی از حافظه قرار گیرد. در این مورد، لینکر (Linker) تصمیم گرفته است آن را در حافظه فلش با آدرس 0x00000000080007b0 قرار دهد. (این مقدار را با استفاده از نقشه لینکر که در بخش بعدی توضیح داده میشود، پیدا کردیم). ازآنجاییکه main در ابتدای این بخش قرار دارد، مقدار 0x00000000080007b0 را به خود میگیرد.
بهعنوان بخشی از فرایند، لینکر فایلهای شیء قابلجابهجایی را گرفته و آنها را در یک مکان خاصی از حافظه قرار میدهد. نتیجه، یک فایل برنامه با آدرسهای مطلق برای هر فایل شیء است.
همچنین لینکر فایلهای شیء را به هم پیوند میدهد. برای مثال، فایل startup_stm32f030x8.S تابع main را فراخوانی میکند. مشکل این کد است که نمیداند تابع main کجا قرار دارد. تابع main در ماژول دیگری (main.o) تعریف شده است، بنابراین در زمان لینک، لینکر متوجه میشود که startup_stm32f030x8.S نیاز دارد بداند تابع main در کجا تعریف شده است پس یک عملیات لینک بین فراخوانی main در startup_stm32f030x8.S و آدرس مطلق main (0x7B0) انجام میدهد.
یک کتابخانه مجموعهای از فایلهای شیء (object) است که با فرمت بایگانی (شبیه به .zip، ولی سادهتر) ذخیره میشوند. اسکریپت لینکر به لینکر دستور میدهد کتابخانههای libc.a، libm.a و libgcc.a را در برنامه نهایی قرار دهد. بهعنوانمثال، کتابخانهی libm.a شامل فایلهای شیء زیر است:
|
1 2 3 4 5 6 |
s_sin.o (تابع سینوس) s_tan.o (تابع تانژانت) s_tanh.o (تابع هایپر تانژانت) s_fpclassify.o (طبقهبندی اعداد ممیز) s_trunc.o (گرد کردن به سمت پایین) s_remquo.o (باقیمانده و بهره) |
هنگام پردازش یک کتابخانه، لینکر فقط فایلهایی از شیء را بارگذاری میکند که حاوی نماد موردنیاز برنامه شما باشند. بهعنوانمثال، اگر برنامه شما از تابع سینوس (sin) استفاده کند، لینکر فایل شیء s_sin.o که این تابع را تعریف میکند، در برنامه نهایی قرار خواهد داد. اما اگر از تابع سینوس استفاده نکنید، لینکر متوجه میشود که به کد موجود در s_sin.o نیازی ندارید و در نتیجه آن را در برنامه نهایی قرار نمیدهد.
هنگام بارگذاری دادهها توسط لینکر در برنامه، یک فایل نقشه به نام Debug/output.map ایجاد میشود. این فایل شامل اطلاعاتی در مورد محل قرارگیری کد و دادههای ما است.
فایل نقشه بسیار جامع است و حاوی اطلاعات مفید و همچنین جزئیات کماهمیتتری است. بهعنوانمثال، این فایل، پیکربندی حافظهی ما را نشان میدهد که انواع مختلف حافظه و موقعیت آنها را برای پردازندهی ما مشخص میکند:
| نام | نشانی شروع (Origin) | طول (Length) | ویژگیها (Attributes) |
| FLASH | 0x0000000008000000 | 0x0000000000010000 | خواندنی (r) و اجرایی (x) |
| RAM | 0x0000000020000000 | 0x0000000000002000 | خواندنی (r)، نوشتنی (w)، و اجرایی (x) |
| پیشفرض (default) | 0x0000000000000000 | 0xffffffffffffffff | – |
در این مورد، تراشهی ما دارای حافظهی FLASH است که میتوان از آن برای خواندن (r) و اجرای کد (x) استفاده کرد. این حافظه از آدرس 0x8000000 شروع شده و به اندازهی 0x10000 بایت ادامه دارد. بخش RAM از آدرس 0x20000000 شروع شده و فقط به اندازهی 0x2000 بایت ادامه دارد. این بخش حافظه قابل خواندن (r)، نوشتن (w) و اجرا (x) است.
همانطور که قبلاً ذکر شد، بخش .isr_vector اولین بخشی است که بارگذاری میشود. نقشه لینکر محل قرارگیری این بخش را به ما نشان میدهد:
|
1 |
.isr_vector 0x0000000008000000 0xc0 |
آدرس 0x8000000 شروع حافظهی FLASH است. سختافزار انتظار دارد که بردار وقفه (interrupt vector) در این آدرس قرار داشته باشد. اطلاعات دیگری که میبینم طول این بخش 0xc0 بایت است.
نماد اصلی (main) در فایل src/main.o تعریف شده است. این نماد بخشی از سگمنت .text.main بوده و در آدرس 0x0000000008000138 قرار دارد:
|
1 |
.text.main 0x00000000080000138 0x60 src/main.o 0x0000000008000138 main |
همچنین این بخش شامل قطعهای از کد (0x60 بایت) است که باتوجهبه خالی بودن کد ۱۱-۳، حجیم به نظر میرسد.
همچنین میتوانیم ببینیم که متغیرهای سراسری ما کجا قرار دارند. برای مثال، در اینجا موقعیت برای uninitializedGlobal آمده است:
|
1 2 3 4 5 |
COMMON 0x0000000020000464 0x4 src/main.o 0x0000000020000464 uninitializedGlobal |
فایل linker map آدرس مطلق (absolute address) همهی متغیرها و تابعهای موجود در برنامه را نشان میدهد. این به چه دردی میخورد؟ وقتی در شرایط واقعی (بدون JTAG debugger) در حال دیباگکردن هستیم، معمولاً فقط آدرسهای مطلق در دسترس ما هستند؛ بنابراین اگر برنامه دچار یک خطای جدی شود و در کنسول دیباگ فقط این را ببینید:
|
1 |
FATAL ERROR: Address 0x0000000008000158 |
میفهمید که خطا 20 بایت بعد از شروع تابع main رخ داده است.
ما با برد STM خودمان از یک دیباگر خارجی استفاده کردهایم. این سیستم شامل یک کامپیوتر میزبان که دیباگر روی آن اجرا میشود، یک ماژول JTAG، و یک دستگاه هدف (target) است. دیباگر روی کامپیوتر میزبان به کد منبع و جدول سمبلها (symbol table) که توسط لینکِر تولید میشود دسترسی دارد. وقتی خطایی در آدرس 0x8000158 تشخیص دهد، میتواند در جدول سمبلها نگاه کند، ببیند که این آدرس دقیقاً 20 بایت بعد از شروع برنامه یا تابع مربوطه است، تشخیص بدهد خطا در کدام خط کد رخداده، و سپس یک فلش قرمز بزرگ در فایل سورس نشان دهد تا محل دقیق خطا را مشخص کند.
برخی از سیستمها دارای یک دیباگر داخلی هستند که خود دیباگر و تمام فایلهای موردنیاز آن روی سیستم هدف قرار دارند. برخی از این دیباگرهای داخلی امکان ریختن حافظه بر اساس آدرسهای مطلق را فراهم میکنند. این دیباگرها کوچک و ساده هستند، همچنین در عیبیابی میدانی میتوانند به طرز شگفتانگیزی مفید باشند.
فرض کنید چنین دیباگری دارید و میخواهید مقدار متغیر سراسری «uninitializedGlobal» را بدانید. یک دیباگر ساده هیچچیزی در مورد نامهای نماد نمیداند. این دیباگر حافظه را بر اساس آدرس تخلیه میکند و کار دیگری انجام نمیدهد.
از طرف دیگر، شما از نامهای نماد اطلاع دارید همچنین نقشهی لینکر (linker map) را در اختیار دارید، بنابراین میتوانید به دیباگر بگویید یک مقدار ۴ بایتی را در موقعیت 0x20000464 نمایش دهد:
|
1 2 3 |
D> x/4 20000464 0x20000464: 1234 0x4D2 |
این نوع دیباگکردن خیلی ابتدایی و سخت است، اما گاهی در سیستمهای امبدد تنها راهی است که میشود از آن استفاده کرد.
شاید بپرسید چرا ما به دیباگر نمیگوییم که متغیر uninitializedGlobal کجاست تا کار راحتتر شود. مشکل اینجاست که جدول سمبلها (symbol table) حجم زیادی از حافظه را اشغال میکند، درحالیکه ما محدودیت حافظه داریم. علاوه بر این، داشتن جدول سمبلها داخل خود سیستم یک ریسک امنیتی است. (یک هکر عاشق این است که بداند آدرس تابع passwordCheckingFunction کجاست!)
سیسوگ با افتخار فضایی برای اشتراک گذاری دانش شماست. برای ما مقاله بنویسید.