שפת אסמבלי, בניגוד ל-, ייחודית לכל מעבד. אנחנו נלמד סט פקודות המתאימות למעבד מסויים. לרוב, למעבדים של אותה חברה יהיו את אותן פקודות. כל מעבד מגיע עם אסמבלר מובנה מראש שמתרגם את שפת האסמבלי שהוא קורא(עם הפקודות הייחודיות לו) לשפת מכונה.
מעבד שתומך ברשימת פקודות בעצם מיישם סטנדרט שנקרא שזה Instruction Set Architecture.
בתחילת עולם המחשוב השתמשו במשהו שנקרא שאומר שככל שמתקדמים נוסיף עוד ועוד פקודות כדי שהמעבד יוכל לעשות דברים יותר מורכבים.
כיום משתמשים לרוב ב- (שאיתו גם אנחנו נעבוד) שהוא נותן סט פקודות קטן אשר מאפשרות לך לבצע כל דבר ואתה הוא שתתכנת את התוכניות המסובכות באמצעות מעט הפקודות הפשוטות. כל הפקודות נמצאות על יצור בשם (התמונה במצגת לא באיכות טובה ולא הצלחתי למצוא אותה באינטרנט - לטענת המצגת יש אותה במודל אבל לא הצלחתי למצוא אותה במודל).
הרגיסטרים
בניגוד לשפות תכנות עיליות כמו או , באסמבלי אין לנו משתנים כפי שהכרנו אותם. יש לנו רגיסטרים שהם מספר סופי ומוגבל של מקומות מיוחדים שמחזיקים ערך יחיד. הם בנויים ישירות לתוך החומרה. פעולות יכולות להתבצע רק על מידע המאוחסן ברגיסטרים ולכן הם מאוד חשובים לנו, גם בתור מדמחיסטים וגם בתור חשמליסטים.
הייתרון של הרגיסטרים הוא שהם ממש קרובים למעבד ולכן מאוד מהירים.
בריסק-5 יש רגיסטרים הנקראים אך לנו יש רק 31 רגיסטרים לעבוד איתם שכן רגיסטר הוא מיוחד. לכן, נדרש להיות מאוד יעילים עם המידע שלנו כדי שלא נעבור את מספר הרגיסטרים הקיים שכן פשוטו של עניין - אין עוד רגיסטרים.
קבוצה של 32 ביטים נקראת מילה בריסק-5.
זו לא כמו מילה של מתקא :(
ניתן לקרוא רק (4 גיגה בית) עם מילה של ריסק-5 מכיוון שכדי להגיע לשם צריך לכתוב את הערך שאנחנו צריכים ברגיסטר וגם אם נמלא את כל הסיביות באחדים נקבל שזה גיגה.
בניגוד לשפות תכנות עיליות, אין סוגי משתנים ברגיסטרים וכל רגיסטר יכול להכיל כל דבר ואנו צריכים לזכור מה שמנו בכל רגיסטר.
פקודות ריסק-5
לכל פקודה יש שהוא המזהה של הפקודה ו- שהם מה שהפקודה מקבלת(כמו פונקציה ב-)
add x1, x2, x3 # x1 = <x2> + <x3>
בואו ננתח את השורה הזו:
- מתחילים עם שם הפקודה, ה- שבמקרה הזה הוא
add. זוהי פקודה שמוסיפה. - אחרי זה מופיע רגיסטר היעד שאליו נכנס הערך המחושב (כמעט תמיד רגיסטר היעד הוא האופרנד הראשון שנכתוב)
- לאחריו יש את שני האופרנדים החשובים: המחובר הראשון והמחובר השני.
- לאחר מכן יש שהוא הערה ומה שנכתב לאחריו לא מחושב (כמו ב-)
והנה רשימת הפקודות:
פקודות בסיסיות
- פקודת ההוספה :
מוסיפה את ו- ושמה את הערך ב-.add x1,x2,x2 - פקודת החיסור :
מחסרת את מ- ומציבה את הערך ב-.sub x3,x4,x5
עבור קוד ה-:
a=b+c+d-e;
נכתוב באסמבלי של ריסק-5:
add x10, x1, x2 # temp = b + c
add x10, x10, x3 # temp = temp + d
sub x10, x10, x4 # a = temp - e
אם נרצה לשים ערך של רגיסטר אחד ברגיסטר אחר נשתמש ברגיסטר שהוא תמיד אפס כך:
add x1,x3,x0
ואז זה ישים את הערך של ברגיסטר (כמובן שהערך ב- לא משתנה)
- פקודה לחיבור עם קבועים :
הפקודה הזו מחברת את הערך ב- לקבוע ושמה את הערך ב-.addi x3,x4,-10
הפקודה תמיד תפעל עם רגיסטר יעד שלאחריו רגיסטר מקור ולאחריו ערך קבוע.
פקודות זיכרון
כעת אנו יכולים לבצע פעולות בין רגיסטרים, אך יש לנו רק מעט מהם ונדרש לשמור בזיכרון רחוק יותר ואיטי יותר ערכים שנרצה לשמור לאורך זמן. פקודות הפנייה לזיכרון הן איטיות ונביט בהן כעת.
אנחנו מתייחסים לכתובות של הזיכרון בקבוצות של 8 ביטים (שזה בית() אחד) - ניתן לדמיין את הזיכרון כווקטור חד מימדי ארוך של מטריצות של .
כדי לכתוב לזיכרון משהו באורך של יותר מביט, נכתוב תמיד לביט הכי פחות חשוב() - הימני ביותר.
תמיד מידע עובר מהזיכרון לרגיסטר או מהרגיסטר לזיכרון
אנו לא יכולים לעביר מידע ישר למעבד או ישר לזיכרון מהמעבד
- הפקודה תשמש אותנו כדי להביא מידע מהזיכרון לרגיסטר.
נניח ויש לנו מערך של אינטים (כל אחד בתים) בזיכרון ואנו רוצים להביא אותו לרגיסטר נניח כדי שנוכל להשתמש בו. נבצע:
מה זה המוזר הזה? מייצג את הרגיסטר הבסיסי שמצביע ל- ו- זה מספר הבתים שאנחנו זזים כדי לקחת את .lw x10, 12(x13)
אזי, בעצם אנחנו לוקחים את הערך הנמצא ב- ושמים אותו ברגיסטר . - הפקודה תשמש אותנו כדי לשמור מידע מרגיסטר בזיכרון
נניח ונרצה לקחת את הערך מ- שנמצא כרגע ב- לבצע עליו איזושהי פעולה ואז לשים אותו ב- - נעשה את פעולה ההחזרה כך:
מכיוון שכל איבר במערך הוא 4 בתים, נכפיל 4 ב-10 כדי להגיע מ- שמאחסן פויינטר לתחילת המערך, למיקום במערך שאנחנו רוצים.sw x10,40(x13) - פקודה לטעינת בית יחיד שעובדת באותו האופן שעבדה עבדה כך:
כשמעתיקים בית מהזיכרון אל 32 בתים של רגיסטר, מתבצע תהליך של הרחבת הסימן על מנת שהסימן ישאר זהה, נניח אם הסימן הוא למספר חיובי, כל שאר הסיביות של הרגיסטר יהיו אפסים ואז בסוף יופיע המספר.lb x10,3(x11) - פקודה לשמירה של בית יחיד בזיכרון עובדת גם היא באותו האופן שעבדה כך:
sb x10,3(x11) - יש בנוסף פקודה לטעינת בית - כלומר, ללא סימן. הפקודה היא והיא מתנהגת באופן זהה ל- רק שאז הסימן לא ישנה.
- אין פקודה לשמירת בית שכן אין לזה משמעות.
מה יקרה כשנבצע את הקוד הזה:
addi x11,x0,0x3f5
sw x11,0(x5)
lb x12,1(x5)
תחילה, אנחנו שמים ברגיסטר את הערך ההקסה ששווה ל- בבינארי.
כעת, אנחנו שומרים את הערך שיש ב- (כלומר את בהקסה) במיקום בזיכרון.
כעת, אנו טוענים לרגיסטר את הערך שיש בבית השני של - כלומר, אנחנו טוענים את שהוא המספר בבסיס עשרוני ובבסיס הקסה.
אם נרצה לתאר מספר בבסיס עשרוני נכתוב לפניו.
אם נרצה לתאר מספר בבסיס הקסה נכתוב לפניו.
אם נרצה לתאר מספר בבסיס בינארי נכתוב לפניו.
פקודות לוגיות
ניתן להשתמש בהן כך:
opcode <result>,<compare1>,<compare2>
הפעולה הלוגית נעשית על שני האופרנדנים ומושמת לרגיסטר הנמצא ב-.
לפקודות ההזזה, כמובן שהערך מגיע מרגיסטר אחר להזזה ומושם אחרי ההזזה ברגיסטר התוצאה.
בנוסף, פקודות נוספות הינן:
- פקודת ההזזה שמאלה עם קבוע כך: (מכפילה ב-)
slli x11,x12,2 - פקודת הזזה ימינה עם קבוע כך: (מחלקת ב-)
srli x11,x12,2 - פקודת הזזה ימינה של המספר עם תיקון למען שמירת הסימן:
srai x10,x10,4
הפקודה היא לא חלוקה ב-.
זה עובד לרוב, אך לא עובד עם מספרים שליליים אי-זוגיים.
הפקודות ו- זהות במספרים חיוביים.
בקרת זרימה
כעת, נרצה משהו הדומה ל- אחרת לא נוכל לכתוב תוכניות טובות. בנוסף, נרצה דרך לעבור ממקום למקום בשביל שנוכל לעשות לולאות בנוסף לדברים אחרים.
הפקודה הראשונה שאנו נראה היא שמשווה בין שני רגיסטרים ואם הם שווים קופצת להצהרה בשם שכתבנו כך:
beq register1,register2,L1
ישנן עוד פקודות שעובדות כך והן:
- הפקודה (שראינו הרגע) משווה בין שני רגיסטרים ואם הם שווים קופצת להצהרה הרשומה.
- הפקודה משווה בין שני רגיסטרים ואם הם לא שווים קופצת להצהרה הרשומה.
- הפקודה משווה בין שני רגיסטרים ואם הרגיסטר הראשון קטן מהרגיסטר השני אז היא קופצת להצהרה הרשומה.
- הפקודה משווה בין שני רגיסטרים ואם הרגיסטר הראשון גדול או שווה לרגיסטר השני אזי היא קופצת להצהרה הרשומה.
- הפקודה קופצת תמיד להצהרה הרשומה (היא מקבלת רק הצהרה)
בואו נראה דוגמות:
נניח התוכנית:
if (i == j)
f = g + h;
ב- תראה כך באסמבלי:
bne x13,x14,Exit
add x10,x11,x12
Exit:
התוכנית:
if (i == j)
f = g + h;
else
f = g - h;
ב- תראה כך באסמבלי:
bne x13,x14, Else
add x10,x11,x12
j Exit
Else: sub x10,x11,x12
Exit:
נניח קוד ה- הבא שמכיל לולאה:
int A[20];
int sum = 0;
for (int i = 0; i < 20; i++)
sum += A[i];
תראה כך באסמבלי כאשר הוא הרגיסטר שמצביע על תחילת המערך:
add x9,x8,x0 # x9=&[0]
add x10,x0,x0 # sum=0
add x11,x0,x0 # i=0
addi x13,x0,20 # x13=20
Loop:
lw x12,0(x9) # x12=A[i]
add x10,x10,x12 # sum+=A[i]
addi x9,x9,4 # &A[++i]
addi x11,x11,1 # i++
blt x11,x13,Loop
קריאות לפונקציות
עוד לפני שנדבר על איך אנו יוצרים פונקציות, נדבר על איך משתמשים בהן.
יש לנו שישה שלבים בקריאה לפונקציה:
- נשים את הארגומנטים במקום שהפונקציה יכולה לגשת אליו.
- נעביר שליטה לפונקציה
- נאסוף את כל הזכרון שנדרש עבור הפונקציה
- נבצע את הפעולה של הפונקציה
- נשים את התוצאה איפה שהקוד שקרא יכול לגשת אליה ושחזור הרגיסטרים שהשתמשנו בהם בפונקציה.
- החזרת שליטה למקור.
כדי לשמור את המיקום אליו צריך לחזור לאחר הפונקציה, נשתמש ברגיסטר אשר הינו קיצור ל-return address.
בסוף פונקציה תמיד נבצע jr ra (הפקודה jr היא פסודו פקודה אשר מאפשרת קפיצה למיקום המאוחסן ברגיסטר המסויים שנקרא, במקרה הזה )
נממש את הפונקציה:
int sum(int x, int y)
{
return (x + y);
}
וקריאה אליה כך:
1000 mv a0, s0
1004 mv a1, s1
1008 addi ra, zero, 1016
1012 j sum
1016 ...
...
2000 sum: add a0, a0, a1
2004 jr ra
יש לנו פקודה שימושית על מנת לשמור את ה- ולקרוא לפונקציה באותו הזמן והיא jal. היא שומרת ב- את הכתובת הבאה מהשורה שהיא נרשמה בה וקוראת לפונקציה הרשומה.
בדוגמה שראינו קודם נוכל לכתוב:
1000 mv a0, s0
1004 mv a1, s1
1008 jal sum
....
ישנו בנוסף, קיצור עבור עם הרגיסטר ספציפית והוא: ret שהוא זהה בדיוק.
נראה בדוגמה ממקודם:
2000 sum: add a0, a0, a1
2004 ret
המחסנית
לפעמים נרצה לאחסן דברים נוספים אשר לא יתאימו ברגיסטרים.
לשם כך, נוכל להשתמש במחסנית, אשר פועלת בתצורת . על מנת להשתמש במחסנית, נדרש מצביע אליה כדי לגשת אליה. לכן, יש לנו את הרגיסטר בשם אשר תמיד מצביע לנקודה הכי נמוכה במחסנית (כאשר המחסנית גדלה למטה ⟸ כאשר מכניסים איבר מכניסים אותו ללמטה
נרצה לממש את הפונקציה הבאה באסמבלי.
int leaf(int g, int h, int i, int j)
{
int f;
f = (g + h) - (i + j);
return f;
}
נניח ואנו משום מה חייבים להשתמש ב- שכפי שאנו יודעים הם רגיסטרים שחייבים לשמור על הערך שלהם.
נרצה לראות כיצד מגבים אותם:
Leaf:
addi sp, sp, -8 # adjust stack for 2 items
sw s1, 4(sp) # save s1 for use afterwards
sw s0, 0(sp) # save s0 for use afterwards
add s0, a0, a1 # s0 = g + h
add s1, a2, a3 # s1 = i + j
sub a0, s0, s1 # return value (g + h) - (i + j)
lw s0, 0(sp) # restore register s0 for caller
lw s1, 4(sp) # restore register s1 for caller
addi sp, sp, 8 # adjust stack to delete 2 items
ret # returning to original location
קריאה לפונקציה מתוך פונקציה
ראינו כי אנו משתמשים ברגיסטר לכתובת החזרה, אך אם אנו קוראים לפונקציה בתוך פונקציה, אנו צריכים לשמור שני ערכים עבור כתובת ההחזרה: כתובת ההחזרה מהפונקציה המקוננת אל הפונקציה וכתובת ההחזרה מהפונקציה אל המקור.
נעשה זאת באמצעות שמירה של למחסנית כפי שראינו עכשיו.
int sumSquare(int x, int y)
{
return mult(x,x) + y;
}
נממש את הקוד הבא באסמבלי כך:
sumSquare: # param x in a0, y in a1, return in a0
addi sp, sp, -8 # space on stack
sw ra, 4(sp) # save ret addr
sw a1, 0(sp) # save y (since we are going to use a1)
mv a1, a0
jal mult # call mult: get param in a0,a1; return in a0
lw a1, 0(sp) # restore y
add a0, a0, a1
lw ra, 4(sp) # get ret addr
addi sp, sp, 8 # restore stack
ret # = jr ra