إذا كنت تقوم بالبرمجة على جهاز كمبيوتر "كبير" ، فربما لا يكون لديك مثل هذا السؤال. هناك الكثير من المكدس لتجاوزه ، تحتاج إلى المحاولة. في أسوأ الأحوال ، انقر فوق "موافق" في نافذة مثل هذه ثم انتقل إلى حل المشكلة.
ولكن إذا قمت ببرمجة وحدات التحكم الدقيقة ، فستبدو المشكلة مختلفة قليلاً. تحتاج أولاً إلى
ملاحظة أن المكدس ممتلئ.
في هذه المقالة سأتحدث عن بحثي الخاص حول هذا الموضوع. نظرًا لأنني أبرمج بشكل أساسي في إطار STM32 وتحت Milander 1986 - فقد ركزت عليهم.
مقدمة
دعونا نتخيل أبسط حالة - نكتب رمزًا بسيطًا واحدًا مترابطًا بدون أي أنظمة تشغيل ، أي لدينا مكدس واحد فقط. وإذا كنت ، مثلي ، تبرمج في uVision Keil ، يتم توزيع الذاكرة بطريقة أو بأخرى على النحو التالي:

وإذا كنت ، مثلي ، تعتبر الذاكرة الديناميكية على المتحكم الدقيق شريرة ، فعندئذٍ:

بالمناسبةإذا كنت تريد حظر استخدام كومة الذاكرة المؤقتة ، يمكنك القيام بذلك على النحو التالي:
#pragma import(__use_no_heap_region)
التفاصيل
هنا حسنًا ، ما المشكلة؟ المشكلة هي أن Keil يضع المكدس
مباشرة خلف منطقة البيانات الثابتة. والصف في Cortex-M ينمو في اتجاه تناقص العناوين. وعندما يفيض ، فإنه ببساطة يزحف خارج الجزء المخصص من الذاكرة. ويحل محل أي متغيرات ثابتة أو عالمية.
عظيم بشكل خاص إذا تجاوز المكدس فقط عند دخول المقاطعة. أو ، بشكل أفضل ، في مقاطعة متداخلة! ويفسد بهدوء بعض المتغيرات المستخدمة في قسم مختلف تمامًا من التعليمات البرمجية. وتعطل البرنامج على التأكيد. إذا كنت محظوظا. heisenbag الكروية ، يمكن للمرء أن يبحث عن مثل هذا الأسبوع كله مع مصباح يدوي.
قم بإجراء حجز على الفور إذا كنت تستخدم كومة ، فإن المشكلة لا تذهب إلى أي مكان ، فقط بدلاً من المتغيرات العالمية تفسد كومة الذاكرة المؤقتة. ليس أفضل بكثير.
حسنًا ، المشكلة واضحة. ماذا تفعل
MPU
أبسطها وأكثرها وضوحًا هو استخدام MPU (بمعنى آخر ، وحدة حماية الذاكرة). يسمح لك بتعيين سمات مختلفة لأجزاء مختلفة من الذاكرة ؛ على وجه الخصوص ، يمكنك إحاطة المكدس بمناطق للقراءة فقط والتقاط MemFault عند الكتابة هناك.
على سبيل المثال ، في stm32f407 MPU هي. لسوء الحظ ، في العديد من STM "صغار" أخرى ليست كذلك. وفي Milandrovsky 1986VE1 ليس هناك أيضا.
على سبيل المثال الحل جيد ، ولكن ليس دائمًا بأسعار معقولة.
تحكم يدوي
عند الترجمة ، يمكن لـ Keil إنشاء (ويفعل ذلك بشكل افتراضي) تقرير html مع رسم بياني للمكالمات (خيار الرابط "--info = stack"). ويقدم هذا التقرير أيضًا معلومات حول المكدس المستخدم. يمكن لدول مجلس التعاون الخليجي القيام بذلك أيضًا (خيار -ststack- استخدام). وفقًا لذلك ، يمكنك أحيانًا الاطلاع على هذا التقرير (أو كتابة نص برمجي يفعل ذلك نيابة عنك ، والاتصال به قبل كل بنية).
علاوة على ذلك ، في بداية التقرير ، يتم كتابة مسار يؤدي إلى أقصى استخدام للمكدس:

تكمن المشكلة في أنه إذا كانت شفرتك تحتوي على استدعاءات وظيفية بواسطة مؤشرات أو طرق افتراضية (ولديها) ، فإن هذا التقرير يمكن أن يقلل بشكل كبير من أقصى عمق للمكدس. حسنًا ، المقاطعات ، بالطبع ، لا تؤخذ في الاعتبار. ليست طريقة موثوقة للغاية.
وضع المكدس صعبة
تعلمت عن هذه الطريقة من
هذه المقالة . المقالة حول الصدأ ، لكن الفكرة الرئيسية هي:

عند استخدام دول مجلس التعاون الخليجي ، يمكن القيام بذلك باستخدام "
الرابط المزدوج ".
وفي Keil ، يمكن تغيير موقع المناطق باستخدام البرنامج النصي الخاص بك للرابط (ملف مبعثر في مصطلحات Keil). للقيام بذلك ، افتح خيارات المشروع وقم بإلغاء تحديد "استخدام تخطيط الذاكرة من مربع حوار الهدف". ثم سيظهر الملف الافتراضي في حقل "ملف مبعثر". يبدو شيء مثل هذا:
; ************************************************************* ; *** Scatter-Loading Description File generated by uVision *** ; ************************************************************* LR_IROM1 0x08000000 0x00020000 { ; load region size_region ER_IROM1 0x08000000 0x00020000 { ; load address = execution address *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) } RW_IRAM1 0x20000000 0x00005000 { ; RW data .ANY (+RW +ZI) } }
ماذا تفعل بعد ذلك؟ الخيارات الممكنة. تقترح
الوثائق الرسمية تحديد الأقسام بأسماء محجوزة - ARM_LIB_HEAP و ARM_LIB_STACK. ولكن هذا ينطوي على عواقب غير سارة ، على الأقل بالنسبة لي - يجب تعيين حجم المكدس والكومة في ملف المبعثر.
في جميع المشاريع التي أستخدمها ، يتم تعيين أحجام المكدس والأكوام في ملف بدء تشغيل المجمّع (الذي ينشئه Keil عند إنشاء المشروع). لا أريد حقًا تغييره. أريد فقط تضمين ملف مبعثر جديد في المشروع ، وسيكون كل شيء على ما يرام. لذلك ذهبت بطريقة مختلفة قليلا:
المفسد #! armcc -E ; with that we can use C preprocessor #define RAM_BEGIN 0x20000000 #define RAM_SIZE_BYTES (4*1024) #define FLASH_BEGIN 0x8000000 #define FLASH_SIZE_BYTES (32*1024) ; This scatter file places stack before .bss region, so on stack overflow ; we get HardFault exception immediately LR_IROM1 FLASH_BEGIN FLASH_SIZE_BYTES { ; load region size_region ER_IROM1 FLASH_BEGIN FLASH_SIZE_BYTES { ; load address = execution address *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) } ; Stack region growing down REGION_STACK RAM_BEGIN { *(STACK) } ; We have to define heap region, even if we don't actually use heap REGION_HEAP ImageLimit(REGION_STACK) { *(HEAP) } ; this will place .bss region above the stack and heap and allocate RAM that is left for it RW_IRAM1 ImageLimit(REGION_HEAP) (RAM_SIZE_BYTES - ImageLength(REGION_STACK) - ImageLength(REGION_HEAP)) { *(+RW +ZI) } }
ثم قلت أنه يجب وضع جميع الكائنات المسماة STACK في منطقة REGION_STACK ، ويجب وضع جميع كائنات HEAP في منطقة REGION_HEAP. وكل شيء آخر في المنطقة RW_IRAM1. وقام بترتيب المناطق بهذا الترتيب - بداية العامل ، المكدس ، الكومة ، كل شيء آخر. الحساب هو أنه في ملف بدء تشغيل المجمّع يتم تعيين المكدس والكومة باستخدام هذا الرمز (أي ، كمصفوفات بأسماء STACK و HEAP):
المفسد Stack_Size EQU 0x00000400 AREA STACK, NOINIT, READWRITE, ALIGN=3 Stack_Mem SPACE Stack_Size __initial_sp Heap_Size EQU 0x00000200 AREA HEAP, NOINIT, READWRITE, ALIGN=3 __heap_base Heap_Mem SPACE Heap_Size __heap_limit PRESERVE8 THUMB
حسنًا ، قد تسأل ، ولكن ماذا يمنحنا ذلك؟ وهنا ما. الآن ، عند الخروج من المكدس ، يحاول المعالج كتابة (أو قراءة) ذاكرة غير موجودة. وعلى STM32 ، يحدث انقطاع بسبب استثناء - HardFault.
هذا ليس مناسبًا مثل MemFault بسبب MPU ، لأن HardFault يمكن أن يحدث لأسباب عديدة ، ولكن على الأقل الخطأ مرتفع وغير هادئ. على سبيل المثال يحدث على الفور ، وليس بعد فترة زمنية غير معروفة ، كما كان من قبل.
أفضل ما في الأمر أننا لم ندفع أي شيء مقابل ذلك ، لا وقت تشغيل علني! واو. لكن هناك مشكلة واحدة.
هذا لا يعمل على Milander.نعم بالطبع ، على Milandra (أنا مهتم بشكل رئيسي في 1986BE1 و BE91) ، تبدو بطاقة الذاكرة مختلفة. في STM32 ، قبل بدء العامل ، لا يوجد شيء ، وعلى ميلاندرا ، قبل المنطوق ، تقع منطقة الحافلة الخارجية.
ولكن حتى إذا لم تستخدم حافلة خارجية ، فلن تتلقى أي HardFault. أو ربما احصل عليه. أو ربما تحصل عليه ، ولكن ليس على الفور. لم أتمكن من العثور على أي معلومات حول هذا الموضوع (وهذا ليس مفاجئًا لميلاندر) ، ولم تقدم التجارب أي نتائج واضحة. يحدث HardFault في
بعض الأحيان إذا كان حجم المكدس من مضاعفات 256. في بعض الأحيان يحدث HardFault إذا ذهب المكدس إلى حد بعيد في الذاكرة غير الموجودة.
ولكن لا يهم حتى. إذا لم يحدث HardFault في كل مرة ، فإن نقل المكدس إلى بداية ذاكرة الوصول العشوائي لم يعد يوفر علينا. ولكي نكون صادقين تمامًا ، فإن STM ليست ملزمة أيضًا بإلقاء استثناء في نفس الوقت ، يبدو أن المواصفات الأساسية لـ Cortex-M لا تقول شيئًا ملموسًا حول هذا الأمر.
لذلك حتى في STM ، يبدو الأمر أشبه بالقرصنة ، ولكن ليس قذرًا جدًا.
لذا ، تحتاج إلى البحث عن طريقة أخرى.
نقطة الوصول في السجل
إذا قمنا بنقل المكدس إلى بداية ذاكرة الوصول العشوائي ، فستكون القيمة الحدية للمكدس هي نفسها دائمًا - 0x20000000. ويمكننا فقط وضع نقطة توقف على السجل في هذه الخلية. يمكن القيام بذلك باستخدام الأمر وحتى التسجيل في التشغيل التلقائي باستخدام ملف .ini:
// breakpoint on stackoverflow BS Write 0x20000000, 1
لكن هذه ليست طريقة موثوقة للغاية. سيتم إطلاق نقطة التوقف هذه في كل مرة يتم فيها تهيئة المكدس. من السهل التغلب عليه عن طريق الخطأ بالنقر على "اقتل جميع نقاط التوقف". وسوف يحميك فقط في وجود مصحح. ليس جيد
حماية التدفق الديناميكي
قادني بحث سريع حول هذا الموضوع إلى خيارات Keil --protect_stack و --protect_stack_all. لسوء الحظ ، الخيارات المفيدة لا تحمي من تجاوز المكدس بأكمله ، ولكن من ظهور وظيفة أخرى في إطار المكدس. على سبيل المثال ، إذا تجاوز رمزك حدود صفيف أو فشل مع عدد متغير من المعلمات. يمكن لمجلس التعاون الخليجي بالطبع أن يفعل ذلك أيضًا (-stack-protector).
جوهر هذا الخيار هو كما يلي: "متغير الحراسة" يضاف إلى كل إطار مكدس ، أي رقم الحارس. إذا تم تغيير هذا الرقم بعد الخروج من الوظيفة ، فسيتم استدعاء وظيفة معالج الأخطاء. التفاصيل
هنا .
شيء مفيد ، ولكن ليس بالضبط ما أحتاج. أحتاج إلى فحص أبسط بكثير - بحيث عند إدخال كل وظيفة ، يتم التحقق من قيمة سجل SP (مؤشر المكدس) مقابل الحد الأدنى المعروف سابقًا. ولكن لا تكتب هذا الاختبار بيديك عند مدخل كل وظيفة؟
تحكم SP ديناميكي
لحسن الحظ ، لدى gcc الخيار الرائع "-finstrument-وظائف" ، والذي يسمح لك باستدعاء وظيفة معرفة من قبل المستخدم عند إدخال كل وظيفة وعند الخروج من كل وظيفة. يُستخدم هذا عادةً لإخراج معلومات التصحيح ، ولكن ما الفرق؟
لحسن الحظ ، يقوم Keil بنسخ وظيفة مجلس التعاون الخليجي عمدا ، وهناك نفس الخيار متاح تحت اسم "--gnu_instrument" (
التفاصيل ).
بعد ذلك ، تحتاج فقط إلى كتابة هذا الرمز:
وفويلا! الآن ، عند إدخال كل وظيفة (بما في ذلك معالجات المقاطعة) ، سيتم إجراء فحص لتجاوز سعة المكدس. وإذا تجاوزت المكدس ، فسيكون هناك تأكيد.
شرح صغير:- نعم ، بالطبع ، تحتاج إلى التحقق من تجاوز الحد مع بعض الهامش ، وإلا فهناك خطر "القفز" فوق الجزء العلوي من المكدس.
- Image $$ REGION_STACK $$ RW $$ Base هو سحر خاص للحصول على معلومات حول مناطق الذاكرة باستخدام الثوابت التي تم إنشاؤها بواسطة الرابط. التفاصيل (على الرغم من عدم وضوحها في الأماكن) هنا .
هل الحل مثالي؟ بالطبع لا.
أولاً ، هذا الفحص بعيد عن كونه مجانيًا ، حيث يتضخم الرمز منه بنسبة 10 بالمائة. حسنًا ، يعمل الرمز بشكل أبطأ (على الرغم من أنني لم أقم بقياسه). سواء كان الأمر حاسمًا أم لا ، فالأمر متروك لك ؛ في رأيي ، هذا سعر معقول للأمن.
ثانيًا ، لن يعمل هذا على الأرجح عند استخدام مكتبات مُجمَّعة مسبقًا (ولكن نظرًا لأنني لا أستخدمها على الإطلاق ، لم أتحقق).
ولكن من المحتمل أن يكون هذا الحل مناسبًا للبرامج متعددة الخيوط ، نظرًا لأننا نقوم بالتحقق بأنفسنا. لكنني لم أفكر حقًا في هذه الفكرة ، لذا سأحتفظ بها الآن.
لتلخيص
اتضح إيجاد حلول عمل لـ stm32 و Milander ، على الرغم من أنه بالنسبة لهذا الأخير كان علي الدفع مع بعض النفقات العامة.
بالنسبة لي ، كان الشيء الأكثر أهمية هو التحول الصغير في نموذج التفكير. قبل
المقالة المذكورة أعلاه ، لم أكن أعتقد على الإطلاق أنه يمكنك حماية نفسك بطريقة أو بأخرى من تجاوز سعة المكدس. لم أكن أرى ذلك على أنه مشكلة يجب حلها ، بل كظاهرة طبيعية معينة - أحيانًا تمطر ، وأحيانًا يكدس المكدس ، حسنًا ، لا يوجد شيء يجب القيام به ، عليك أن تعض الرصاصة وتتحملها.
وعادة ما ألاحظ ذلك بنفسي (وللآخرين) - بدلاً من قضاء 5 دقائق في Google وإيجاد حل تافه - كنت أعيش مع مشاكلي لسنوات.
هذا كل شيء بالنسبة لي. أفهم أنني لم أكتشف أي شيء جديد بشكل أساسي ، لكنني لم أجد أي مقالات جاهزة مع مثل هذا القرار (على الأقل جوزيف يو نفسه لا يعرض هذا مباشرة في
مقال حول هذا الموضوع). آمل في التعليقات أن يخبروني ما إذا كنت على حق أم لا ، وما هي مخاطر هذا النهج.
UPD: إذا ، عند إضافة ملف مبعثر ، يبدأ Keil في إصدار تحذير غير مفهوم ألا "AppData \ Local \ Temp \ p17af8-2 (33): تحذير: # 1-D: آخر سطر من الملف ينتهي بدون سطر جديد" - ولكن هذا الملف نفسه ليس يفتح ، لأنه مؤقت ، ثم فقط أضف فاصل الأسطر مع الحرف الأخير في ملف مبعثر.