تصحيح الأخطاء بعد الوفاة على Cortex-M

تصحيح الأخطاء بعد الوفاة على Cortex-M



خلفية:


شاركت مؤخرًا في تطوير جهاز غير عادي بالنسبة لي من فئة الأجهزة الإلكترونية الاستهلاكية. يبدو أنه ليس شيئًا معقدًا ، صندوقًا يجب أن يخرج أحيانًا عن وضع السكون ، ويبلغ إلى الخادم وينام.


أظهرت الممارسة بسرعة أن مصحح الأخطاء لا يساعد كثيرًا عند العمل مع متحكم مستمر في وضع السكون العميق أو يقطع الطاقة. في الأساس ، لأن المربع في وضع الاختبار كان بدون مصحح وبدوني قريب وأحيانًا كان عربات التي تجرها الدواب. حوالي مرة واحدة كل بضعة أيام.


تم تصحيح أخطاء UART على الفتحات ، والتي بدأت في إخراج السجلات منها. أصبح أسهل ، تم حل بعض المشاكل. ولكن بعد ذلك حدث تأكيد وكل ذلك حدث.


في حالتي ، يبدو الماكرو للتأكيد شيئًا مثل هذا:
#define USER_ASSERT( statement ) \ do \ { \ if(! (statement) ) \ { \ DEBUG_PRINTF_ERROR( "Assertion on line %d in file %s!\n", \ __LINE__, __FILE__ ); \ \ __disable_irq(); \ while(1) \ { \ __BKPT(0xAB); \ if(0) \ break; \ } \ } \ } while(0) 

__BKPT(0xAB) نقطة توقف برنامج؛ إذا حدث التوكيد تحت تصحيح الأخطاء ، فإن المصحح يتوقف عند خط المشكلة ، فهذا مناسب للغاية.


بالنسبة لبعض التأكيدات ، يتضح على الفور سبب ذلك - لأن السجل يُظهر اسم الملف ورقم السطر الذي عمل عليه التأكيد.


ولكن وفقًا للتأكيد ، كان من الواضح فقط أن المصفوفة كانت تفيض - وبشكل أكثر دقة ، غلاف مؤقت فوق المصفوفة ، والذي يتحقق من المخرج. لهذا السبب ، كان اسم الملف "super_array.h" ورقم السطر فيه مرئيًا فقط في السجل. وما مجموعة محددة ليست واضحة. من السجلات المحيطة ، فمن غير الواضح أيضا.


بالطبع ، يمكن للمرء أن يعض الرصاصة ويذهب إلى قراءة الكود ، لكنني كنت كسولًا جدًا ، ومن ثم لن تنجح المقالة.


منذ أن كتبت في uVision Keil 5 مع مترجم armcc ، تم اختبار رمز إضافي فقط تحته. كما أنني استخدمت C ++ 11 ، لأنه بالفعل 2019 في الفناء ، لقد حان الوقت بالفعل.


تتبع مكدس الذاكرة المؤقتة


بطبيعة الحال ، فإن أول ما يتبادر إلى الذهن هو ، لعنة ، لأنه عندما يحدث تأكيد على كمبيوتر سطح المكتب العادي ، يتم إخراج تتبع مكدس إلى وحدة التحكم ، مثل KDPV. من تتبع المكدس ، يمكنك عادة فهم تسلسل المكالمات الذي أدى إلى الخطأ.
حسنًا ، لذلك أحتاج أيضًا إلى مسار خلسة. كيف اصنعها؟


ربما إذا رميت استثناءًا ، فسيتم استنتاجه؟


نلقي استثناء ولا نلجأ إليه ؛ نرى إخراج "SIGABRT" والدعوة إلى _sys_exit . ليست رحلة ، حسناً ، حسنًا ، ليس حقًا ، وأردت حقًا السماح بالاستثناءات.


غوغلينغ كيف يفعل الآخرون ذلك.


جميع الطرق خاصة بالمنصة (ليست مفاجئة للغاية) ، execinfo.h لـ gcc ضمن POSIX ، يوجد backtrace() و execinfo.h . لم يكن هناك شيء واضح لكيل. نسقط دمعة متوسطة. عليك أن تتسلق المكدس بيديك.


نحن نتسلق في الكومة بأيدينا


من الناحية النظرية ، كل شيء بسيط للغاية.


  1. يوجد عنوان المرتجعات من الوظيفة الحالية في سجل LR ، وعنوان الجزء العلوي الحالي من الرصة (بمعنى آخر عنصر في الرصة) موجود في سجل SP ، وعنوان الأمر الحالي في سجل الكمبيوتر الشخصي.
  2. بطريقة أو بأخرى ، نجد حجم إطار الرصة للوظيفة الحالية ، وخطو طول الرصة في مثل هذه المسافة ، ونجد عنوان المرسل للوظيفة السابقة هناك ونكررها حتى ننتقل من الرصة إلى النهاية.
  3. بطريقة ما ، نطابق عناوين الإرجاع مع أرقام الأسطر في الملفات مع الكود المصدري.

حسنًا ، بالنسبة للمبتدئين - كيف أعرف حجم إطار المكدس؟


على الخيارات بشكل افتراضي - على ما يبدو ، لا شيء على الإطلاق ، يتم ترميزها ببساطة بواسطة المترجم في "prolog" و "epilogue" لكل وظيفة ، في أوامر تخصص وتحرر جزءًا من المكدس للإطار.
ولكن لحسن الحظ ، يتوفر لدى armcc الخيار --use_frame_pointer ، الذي يخصص سجل R11 تحت مؤشر الإطار - أي مؤشر إلى إطار مكدس الوظيفة السابقة. عظيم ، الآن يمكنك المشي من خلال جميع إطارات المكدس.


الآن - كيف تتطابق مع عناوين المرسل مع سلاسل في الملفات المصدر؟


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


نحن تنهد. لذلك ، لن يعمل التكديس الصادق - مثل أسماء الوظائف وأرقام الأسطر على الفور لإخراج التصحيح -. ولكن يمكنك عرض العناوين ، ثم على الكمبيوتر مقارنتها بالوظائف والأرقام ، حيث لا تزال هناك معلومات تصحيح الأخطاء في المشروع.


يبدو الأمر محزنًا للغاية ، لأنه يتعين عليك تحليل ملف .map ، الذي يشير إلى نطاقات العناوين التي تشغلها كل وظيفة. ثم قم بتحليل الملفات بشكل منفصل مع تعليمات برمجية مفككة للعثور على سطر معين. هناك رغبة حادة للتسجيل.


بالإضافة إلى ذلك ، بالنظر بعناية في الوثائق الخاصة بالخيار - يتيح --use_frame_pointer رؤية هذه الصفحة ، التي تقول إن هذا الخيار يمكن أن يؤدي إلى تعطل في HardFault في أوقات عشوائية. هم.
حسنا ، فكر أكثر.


كيف المصحح تفعل هذا؟


ولكن المصحح بطريقة ما يظهر مكدس الاستدعاء حتى بدون frame pointer'a . حسنًا ، من الواضح كيف أن IDE لديه كل معلومات التصحيح في متناول اليد ، من السهل عليها مقارنة عناوين وأسماء الوظائف. هم.


في الوقت نفسه ، يحتوي Visual Studio نفسه على شيء من هذا القبيل - تفريغ مصغر - عندما ينشئ التطبيق المعطوب ملفًا صغيرًا ، ثم تقوم بإطعام الاستوديو ويستعيد حالة التطبيق في وقت التعطل. ويمكنك النظر في جميع المتغيرات ، والمشي على المكدس في راحة. جلالة الملك مرة أخرى.


لكنه نوع بسيط جدا. فقط تحتاج فرك استمرار السوفيتي سميكة في الأرداف كل يوم ملء المكدس مع القيم التي كانت موجودة في وقت السقوط ، وعلى ما يبدو ، استعادة حالة السجلات. وهذا كل شيء ، على ما يبدو؟


مرة أخرى ، قسم هذه الفكرة إلى مهام فرعية.


  1. على المتحكم الدقيق ، تحتاج إلى المرور عبر المكدس ، لذلك تحتاج إلى الحصول على قيمة SP الحالية وعنوان بداية المكدس.
  2. في وحدة التحكم ، تحتاج إلى عرض قيم السجلات.
  3. في IDE ، تحتاج إلى دفع جميع القيم بطريقة ما من "تفريغ مصغر" مرة أخرى إلى المكدس. وقيم السجلات أيضا.

كيفية الحصول على القيمة الحالية ليرة سورية؟


على نحو مفضل ، عدم التظليل على التجميع. في Cale ، لحسن الحظ ، هناك وظيفة خاصة (مضمن) - __current_sp() . لن يعمل Gcc ، لكنني لست بحاجة إلى ذلك.


كيفية الحصول على عنوان بداية المكدس؟ نظرًا لأنني أستخدم البرنامج النصي للحماية من تجاوز السعة (التي كتبت عنها هنا ) ، فإن مجموعتي تكمن في قسم رابط منفصل ، والذي REGION_STACK .
هذا يعني أنه يمكن العثور على عنوانه على رابط باستخدام متغيرات غريبة مع الدولارات في الأسماء .


من خلال التجربة والخطأ ، نختار الاسم المطلوب - Image$$REGION_STACK$$ZI$$Limit ، تحقق ، يعمل.


توضيح

هذا رمز سحري يتم إنشاؤه في مرحلة الربط ، بمعنى أنه بصرامة ، فإنه ليس ثابتًا في مرحلة التجميع.
لاستخدامها ، تحتاج إلى إلغاء التسجيل:


 extern unsigned int Image$$REGION_STACK$$ZI$$Limit; using MemPointer = const uint32_t *; //   ,   static const auto stack_upper_address = (MemPointer) &( Image$$REGION_STACK$$ZI$$Limit ); 

إذا كنت لا تشعر بالضيق ، فيمكنك ببساطة تغيير حجم الرصة ، حيث إنه نادرًا ما يتغير. في أسوأ الحالات ، لا نرى في نافذة مكدس المكالمات جميع المكالمات ، ولكن كعب الروتين.


كيفية عرض قيم التسجيل؟


في البداية اعتقدت أنه من الضروري عرض جميع سجلات الأغراض العامة بشكل عام ، بدأت في التشويش مع المجمّع ، لكنني أدركت سريعًا أنه لن يكون هناك أي معنى في هذا. بعد كل شيء ، سيتم تنفيذ إخراج minidump بواسطة وظيفة خاصة بالنسبة لي ، لا يوجد أي معنى في قيم السجل في سياقها.


نحتاج حقًا إلى Link Register (LR) ، الذي يخزن عنوان المرسل من الوظيفة الحالية ، SP ، التي تعاملنا معها بالفعل ، و Program Counter (PC) ، الذي يخزن عنوان الأمر الحالي.


مرة أخرى ، لم أتمكن من العثور على خيار من شأنه أن يعمل مع أي برنامج التحويل البرمجي ، ولكن هناك وظائف __return_address() لـ Cale: __return_address() لـ LR و __current_pc() للكمبيوتر الشخصي.
ممتاز. يبقى لدفع جميع القيم من تفريغ مصغر العودة إلى المكدس ، وقيم السجلات في السجلات.


كيفية تحميل تفريغ مصغر في الذاكرة؟


في البداية ، خططت لاستخدام الأمر LOAD debugger ، والذي يسمح لك بتحميل القيم من ملف hex أو .bin إلى الذاكرة ، لكنني اكتشفت بسرعة أن LOAD لسبب ما لا يقوم بتحميل القيم إلى ذاكرة الوصول العشوائي.
وما زلت غير قادر على إكمال السجلات بهذا الأمر.


حسنًا ، حسنًا ، لا يزال يتطلب الكثير من الإيماءات ، تحويل النص إلى بن ، تحويل بن إلى ست عشري ...


لحسن الحظ ، يحتوي Cale على محاكي ، وبالنسبة للمحاكي ، يمكنك كتابة البرامج النصية بلغة C- البائسة. وفي هذه اللغة هناك فرصة للكتابة في الذاكرة! هناك وظائف خاصة مثل _WDWORD و _WBYTE . نقوم بجمع كل الأفكار في كومة ، والحصول على مثل هذا الرمز.


كل الكود:
 #define USER_ASSERT( statement ) \ do \ { \ if(! (statement) ) \ { \ DEBUG_PRINTF_ERROR( "Assertion on line %d in file %s!\n", \ __LINE__, __FILE__ ); \ \ print_minidump(); \ __disable_irq(); \ while(1) \ { \ __BKPT(0xAB); \ if(0) \ break; \ } \ } \ } while(0) //   ,    //   ,         scatter- extern unsigned int Image$$REGION_STACK$$ZI$$Limit; void print_minidump() { //   - armcc  arm-clang #if __CC_ARM || ( (__ARMCC_VERSION) && (__ARMCC_VERSION >= 6010050)) using MemPointer = const uint32_t *; //   ,   static const auto stack_upper_address = (MemPointer) &(Image$$REGION_STACK$$ZI$$Limit ); //      , ..      //  SP  stack_upper_address auto LR = __return_address(); auto PC = __current_pc(); auto SP = __current_sp(); auto i = 0; DEBUG_PRINTF("\nCopy the following function for simulator to .ini-file, \n" "start fresh debug session in simulator and call __load_minidump() from command window.\n" "You should be able to see the call stack in CallStack window\n\n"); DEBUG_PRINTF("func void __load_minidump() {\n "); for( MemPointer stack = (MemPointer)SP; stack <= stack_upper_address; stack++ ) { DEBUG_PRINTF("_WDWORD (0x%p, 0x%08x); ", stack, *stack ); //         if( i == 1 ) { DEBUG_PRINTF("\n "); i=0; } else { i++; } } DEBUG_PRINTF("\n LR = 0x%08x;", LR ); DEBUG_PRINTF("\n PC = 0x%08x;", PC ); DEBUG_PRINTF("\n SP = 0x%08x;", SP ); DEBUG_PRINTF("\n}\n"); #endif } 

لتحميل __load_minidump ، نحتاج إلى إنشاء ملف .ini ، ونسخ وظيفة __load_minidump فيه ، وإضافة هذا الملف إلى التشغيل التلقائي - Project -> Options for Target -> Debug وكتابة ملف .ini هذا في قسم "ملف التهيئة" في قسم استخدام المحاكاة.


الآن نذهب فقط إلى تصحيح الأخطاء على جهاز محاكاة ، وبدون بدء تصحيح الأخطاء ، استدعاء الدالة __load_minidump() في إطار الأوامر.
و voila ، ننتقل إلى وظيفة print_minidump على الخط حيث تم حفظ جهاز الكمبيوتر. وفي نافذة Callstack + Localals ، يمكنك رؤية مكدس الاتصال.


ملاحظة:

تتم تسمية الوظيفة على وجه التحديد برمزين سفليين في البداية ، لأنه إذا تزامن اسم الوظيفة أو المتغير في البرنامج النصي للمحاكاة مع الاسم في رمز المشروع ، فلن يتمكن Cale من الاتصال به. يحظر معيار C ++ استخدام الأسماء ذات الشرطين السفلية في البداية ، وبالتالي تقل احتمالية مطابقة الأسماء.


من حيث المبدأ ، هذا كل شيء. بقدر ما تمكنت من التحقق ، يعمل تفريغ مصغر لكل من الوظائف العادية ومعالجات المقاطعة. ما إذا كان سيعمل مع جميع أنواع الانحرافات باستخدام setjmp/longjmp أو setjmp/longjmp - لا أعرف ، لأنني لا أمارس الانحرافات.


أنا سعيد بما حدث ؛ رمز صغير ، النفقات العامة - ماكرو قليلا تورم للتأكيد. في هذه الحالة ، سقطت جميع الأعمال المملة حول تحليل المكدس على أكتاف IDE ، حيث تنتمي.


ثم غوغلد قليلاً ووجدت شيئًا مشابهًا لـ gcc و gdb - CrashCatcher .


أفهم أنني لم أخترع أي شيء جديد ، لكن لم أتمكن من العثور على وصفة جاهزة تؤدي إلى نتيجة مماثلة. سأكون ممتنا لو أخبروني ما الذي يمكن عمله بشكل أفضل.

Source: https://habr.com/ru/post/ar452704/


All Articles