BinaryVision

Stack Format and Usage

מבוא

במעבדים ממשפחת 8086, שכרגע הם המעבדים הכי נפוצים במחשבים ביתיים, יש בכל Thread מחסנית שמשויכת אליו.
מחסנית זה דף בזיכרון שמאולקץ ע"י מערכת ההפעלה והשימוש בדף זיכרון זה מתבצע בצורה הבאה:

  • כשמכניסים מידע למחסנית, המידע מוכנס מסוף דף הזיכרון, וגודל לכיוון תחילת הדף
  • כשמוציאים מידע מהמחסנית, המחסנית קוטנת, וחוזרת חזרה לסוף דף הזיכרון שממנו התחילו להכניס מידע.

לטיפול במחסנית יש כמה אוגרים במעבד לשימוש הקוד:

  • SS – אוגר סגמנט שקובע באיזה דף יושבת המחסנית בזיכרון
  • ESP – המקום אליו יכנס מידע חדש למחסנית, או יוצא מידע מהמחסנית
  • EBP – אוגר שמשמש הרבה פיסות קוד בתור מצביע שני במחסנית לשימוש מתקדם

הפקודות הבסיסיות לשימוש המחסנית הם Push וPop.
פקודות נוספות שמשתמשות במחסנית, משתמשות בפעולה של Push וPop.
לדוגמה הפקודה Call:

call MyFuncAddr
-
push EIP
jmp MyFuncAddr

הצורך במחסנית

הצורך במחסנית נוצר כאשר רצו לשמור מידע שתקף לזמן ביצוע פונקצייה מסויימת,
הצורך לשמור משתנים שהועברו לאותה פונקצייה, והצורך לחזור מכל לפונקצייה למקום שקראו לה ממנו.
במחסנית הדבר מתאפשר ממש בקלות בגלל מבנה LIFO שמאפשר להוציא כל מה שנכנס לאחרונה,
ז"א פונקצייה שהולכת להתבצע מוסיפה את כל המידע שהיא צריכה למחסנית, וכשהיא מסתיימת אפשר להוציא את כל המידע שלה,
כי היא האחרונה שהתבצעה..
מה שמאפשר דברים כמו רקורסיה, שנותן לקרוא לאותה הפונקציה כמה פעמים שנרצה עם משתנים שונים כאילו זה פונקצייה שונה.
דברים נוספים התווספו למחסנית כמו למשל טיפול בחריגים (Exception Handling)

קונבצניות קריאה

קונבנציות קריאה (Calling Convention) זה סטנדרט שהוגדר שמשתמשים בו כדי לקרוא לפונקצייה.
יש כמה וכמה דרכים לקרוא לפונקצייה, הדרכים שונות בעיקר ב:

  • סדר העברת הפרמטרים
  • מקום איחסון הפרמטרים
  • אחריות ניקוי הפרמטרים מהמחסנית

הסעיפים הבאים מדגימים איך תראה קריאה לפונקצייה באסמבלי בכל אחת מהקונבנציות הנפוצות.

int func(int a,int b,int c)
{
 return 0;
}

cdecl

  • סדר העברת פרמטרים מימין לשמאל (סדר הפוך)
  • איחסון פרמטרים במחסנית
  • אחריות ניקוי על הקורא
push c
push b
push a
call func
add esp,12

stdcall

הקונבנציה הסטנדרטית של WIN32API

  • סדר העברת פרמטרים מימין לשמאל (סדר הפוך)
  • איחסון פרמטרים במחסנית
  • אחריות ניקוי על הנקרא
push c
push b
push a
call func

fastcall

קונבנציה לקריאה מהירה, אין סטנדרט אחיד, בדרך כלל זה תלוי בקומפילר.

  • הפרמטרים הראשונים (שמאל לימין) בECX ואחריו בEDX, והשאר במחסנית נדחפים מימין לשמאל (סדר הפוך)
  • איחסון פרמטרים גם באוגרים וגם במחסנית
  • אחריות ניקוי על הנקרא
mov ecx,a
mov edx,b
push c
call func

מסגרת מחסנית

מסגרת מחסנית (Stack Frame) זה קונספט שפיתחו לשימוש נוח יותר במשתנים ופרמטרים בתוך פונקצייה בזמן הריצה שלה.
הקונספט משתמש בEBP כדי להצביע למקום קבוע במחסנית, כך שניתן יהיה בצורה מדוייקית וקלה לגשת למשתנים מקומיים ולפרמטרים של הפונקצייה כאשר ניגשים לזיכרון יחסית לEBP.

ניקח לדוגמה את הפונקציה הבאה:

int func(int a,int b)
{
 int d;
 d = a + b;
 return d;
}

פונקצייה זו לא עושה הרבה, מחשבת סכום במשתנה מקומי, ומחזירה אותו.
נשתמש בקונבנציית קריאה stdcall, וכך בערך זה יראה באסמבלי, כולל הStack Frame.

func:
push ebp
mov ebp,esp
sub esp,4
mov eax, [ebp + 0ch]
add eax, [ebp + 8]
mov [ebp - 4], eax
mov esp,ebp
pop ebp
retn 8

הסבר שורות:

  • דוחפים את EBP כדי שהשימוש שלנו בו לא יהרוס את הStack Frame של הפונקצייה שקראה לנו
  • מעבירים לEBP את המצביע מחסנית הנוכחי בשביל המסגרת החדשה
  • מזיזים את מצביע המחסנית ב4 יותר גבוה כדי להשאיר מקום למשתנה מקומי d
  • מכניסים לאוגר זמני את הערך של פרמטר b (סדר הפוך של פרמטרים) (EBP + 0ch)
  • מוסיפים לאוגר את הערך של a
  • שומרים את הערך במשתנה d המקומי (EBP – 4)
  • מחזירים את המצביע של ESP למצב שהיה לפני שהוספנו משתנים מקומיים
  • משחזרים את הEBP המקורי
  • יוצאים מהפונקצייה ומנקים אחרינו 2 פרמטרים (8 בתים)

מצב המחסנית בזמן ביצוע הפעולה add: (המחסנית גודלת כלפי מעלה)

[EBP-4] - Variable D
[EBP]   - Old EBP
[EBP+4] - Return Address
[EBP+8] - Parameter A
[EBP+C] - Parameter B

שימושים נוספים

SEH

למחסנית יש שימושים נוספים חוץ משמירת משתנים ומסגרות.

שימוש נפוץ נוסף זה Structured Exception Handling או SEH.

כאשר במערכת ההפעלה קופץ Exception, או בגלל טעות של התוכנה, או בגלל שככה התוכנה מנהלת את השגיאות שלה.

נקרא קוד שאמור לטפל בשגיאה הזאת, אם הקוד לא מודיע שהוא טיפל בשגיאה, מעלים את השגיאה לקוד המטפל שבא לפניו.

סיכום

  • המחסנית היא חלק חשוב מאד בריצה של קוד.
  • ניתן לקמפל קוד ללא מחסנית, אבל זה לא פשוטף וברוב המקרים פשוט יממש מחסנית בעצמו
  • שיטת שימוש המחסנית, ושימושים נוספים בה תלוי בקומפילר
  • במחסנית מכילה לא רק נתונים אלא גם בקרה (כתובת חזרה, SEH, וכו') מה שיכול לגרום לבעיות אבטחה בקוד
5 תגובות:
  1. Fate

    מסתבר שזה די פשוט להוסיף כאן גם תגובות,
    אז יאללה,
    אם יש משהו להגיד, תגידו 🙂

  2. Debug

    מאמר נחמד מאוד,
    רק תיקון קטן:
    "במחסנית הדבר מתאפשר ממש בקלות בגלל מבנה FIFO שמאפשר להוציא כל מה שנכנס לאחרונה" – המחסנית היא LIFO ולא FIFO.

  3. Fate

    צודק, תוקן..
    תודה 🙂

  4. SV

    נראה לי שיש טעות בקטע של הפרמטרים בשורה הזאת :

    "מכניסים לאוגר זמני את הערך של פרמטר a (סדר הפוך של פרמטרים) (EBP + 0ch)"

    לפי מה שיצא לי וההסבר שלך מקודם זה אמור להיות פרמטר b ולא a ואחרי זה זה a

  5. Fate

    צודק, תיקנתי…
    בהמשך יש גם טבלה המפרטת את תוכן המחסנית ו- EBP+C זה באמת המשתנה b.

השאר תגובה

מחפש משהו?

תשתמש בטופס למטה כדי לחפש באתר: