The Stack and Windows x86 Calling Conventions Part 2
السلام عليكم ورحمة الله وبركاته
هذه المقالة استكمال للمقالة السابقة
أهدف في هذه المقالة لشرح النقاط الآتية :
- Calling Conventions
- Function’s
Prologue
andEpilogue
لنبدأ بسم الله
Calling Conventions
في كتاب Practical Malware Analysis
نجد التعريف التالي
calling conventions govern the way the function call occurs. These conventions include the order in which parameters are placed on the stack or in registers, and whether the caller or the function called (the callee) is responsible for cleaning up the stack when the function is complete.
حتى نفهم التعريف لنأخذ المثال التالي
1 #include <stdio.h>
2
3 // define a function named MyFunction that returns the sum value
4 int MyFunction(int a, int b)
5 {
6 int sum;
7 sum = a + b; // add a and b and store the result in sum
8 return sum; // return the result
9 }
10
11 int main()
12 {
13 int result;
14 result = MyFunction(10, 5); // call MyFunction and store the returned value in result
15 printf("The sum is: %d\n", result); // print the result
16 return 0; // end the program
17 }
البرنامج جدًا بسيط لجمع رقمين وطباعتهما
دالة الـ main
هي التي قامت بالاستدعاء ، أو بلغة أخرى نسميها الـ Caller
ودالة الـ MyFunction
هي الدالة التي تم نداءها ، أو الـ Callee
نلاحظ أن دالة الـ main
قامت أيضًا بتمرير الـ arguments
لدالة الـ MyFunction
الشرح هذا قد يبدو بسيط، لكن خلفه أشياء أخرى تحدث ، مثلًا :
كيف سيتم تمرير الـ arguments
من دالة الـ main
الى الـ MyFunction
؟
وأين سيتم حفظ هذه الـ arguments
؟ في الـ Stack
؟ أم في الـ registers
؟
من سيقوم باعادة الـ Stack
لوضعه الأولي قبل نداء الدالة MyFunction
؟ هل ستكون دالة MyFunction
هي المسؤولة ؟ أم دالة الـ main
؟
جميع هذه الأسئلة التي طرحناها يُجيب عنها الـ Calling Conventions
فالـ Calling Conventions
بشكل مختصر أشبه بالقوانين التي تحكم كيف سيتم نداء الدوال وكيف سيتم تمرير المتغيرات للدالة المُستدعاة ( Callee
) ، كذلك أين و كيف سيتم تخزين هذه المتغيرات التي تم تمريرها، وأيضًا من سيتولى عملية تنظيف الـ Stack
وإعادته لوضعه الأولي بعد أن تُكمل الدالة المُستدعاة ( Callee
) عملها
Layout of a Function in Assembly
تطرقنا في المقالة السابقة للتعليمة CALL
وعرفنا أن هذه التعليمة في لغة أسمبلي تمكّننا من نداء دالة
ولو عدنا للكود الخاص بنفس البرنامج سنجد السطر الآتي الذي يوضح لنا نداء الدالة MyFunction
ولو تعمقنا في كود الأسمبلي الخاص بأي دالة، سنجد أن كود الأسمبلي الخاص بالدالة يحمل الشكل الآتي
نلاحظ أن بداية الدالة يكون الجزء المسمى Prologue
ونهايتها يكون الـ Epilogue
، فماذا نقصد بهذين المفهومين ؟
Function’s Prologue
في نفس كتابنا الرائع Practical Malware Analysis
نجد التعريف التالي للـ Prologue
prologue—a few lines of code at the start of the function. The prologue prepares the stack and registers for use within the function
فالــ Prologue
هو مجموعة من تعليمات الـ Assembly والغرض الأساسي منها هو تهيئة الـ Stack والـ registers قبل أن يتم تنفيذ الـ Body الخاص بالدالة
في الصورة التالية نجد مثال على الـ Prologue
الخاص بالدالة MyFunction
نلاحظ أن الـ Prologue
عبارة عن هذه التعليمات
1 ; The function prologue
2 push ebp ; Save the old base pointer
3 mov ebp, esp ; Set the new base pointer
4 sub esp, 0CCH ; Allocate 192 bytes for the local variable
لنبدأ بالحديث عن الـ Prologue
بالتفصيل الآن :
2
: هذه التعليمة تحفظ قيمة الـEBP
في الـ Stack ، وقيمة الـEBP
في هذه الحالة يعود للـStack Frame
الخاص بالدالة السابقة، وفي مثالنا الدالة السابقة هي الـmain
، والغرض من هذه الخطوة حتى نستطيع العودة للـStack Frame
الخاص بالدالةmain
بعد أن تُكمل الدالةMyFunction
عملها
لعل الرسم التالي يوضح العملية بشكل أكبر :
3
: بعد أن قمنا بحفظ قيمة الـEBP
الخاص بالدالة السابقة (Caller
) في الـStack
، الآن نستطيع تحديث قيمة الـEBP
لبدءStack Frame
جديد ( الخاص بالدالةMyFunction
) ، و التعليمة في السطر3
هي التي تقوم بهذا ، وهذه التعليمة بشكل مختصر تقوم بنسخ الـESP
الى الـEBP
، أي كأننا قمنا بتحريك الـEBP
، الرسم التالي يلخص هذه الخطوة
4
: في هذه التعليمة قمنا بطرح0CCH
من قيمة الـESP
، والغرض من هذه الخطوة هو اتاحة مساحة للمتغيرات الخاصة بالدالة ( local variables ) ، أو بتعبير آخر قمنا بتحريك الـESP
، ولاحظ أننا قمنا بالطرح على الرغم من أننا نريد إضافة المتغيرات الى الـ Stack Frame ، والسبب في الطرح لأن الـ Stack ينمو بشكل عكسي كما عرفنا في المقالة السابقة ، فكلما تمت إضافة عناصر للـ Stack ، العنصر الأخير سيحمل عنوان أصغر من العنصر السابق ، الرسم التالي لعله يلخّص هذه العملية
an epilogue at the end of a function restores the stack and registers to their state before the function was called.
الـ Epilogue
هو العملية العكسية للـ Prologue
فالغرض الأساسي من الـ Epilogue
هو إعادة الـ Stack والـ registers للوضع الأولي بعد أن تُكمل الدالة المُستدعاة ( Callee
) عملها
ولو أردنا رؤية الـ Epilogue
في الكود الخاص بالدالة MyFunction
سنجد الآتي :
إذًا فالـ Epilogue
مكوّن من العمليات الآتية :
1 ; The function epilogue
2 mov esp, ebp ; Restore the stack pointer
3 pop ebp ; Restore the base pointer
4 ret ; Return to the caller
لنبدأ بالتفصيل الآن :
2
: في هذه التعليمة قمنا بنسخ قيمة الـEBP
الى الـESP
، أي كأننا قمنا بتحريك الـESP
للأعلى وأصبح يشير لنفس العنوان الموجود في الـEBP
، أو إن صح التعبير كأننا انتهينا من الـ Stack Frame الحالي ، الرسم التالي يلخّص هذه العملية
3
: في هذه التعليمة قمنا بعملPOP
للقيمة الأخيرة في الـ Stack وتخزينها في الـEBP
، والقيمة الأخيرة هي الـEBP
السابق ( انظر للسطر2
، الـESP
يشير الى هذه القيمة ) ، وللتلخيص الغرض من هذه الخطوة هو إسترجاع الـEBP
الخاص بالدالة السابقة (Caller
) ، الرسم التالي يوضح هذه الخطوة
4
: تطرقنا في المقالة السابقة الى التعليمةRET
وعرفنا أنها تقوم بحذف القيمة الأخيرة في الـ Stack وتخزّنها في الـEIP register
، وفي هذه المرحلة ، القيمة الحالية في الـ Stack هو العنوان التالي لنداء الدالةMyFunction
، بتعبير آخر ، هذه التعليمة تُعيد التحكم للدالةmain
حتى يستكمل البرنامج عمله ، الرسم التالي لعله يلخص هذه العملية
نكتفي بهذا القدر في هذه المقالة، وسنكمل الحديث عن بقية المواضيع في مقالات لاحقة بمشيئة الله ، وإن أصبت فمن توفيق الله وحده.
ولمن أراد الاستزادة حول هذه المواضيع أرشّح وبشدة الاطلاع على سلسلة هذه المقالات :
ℹ️ [ملاحظة] هذه المقالة تمّت كتابتها خلال دراسة هذه المواضيع، فكل ما تم ذكره هنا قد يحتمل الخطأ، لكن بالإمكان العودة إلى المراجع التي إستندت عليها هذه المقالة