P01NT
0x4 04/10

بنى الدوال (Function Constructs)

أطر المكدس ومقدمة الدالة وخاتمتها، واصطلاحا الاستدعاء stdcall وcdecl بمقارنة عملية جانبية.

· 7 دقائق قراءة · 1386 كلمة
المحتويات

بسم الله الرحمن الرحيم

في هذا المقال سنتعرف على كيفية تعريف الدوال.


1. أطر المكدس (Stack Frames)

في أعلى مستويات التجريد، يُنظَّم المكدس إلى أطر مكدس (Stack Frames). كل استدعاء دالة يُنشئ إطار مكدس جديدًا يحتوي على جميع البيانات المتعلقة بتلك النسخة من الدالة.

الشكل 4.1: المكدس منظّم في سلسلة من أطر المكدس

الشكل 4.1: المكدس منظّم في سلسلة من أطر المكدس

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

الشكل 4.2: كل إطار مكدس يتكون من وسائط الدالة وعنوان الإرجاع وEBP المحفوظ والمتغيرات المحلية

الشكل 4.2: كل إطار مكدس يتكون من وسائط الدالة وعنوان الإرجاع وEBP المحفوظ والمتغيرات المحلية

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

لحل هذه المشكلة، تستخدم الدوال سجل EBP (نقطة الأساس — Base Pointer) كنقطة ارتساء مستقرة. تُنشئ مقدمة الدالة (Prologue) إطار المكدس وتضبط EBP على موقع ثابت داخل الإطار، مما يتيح الوصول إلى الوسائط والمتغيرات عبر إزاحات ثابتة من EBP.

يحتوي إطار المكدس القياسي على العناصر التالية (مرتبة من العنوان الأعلى إلى الأدنى):

  1. وسائط الدالة: تدفعها الدالة المستدعية قبل الاستدعاء. يُوصَل إليها بإزاحات موجبة نسبةً إلى EBP (مثل [ebp+8]).
  2. عنوان الإرجاع: تدفعه تعليمة CALL تلقائيًا. يقع عند [ebp+4].
  3. EBP المحفوظ (Saved EBP): تدفع الدالة المستدعاة قيمة EBP الخاصة بالمستدعي للحفاظ عليها. ثم يُضبط EBP الحالي ليشير إلى هذا الموقع، مشكِّلًا الأساس للإطار الجديد.
  4. المتغيرات المحلية: مساحة تخصّصها الدالة لاستخدامها الخاص. يُوصَل إليها بإزاحات سالبة نسبةً إلى EBP (مثل [ebp-4]).

2. وسائط الدالة والمتغيرات المحلية

push 20
push 10
call calculate
...
calculate proc near
push ebp
mov  ebp, esp
sub  esp, 8

القائمة 4.1: إنشاء إطار مكدس جديد

توضح القائمة 4.1 كيف تُنشئ مقدمة الدالة النموذجية إطار مكدس:

  • وسائط الدالة: تدفع الدالة المستدعية وسيطين، 10 و 20، إلى المكدس.
  • عنوان الإرجاع: تدفع تعليمة CALL عنوان الإرجاع إلى المكدس تلقائيًا.
  • EBP المحفوظ: تعليمة calculate الأولى (push ebp) تحفظ EBP الخاص بالمستدعي بحيث يمكن استعادته. التعليمة التالية (mov ebp, esp) تنسخ مؤشر المكدس الحالي إلى EBP لتأسيس مؤشر الأساس لإطار مكدس calculate.
  • المتغيرات المحلية: التعليمة الأخيرة (sub esp, 8) تعدّل ESP لحجز مساحة للمتغيرات المحلية. ويختلف مقدار التعديل حسب حجم المتغيرات المحلية وعددها.

الشكل 4.3: إطار المكدس المُنشأ بواسطة التسلسل في القائمة 4.1

الشكل 4.3: إطار المكدس المُنشأ بواسطة التسلسل في القائمة 4.1

يوضح الشكل 4.3 إطار المكدس الجديد بعد تسلسل القائمة 4.1. هذا التسلسل PUSH-MOV-SUB هو مقدمة الدالة القياسية (Standard Function Prologue). بعد إنشاء إطار مكدسها، تصل الدالة إلى متغيراتها المحلية عند إزاحات سالبة نسبةً إلى EBP، وإلى وسائطها عند إزاحات موجبة.


3. اصطلاحات الاستدعاء (Calling Conventions: STDCALL و CDECL)

تحدد اصطلاحات الاستدعاء كيفية تفاعل الدوال مع بعضها، فهي تفرض:

  • كيفية تمرير الوسائط إلى الدالة المستدعاة.
  • مكان تخزين قيمة الإرجاع.
  • كيفية تنظيف المكدس بعد الاستدعاء.

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

أكثر اصطلاحي استدعاء شيوعًا في برامج ويندوز ذات 32-بت x86 هما:

  • stdcall: اصطلاح الاستدعاء القياسي لواجهة برمجة ويندوز (Windows API).
  • cdecl: اصطلاح الاستدعاء الافتراضي لمترجمات لغة C.
stdcallcdecl
push  20
push  10
call  calculate
...


calculate proc near
push ebp       ]
mov  ebp, esp  ]-- مقدمة (prologue)
sub  esp, 8    ]
...
mov  esp, ebp  ]
pop  ebp       ]-- خاتمة (epilogue)
retn 8         ]
push  20
push  10
call  calculate
add  esp, 8
...
calculate proc near
push ebp
mov  ebp, esp
sub  esp, 8
...
mov esp, ebp
pop ebp
retn

القائمة 4.2: مقارنة بين اصطلاحي الاستدعاء stdcall و cdecl

مقارنة بين اصطلاحي الاستدعاء:

  • الوسائط: كلا الاصطلاحين يدفع الوسائط إلى المكدس من اليمين إلى اليسار. ففي استدعاء الدالة calculate(10, 20)، تُدفع القيمة 20 أولًا تليها 10.
  • قيم الإرجاع: كلا الاصطلاحين يرجع الناتج في سجل EAX.
  • تنظيف المكدس:
    • stdcall (تنظيف المستدعى): تنظّف الدالة المستدعاة الوسائط قبل الإرجاع. في العمود الأيمن من القائمة 4.2، تعليمة retn 8 تسحب عنوان الإرجاع وتزيد ESP بمقدار 8 بايتات.
    • cdecl (تنظيف المستدعي): ينظّف المستدعي الوسائط بعد رجوع الدالة المستدعاة. في العمود الأيسر من القائمة 4.2، يعدّل المستدعي ESP بالتعليمة add esp, 8 فور رجوع calculate.

4. المتغيرات المحلية (Local Variables)

CAssembly
void main()
{
  int a = 0;
  int b = 1;

  a = a + b;
  printf("total = %d\n", a);
}
mov     [ebp+var_4], 0
mov     [ebp+var_8], 1
mov     eax, [ebp+var_4]
add     eax, [ebp+var_8]
mov     [ebp+var_4], eax
mov     ecx, [ebp+var_4]
push    ecx
push    offset aTotalD  ; "total = %d\n"
call    printf

القائمة 4.3: برنامج C صغير بدالة تستخدم متغيرات محلية، وشيفرة الملف المُصرَّف المقابلة له

تحتوي الدالة main في القائمة 4.3 على متغيرين محليين، a و b:

  • تُهيّئ الدالة a إلى 0 و b إلى 1. في التجميع، تُجري تعليمتا MOV الأوليان هذه التهيئة، فتضبطان موقعَي المكدس var_4 إلى 0 و var_8 إلى 1. تُنشئ IDA تلقائيًا تسميات مثل var_4 و var_8 لتمثيل الإزاحات الرقمية الخام (مثل [ebp-4] و [ebp-8]).
  • بعد ذلك، يجمع البرنامج a و b ويخزن الناتج في a. ولأن x86 لا تستطيع جمع موقعين في الذاكرة مباشرةً، يُولّد المترجم تسلسلًا من ثلاث خطوات: يحمّل var_4 إلى سجل، ثم يجمعه مع قيمة var_8، ثم يخزن الناتج مجددًا في var_4.
  • أخيرًا، يستدعي البرنامج printf مع سلسلة تنسيق والوسيط a. يدفع البرنامج الوسائط من اليمين إلى اليسار: يدفع var_4 أولًا ثم عنوان سلسلة التنسيق.

5. المتغيرات العامة (Global Variables)

CAssembly
int a = 0;
int b = 1;

void main()
{
  a = a + b;
  printf("total = %d\n", a);
}
mov     eax, dword_40CF60
add     eax, dword_40C000
mov     dword_40CF60, eax
mov     ecx, dword_40CF60
push    ecx
push    offset aTotalD  ; "total = %d\n"
call    printf

القائمة 4.4: برنامج C صغير بدالة تستخدم متغيرات عامة، وشيفرة الملف المُصرَّف المقابلة له

تعرض القائمة 4.4 البرنامج نفسه الوارد في القائمة 4.3 تقريبًا، لكنها تستبدل المتغيرات المحلية بمتغيرات عامة.

المتغيرات المحلية في القائمة 4.3 مخصصة ضمن إطار مكدس الدالة؛ ولهذا فهي لا توجد بعد انتهاء عمر الدالة: فعندما ترجع الدالة، يؤدي تدمير إطار مكدسها إلى تحريرها تلقائيًا.

أما المتغيرات العامة فتوجد طوال مدة تشغيل البرنامج بأكمله. وتُخزَّن المتغيرات العامة في مقطع بيانات ثابت من الملف الثنائي (عادةً .data).

يوضح التفكيك في القائمة 4.4 الفرق: التسميتان dword_40CF60 و dword_40C000 تمثلان عناوين ذاكرة مطلقة مُدمجة في الشيفرة (Hard-Coded). تُنشئ IDA هذه التسميات لتكون أسماء بديلة لمراجع الذاكرة مثل [40CF60] و [40C000]، مما يجعل التجميع أكثر قابلية للقراءة. وبادئة التسمية تدل على حجم البيانات المخزنة هناك، بناءً على تحليل IDA.


6. اختبار سريع: argumentative

في الأسئلة التالية، افترض وجود إطار مكدس قياسي مبني على EBP.

السؤال الأول: هل تصل التعليمة التالية إلى متغير محلي أم إلى وسيط (Argument)؟

mov eax, [ebp+8]

السؤال الثاني: هل تصل التعليمة التالية إلى متغير محلي أم إلى وسيط؟

mov dword ptr [ebp-4], 42
الحل (اضغط للعرض)
  • السؤال الأول: إزاحة موجبة عن EBP ([ebp+8]) تعني الوصول إلى وسيط (الوسيط الأول؛ إذ يقع [ebp+4] على عنوان الإرجاع).
  • السؤال الثاني: إزاحة سالبة عن EBP ([ebp-4]) تعني الوصول إلى متغير محلي.

7. تمرين اختراق: funky_print

اكتب ونفّذ تسلسل تعليمات من أجل:

  • عند العنوان 401020h، أنشئ دالة تقوم بما يلي:
    • طباعة سلسلة نصية إلى الشاشة مُمرَّرة إلى الدالة على المكدس كوسيط. تتبع دالة printf اصطلاح الاستدعاء cdecl — تذكّر ضبط ESP بعد الاستدعاء بإزاحة تساوي أربعة أضعاف عدد الوسائط الممررة إلى printf.
  • عند العنوان 401000h، أنشئ شيفرة تقوم بما يلي:
    • استدعاء الدالة عند 401020h مع سلسلة وسيطة أنشأتها أو سلسلة موجودة في الذاكرة.
    • استدعاء ExitProcess برمز إرجاع 0.

درجة إضافية (Bonus):

  • أنشئ الدالة عند العنوان 401020h مع مقدمة (Prologue) وخاتمة (Epilogue) وتنظيف مكدس صحيحين.
الحل (اضغط للعرض)
; عند 401000h:
push 402118h          ; عنوان السلسلة النصية
call 401020h
push 0
call dword ptr [_ExitProcess]

; عند 401020h:
push ebp              ; مقدمة
mov  ebp, esp
push dword ptr [ebp+8] ; الوسيط الأول (عنوان السلسلة)
call dword ptr [_printf]
add  esp, 4           ; تنظيف وسيط واحد (cdecl)
mov  esp, ebp         ; خاتمة
pop  ebp
ret

خلاصة المقال

المفهومالنقاط الرئيسية
أطر المكدسكل استدعاء دالة ينشئ إطاراً؛ عمره = مدة تنفيذ الدالة
EBPنقطة ارتساء ثابتة للإطار؛ ESP يتحرك باستمرار
بنية الإطاروسائط ([ebp+8] فأعلى) ← عنوان الإرجاع ([ebp+4]) ← EBP محفوظ ← متغيرات محلية ([ebp-4] فأدنى)
مقدمة الدالةالتسلسل القياسي: push ebp ← mov ebp, esp ← sub esp, N
stdcallتنظيف المستدعى عبر retn n؛ اصطلاح Windows API
cdeclتنظيف المستدعي عبر add esp, n؛ اصطلاح مترجمات C
الوسائط والقيم المرجعةالوسائط تُدفع من اليمين إلى اليسار؛ الناتج يرجع في EAX
المتغيرات المحليةفي إطار المكدس؛ تُحرَّر عند رجوع الدالة
المتغيرات العامةفي مقطع .data؛ عناوين مطلقة ثابتة طوال عمر البرنامج

المصدر الأصلي: Malware Analysis Crash Course — FLARE Learning Hub