كيف يعمل تتبع المكدس على ARM

مساء الخير قبل بضعة أيام واجهت مشكلة صغيرة في مشروعنا - في معالج المقاطعة gdb ، تم عرض تتبع المكدس لـ Cortex-M بشكل غير صحيح. لذلك ، مرة أخرى ، كان من المفيد معرفة ذلك ، وبأي طرق يمكنني الحصول على تتبع مكدس ARM؟ ما هي علامات التجميع التي تؤثر على تتبع المكدس على ARM؟ كيف يتم تنفيذ ذلك في نواة لينكس؟ بناءً على البحث ، قررت كتابة هذا المقال.

دعونا نلقي نظرة على طريقتين رئيسيتين لتتبع التكديس في نواة لينكس.

تكديس المكدس من خلال الإطارات


لنبدأ بمقاربة بسيطة يمكن العثور عليها في Linux kernel ، ولكنها في الوقت الحالي قد أوقفت حالتها في دول مجلس التعاون الخليجي.

تخيل أن برنامجًا معينًا يعمل على المكدس في ذاكرة الوصول العشوائي ، وفي وقت ما نقوم بمقاطعته ونريد إظهار مكدس المكالمة. افترض أن لدينا مؤشر للتعليمات الحالية التي يتم تنفيذها بواسطة المعالج (الكمبيوتر الشخصي) ، بالإضافة إلى المؤشر الحالي في أعلى المكدس (SP). الآن ، من أجل "القفز" فوق المكدس إلى الوظيفة السابقة ، تحتاج إلى فهم نوع الوظيفة التي كانت وأين يجب أن نقفز إلى هذه الوظيفة. يستخدم ARM Link Link (LR) لهذا الغرض.
سجل الوصلة (LR) هو السجل R14. يقوم بتخزين معلومات الإرجاع للروتينات الفرعية واستدعاءات الدوال والاستثناءات. عند إعادة التعيين ، يقوم المعالج بتعيين قيمة LR إلى 0xFFFFFFFF
بعد ذلك ، نحتاج إلى صعود المكدس وتحميل القيم الجديدة لسجلات LR من المكدس. هيكل إطار المكدس للمترجم هو كما يلي:

/* The stack backtrace structure is as follows: fp points to here: | save code pointer | [fp] | return link value | [fp, #-4] | return sp value | [fp, #-8] | return fp value | [fp, #-12] [| saved r10 value |] [| saved r9 value |] [| saved r8 value |] ... [| saved r0 value |] r0-r3 are not normally saved in a C function. */ 

هذا الوصف مأخوذ من ملف رأس دول مجلس التعاون الخليجي gcc / gcc / config / arm / arm.h.

على سبيل المثال يمكن إعلام المترجم (في حالتنا دول مجلس التعاون الخليجي) بطريقة أو بأخرى أننا نريد إجراء تتبع مكدس. وبعد ذلك في مقدمة كل وظيفة ، سيقوم المترجم بإعداد نوع من البنية المساعدة. يمكنك ملاحظة أنه في هذا الهيكل تكمن القيمة "التالية" لسجل LR الذي نحتاجه ، والأهم من ذلك أنه يحتوي على عنوان الإطار التالي | return fp value | [fp, #-12] | return fp value | [fp, #-12]

يتم تحديد وضع المترجم هذا من خلال خيار -mapcs-frame. هناك ذكر في وصف الخيار حول "تحديد -fomit-frame-pointer مع هذا الخيار يؤدي إلى عدم إنشاء إطارات المكدس لوظائف الشجرة." هنا ، تُفهم الدوال الورقية على أنها تعني تلك التي لا تجري أي مكالمات إلى وظائف أخرى ، بحيث يمكن جعلها أسهل قليلاً.

قد تتساءل أيضًا عما يجب القيام به مع وظائف التجميع في هذه الحالة. في الواقع ، لا يوجد شيء صعب - تحتاج إلى إدراج وحدات ماكرو خاصة. من ملف tools / objtool / Documentation / stack-validation.txt في Linux kernel:
يجب وضع علامة على كل وظيفة قابلة للاستدعاء على هذا النحو مع ELF
نوع الوظيفة. في كود ASM ، يتم ذلك عادة باستخدام
إدخال / ماكرو ENDPROC.
لكن الوثيقة نفسها تناقش أن هذا يمثل أيضًا عيبًا واضحًا لهذا النهج. تتحقق الأداة objtool مما إذا كانت جميع الوظائف في kernel مكتوبة بالتنسيق الصحيح لتتبع المكدس.

فيما يلي وظيفة فك حزمة من نواة لينكس:

 #if defined(CONFIG_FRAME_POINTER) && !defined(CONFIG_ARM_UNWIND) int notrace unwind_frame(struct stackframe *frame) { unsigned long high, low; unsigned long fp = frame->fp; /*    ,    */ /* restore the registers from the stack frame */ frame->fp = *(unsigned long *)(fp - 12); frame->sp = *(unsigned long *)(fp - 8); frame->pc = *(unsigned long *)(fp - 4); return 0; } #endif 

ولكن هنا أريد وضع علامة على الخط بـ defined(CONFIG_ARM_UNWIND) . تلمح إلى أن نواة لينكس تستخدم أيضًا تطبيقًا آخر لـ undind_frame ، وسنتحدث عنه لاحقًا.

خيار -mapcs-frame صالح فقط لمجموعة تعليمات ARM. ولكن من المعروف أن الميكروكونترولر ARM لديها مجموعة أخرى من التعليمات - Thumb (Thumb-1 و Thumb-2 ، لتكون أكثر دقة) ، يتم استخدامه بشكل أساسي لسلسلة Cortex-M. لتمكين إنشاء إطار لوضع الإبهام ، استخدم علامات إطار -mtpcs وإطار -mtpcs-leaf-frame. في الجوهر ، إنه تناظري من إطار-خرائط. ومن المثير للاهتمام أن هذه الخيارات تعمل حاليًا فقط مع Cortex-M0 / M1. لبعض الوقت لم أستطع معرفة سبب عدم تمكني من ترجمة الصورة المطلوبة لـ Cortex-M3 / M4 / .... بعد أن قمت بإعادة قراءة جميع خيارات gcc لـ ARM وبحثت في الإنترنت ، أدركت أن هذا ربما كان خطأ. لذلك ، صعدت مباشرة إلى شفرة المصدر لمترجم arm-none-eabi-gcc . بعد دراسة كيفية إنشاء المحول البرمجي لإطارات ARM و Thumb-1 و Thumb-2 ، توصلت إلى استنتاج مفاده أنهم تجاوزوا Thumb-2 ، أي أنه يتم إنشاء الإطارات فقط لـ Thumb-1 و ARM. بعد إنشاء الأخطاء ، أوضح مطورو دول مجلس التعاون الخليجي أن معيار ARM قد تغير بالفعل عدة مرات وأن هذه الأعلام قديمة جدًا ، ولكن لسبب ما لا تزال كلها موجودة في المترجم. يوجد أدناه تفكيك الوظيفة التي تم إنشاء الإطار لها.

 static int my_func(int a) { my_func2(7); return 0; } 

 00008134 <my_func>: 8134: b084 sub sp, #16 8136: b580 push {r7, lr} 8138: aa06 add r2, sp, #24 813a: 9203 str r2, [sp, #12] 813c: 467a mov r2, pc 813e: 9205 str r2, [sp, #20] 8140: 465a mov r2, fp 8142: 9202 str r2, [sp, #8] 8144: 4672 mov r2, lr 8146: 9204 str r2, [sp, #16] 8148: aa05 add r2, sp, #20 814a: 4693 mov fp, r2 814c: b082 sub sp, #8 814e: af00 add r7, sp, #0 

في المقابل ، مفكك نفس الوظيفة لتعليمات ARM

 000081f8 <my_func>: 81f8: e1a0c00d mov ip, sp 81fc: e92dd800 push {fp, ip, lr, pc} 8200: e24cb004 sub fp, ip, #4 8204: e24dd008 sub sp, sp, #8 

للوهلة الأولى ، قد يبدو أن هذه أشياء مختلفة تمامًا. ولكن في الواقع ، الإطارات هي نفسها تمامًا ، والحقيقة هي أنه في وضع الإبهام ، فإن تعليمات الدفع تسمح فقط بالتسجيلات المنخفضة (r0 - r7) وتسجيل lr ليتم تكديسها. بالنسبة لجميع السجلات الأخرى ، يجب أن يتم ذلك على مرحلتين من خلال التعليمات mov و str ، كما في المثال أعلاه.

تكديس المكدس خلال الاستثناءات


الطريقة البديلة هي فك المكدس بناءً على معيار معالجة الاستثناءات لمعيار ARM Architecture ( EHABI ). في الواقع ، المثال الرئيسي لاستخدام هذا المعيار هو معالجة الاستثناء في لغات مثل C ++. يمكن أيضًا استخدام المعلومات التي أعدها المترجم لمعالجة الاستثناء لتتبع المكدس. يتم تمكين هذا الوضع مع خيار GCC -fexceptions (أو -funwind-Frames ).

دعونا نلقي نظرة فاحصة على كيفية القيام بذلك. بادئ ذي بدء ، تفرض هذه الوثيقة (EHABI) متطلبات معينة على المترجم لإنشاء جداول مساعدة. ARM.exidx و. ARM.extab. هذه هي الطريقة التي يتم تعريف قسم ARM.exidx هذا في مصادر نواة لينكس. من قوس الملف / arm / kernel / vmlinux.lds.h :

 /* Stack unwinding tables */ #define ARM_UNWIND_SECTIONS \ . = ALIGN(8); \ .ARM.unwind_idx : { \ __start_unwind_idx = .; \ *(.ARM.exidx*) \ __stop_unwind_idx = .; \ } \ 

يحدد معيار "معالجة الاستثناء ABI لهيكل ARM" كل عنصر من عناصر .ARM.exidx كالبنية التالية:

 struct unwind_idx { unsigned long addr_offset; unsigned long insn; }; 

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

هذه التعليمات موصوفة في معيار EHABI المذكور بالفعل:



علاوة على ذلك ، فإن التطبيق الرئيسي لهذا المترجم على لينكس موجود في ملف arch / arm / kernel / undind.c

تنفيذ دالة Wind_frame
 int unwind_frame(struct stackframe *frame) { unsigned long low; const struct unwind_idx *idx; struct unwind_ctrl_block ctrl; /*   ,   */ /*   ARM.exidx    ,   PC */ idx = unwind_find_idx(frame->pc); if (!idx) { pr_warn("unwind: Index not found %08lx\n", frame->pc); return -URC_FAILURE; } ctrl.vrs[FP] = frame->fp; ctrl.vrs[SP] = frame->sp; ctrl.vrs[LR] = frame->lr; ctrl.vrs[PC] = 0; if (idx->insn == 1) /* can't unwind */ return -URC_FAILURE; else if ((idx->insn & 0x80000000) == 0) /* prel31 to the unwind table */ ctrl.insn = (unsigned long *)prel31_to_addr(&idx->insn); else if ((idx->insn & 0xff000000) == 0x80000000) /* only personality routine 0 supported in the index */ ctrl.insn = &idx->insn; else { pr_warn("unwind: Unsupported personality routine %08lx in the index at %p\n", idx->insn, idx); return -URC_FAILURE; } /*       ,    - * ,       */ /* check the personality routine */ if ((*ctrl.insn & 0xff000000) == 0x80000000) { ctrl.byte = 2; ctrl.entries = 1; } else if ((*ctrl.insn & 0xff000000) == 0x81000000) { ctrl.byte = 1; ctrl.entries = 1 + ((*ctrl.insn & 0x00ff0000) >> 16); } else { pr_warn("unwind: Unsupported personality routine %08lx at %p\n", *ctrl.insn, ctrl.insn); return -URC_FAILURE; } ctrl.check_each_pop = 0; /* ,      */ while (ctrl.entries > 0) { int urc; if ((ctrl.sp_high - ctrl.vrs[SP]) < sizeof(ctrl.vrs)) ctrl.check_each_pop = 1; urc = unwind_exec_insn(&ctrl); if (urc < 0) return urc; if (ctrl.vrs[SP] < low || ctrl.vrs[SP] >= ctrl.sp_high) return -URC_FAILURE; } /*   */ /* ,       */ frame->fp = ctrl.vrs[FP]; frame->sp = ctrl.vrs[SP]; frame->lr = ctrl.vrs[LR]; frame->pc = ctrl.vrs[PC]; return URC_OK; } 


هذا هو تطبيق وظيفة الوافد_إطار ، والتي يتم استخدامها إذا تم تمكين الخيار CONFIG_ARM_UNWIND. لقد أدرجت التعليقات مع شرح باللغة الروسية مباشرة في النص المصدر.

فيما يلي مثال لكيفية بحث عنصر الجدول ARM.exidx عن دالة kernel_start في Embox:

 $ arm-none-eabi-readelf -u build/base/bin/embox Unwind table index '.ARM.exidx' at offset 0xaa6d4 contains 2806 entries: <...> 0x1c3c <kernel_start>: @0xafe40 Compact model index: 1 0x9b vsp = r11 0x40 vsp = vsp - 4 0x84 0x80 pop {r11, r14} 0xb0 finish 0xb0 finish <...> 

وهنا تفكيكها:

 00001c3c <kernel_start>: void kernel_start(void) { 1c3c: e92d4800 push {fp, lr} 1c40: e28db004 add fp, sp, #4 <...> 

دعنا نذهب عبر الخطوات. نرى المهمة vps = r11 . (R11 هذا هو FP) ثم vps = vps - 4 . ويقابل ذلك الأمر add fp, sp, #4 . يأتي بعد ذلك pop {r11، r14} ، الذي يتوافق مع تعليمات push {fp, lr} . تشير تعليمات finish الأخيرة إلى نهاية التنفيذ (لأكون صريحًا ، ما زلت لا أفهم سبب وجود اثنين من تعليمات الإنهاء هناك).

الآن دعونا نرى مقدار الذاكرة التي يأكلها التجميع مع إشارة إطارات -funwind.
للتجربة ، قمت بتجميع Embox لمنصة STM32F4-Discovery. فيما يلي نتائج objdump:

مع العلم -funwind-Frames:
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 0005a600 08000000 08000000 00004000 2**14
CONTENTS, ALLOC, LOAD, CODE
1 .ARM.exidx 00003fd8 0805a600 0805a600 0005e600 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .ARM.extab 000049d0 0805e5d8 0805e5d8 000625d8 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
3 .rodata 0003e380 08062fc0 08062fc0 00066fc0 2**5


بدون علم:
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00058b1c 08000000 08000000 00004000 2**14
CONTENTS, ALLOC, LOAD, CODE
1 .ARM.exidx 00000008 08058b1c 08058b1c 0005cb1c 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .rodata 0003e380 08058b40 08058b40 0005cb40 2**5


من السهل حساب أن مقاطع .ARM.exidx و. ARM.extab تحتل ما يقرب من 1/10 من حجم النص. بعد ذلك ، جمعت صورة أكبر - لـ ARM Integrator CP استنادًا إلى ARM9 ، وكانت هذه الأقسام 1/12 من حجم قسم النص. ولكن من الواضح أن هذه النسبة يمكن أن تختلف من مشروع إلى آخر. اتضح أيضًا أن حجم الصورة التي تضيف علامة إطار -macps أصغر من خيار الاستثناء (المتوقع). لذلك ، على سبيل المثال ، عندما كان حجم مقطع النص .600 كيلوبايت ، كان الحجم الإجمالي .ARM.exidx + .ARM.extab 50 كيلوبايت ، وكان حجم الكود الإضافي بعلامة إطار -mapcs 10 كيلوبايت فقط. ولكن إذا نظرنا أعلاه ، ما تم إنشاء مقدمة كبيرة لـ Cortex-M1 (تذكر ، من خلال mov / str؟) ، يصبح من الواضح أنه في هذه الحالة لن يكون هناك فرق عمليًا ، مما يعني أنه من غير المحتمل استخدام -mtpcs-frame لوضع الإبهام تبدو منطقية على الأقل.

هل تتبع المكدس هذا مطلوب لـ ARM الآن؟ ما هي البدائل؟


النهج الثالث هو تتبع المكدس باستخدام مصحح أخطاء. يبدو أن العديد من أنظمة التشغيل للعمل مع FreeRTOS و NuttX microcontrollers تشير حاليًا إلى خيار التتبع المحدد هذا أو تعرض مشاهدة أداة تفكيك.

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

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

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


All Articles