מה קורה כשאנחנו כותבים int n= 10
בתוכנית שלנו? הקומפיילר מקצה למשתנה הזה כתובת בזכרון ומגדיר לו תג n
.
באסמבלי, אנחנו נעשה את הדברים האלה בעצמו, כדי להבין בכלל איך עושים את זה, נבין שהתוכנית שלנו מחולקת ל 4 חלקים עיקריים
בתוכנית אסמבלי הסגמנט הזה יסומן על ידי data.
לכל דבר שנרצה שיהיה לו זכרון בdata נשייך label כשלהו בסינתקס :my_label
, ניתן לשים נקודה לפני label או לתת לו ערך שהוא ספרה בין 0 ל 9 ואז הוא numeric אבל לא אכנס לזה בסיכום הזה.
counter: .word 15
במערכת little endian זה ייראה ככה בזכרון
נוכל לתת מספר ערכים לאלמנט ב data למשל
.data
alice: .byte 3,5,10,-7
בזכרון זה יישמר ככה
כמו כן נשים לב שאם היינו שמים אלמנט נוסף מתחת לאליס אז הגישה אליו בזכרון הייתה רציפה כלומר alice+4 היה מוביל אותנו לאלאמנט הזה.
space 10*4.
זה יקצה 4 בתים על 10 משתנים כלומר מערך של 10 אינטים למשל.איך לגשת למשתנים האלו?
נניח שיש לנו את הlabels הבאים
a: .quad 1000
b: .word 0x10
c: .byte 3
נגשים לכל אחד מהם כאילו ניגשים ל immediate, למשל
movq $b , %rax
הסימון הזה מחזיר את ה״ערך המיידי של a" כלומר הכתובת בזכרון של a. זה קצת מבלבל כי אם היינו עושים את הסימון הזה על מספר היינו מקבלים את הערך עצמו ולא את הכתובת, אבל ה immediate של לייבל זה הכתובת בזכרון.
החלק הזה section .rodata.
הוא מכיל רק read only data. למשל סטרינגים קבועים וכו. למשל
.section .rodata
format1: .string "%d +0x%x =%d\n"
format2: .string "%ld*2%d = %ld\n"
יתחיל ב text.
ויכיל את החלק של הקוד עצמו. למשל
.text
addl $20, %eax
גם לפעולות יש כתובת ונגע בזה בהמשך בפונקציות.
אנחנו יכולים לקרוא ולכתוב לאן שאנחנו רוצים במחסנית כל עוד לא חרגנו ממנה באמצעו ה rsp . שתמיד מצביע לראש המחסנית בעת שלב כלשהו בתוכנית.
לכל פונקצייה שתכנס למחסנית נוסיף אליה זכרון במבנה של LIFO.
נשים לב שהמחסנית היא מימוש שלנו למבנה המחסנית המוכר שאנחנו מכירים מתוכניות c. כלומר ככל הנראה יהיו אי דיוקים ביחס למה שאנחנו עושים לאיך שהקומפיילר מממש את זה במדויק.
כמו כן יש גם תהליכים אוטומטיים שגורמים לתזוזה של המחסנית בזכרון כמו
כמו כן נשים לב שהמחסנית והערימה ״חולקות״ את אותו מרחב זכרון והם מתקרבים אחד לשני, כלומר, על ידי החסרה של rsp אנחנו מוסיפים לראש המחסנית.
בסיום תהליך כלשהו שקשור למחסנית, נעלה את כתובת המחסנית בחזרה.
אמרנו ש rsp מצביע לראש המחסנית, rbp הוא רגיסטר שמשמש כframe pointer. כלומר כאשר אנחנו לא יודעים בכמה הstack frame גדל, נרצה להשתמש בrbp כדי להחזיק מצביע לראש המחסנית הקודם. (ניתן להשתמש בו כסתם register אם אנחנו יודעים בכמה המחסנית גדלה וקטנה).
אנחנו יודעים שה rsp תמיד מצביע לסוף הזכרון הדרוש עבור פעולה כלשהי ולאט לאט מתחיל לחזור אחורה (במקרה הזה להעלות בערכי הכתובות) . ה rbp מאפשר לנו להחזיק את הכתובת של תחילת התהליך ולא של סופו ובאופן הזה נוכל להתחיל מהתחלה אם קרתה איזשהי שגיאה בניהול ה sp.
נשים לב שהשימוש ב rbp רלוונטי כאשר אנחנו לא יודעים מהו הגודל שנצטרך להוסיף ל rsp , אם זה גודל קבוע אז לא צריך אותו וניתן להשתמש בו כregister כללי.
נוכל להשתמש בכתובות המחסנית כדי לבצע השמה לערכים מרגיסטרים באופן הבא:
subq $8, %rsp
movq %rdi, (%rsp)
או שנשתמש בפקודה pushq %rdi
, הפעולה הזאת תזיז באופן אוטומטי את rsp בקפיצה של 8 בייטים.
באופן דומה נשתמש ב popq
כדי להוריד איברים מהמחסנית ולהגדיל את rsp.
בתחילת תוכנית לרוב נראה את שתי הפקודות הבאות
main:
# in case of a variable-size stack frame:
pushq %rbp #saving the old frame pointer
movq %rsp, %rbp #creating the new frame pointer.