חזור לתיקייה: 00440252 - מערכות ספרתיות ומבנה המחשב

1 - אסמבלי

חיפוש בתיקיה
Ctrl K
לחץ כאן או הקש Ctrl K כדי לחפש בכל התיקיה
  • הרגיסטרים
  • פקודות ריסק-5
  • פקודות בסיסיות
  • פקודות זיכרון
  • פקודות לוגיות
  • בקרת זרימה
  • קריאות לפונקציות
  • המחסנית
  • קריאה לפונקציה מתוך פונקציה

1 - אסמבלי

00440252

מערכות ספרתיות ומבנה המחשב

רשימות הרצאה לקריאה בשבת

אסמבלי

הופק מתוך תקיית התואר של שקד

uni-folder.vercel.app

שפת אסמבלי, בניגוד ל-CCC, ייחודית לכל מעבד. אנחנו נלמד סט פקודות המתאימות למעבד מסויים. לרוב, למעבדים של אותה חברה יהיו את אותן פקודות. כל מעבד מגיע עם אסמבלר מובנה מראש שמתרגם את שפת האסמבלי שהוא קורא(עם הפקודות הייחודיות לו) לשפת מכונה.
מעבד שתומך ברשימת פקודות בעצם מיישם סטנדרט שנקרא ISAISAISA שזה Instruction Set Architecture.

בתחילת עולם המחשוב השתמשו במשהו שנקרא CISCCISCCISC שאומר שככל שמתקדמים נוסיף עוד ועוד פקודות כדי שהמעבד יוכל לעשות דברים יותר מורכבים.
כיום משתמשים לרוב ב-RISCRISCRISC (שאיתו גם אנחנו נעבוד) שהוא נותן סט פקודות קטן אשר מאפשרות לך לבצע כל דבר ואתה הוא שתתכנת את התוכניות המסובכות באמצעות מעט הפקודות הפשוטות. כל הפקודות נמצאות על יצור בשם הכרטיס הירוק\color{green}\R{\text{הכרטיס הירוק}}הכרטיס הירוק (התמונה במצגת לא באיכות טובה ולא הצלחתי למצוא אותה באינטרנט - לטענת המצגת יש אותה במודל אבל לא הצלחתי למצוא אותה במודל).

הרגיסטרים

בניגוד לשפות תכנות עיליות כמו JavaJavaJava או CCC, באסמבלי אין לנו משתנים כפי שהכרנו אותם. יש לנו רגיסטרים שהם מספר סופי ומוגבל של מקומות מיוחדים שמחזיקים ערך יחיד. הם בנויים ישירות לתוך החומרה. פעולות יכולות להתבצע רק על מידע המאוחסן ברגיסטרים ולכן הם מאוד חשובים לנו, גם בתור מדמחיסטים וגם בתור חשמליסטים.
הייתרון של הרגיסטרים הוא שהם ממש קרובים למעבד ולכן מאוד מהירים.

בריסק-5 יש 323232 רגיסטרים הנקראים x0−x31x0-x31x0−x31 אך לנו יש רק 31 רגיסטרים לעבוד איתם שכן רגיסטר x0x0x0 הוא מיוחד. לכן, נדרש להיות מאוד יעילים עם המידע שלנו כדי שלא נעבור את מספר הרגיסטרים הקיים שכן פשוטו של עניין - אין עוד רגיסטרים.

הגדרה 1 (מילה)

קבוצה של 32 ביטים נקראת מילה בריסק-5.

הערה

זו לא כמו מילה של מתקא :(

הערה (אמיתית הפעם)

ניתן לקרוא רק 4GB4GB4GB (4 גיגה בית) עם מילה של ריסק-5 מכיוון שכדי להגיע לשם צריך לכתוב את הערך שאנחנו צריכים ברגיסטר וגם אם נמלא את כל הסיביות באחדים נקבל 2322^{32}232 שזה 444 גיגה.

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

RegisterABI NameDescriptionSaverx0zeroHard-wired zero−x1raReturn addressCallerx2spStack pointerCalleex3gpGlobal pointer−x4tpThread pointer−x5t0Temporary/alternative link registerCallerx6−7t1−2TemporariesCallerx8s0/fpSaved register/frame pointerCalleex9s1Saved registerCalleex10−11a0−1Function arguments/return valuesCallerx12−17a2−7Function argumentsCallerx18−27s2−11Saved registersCalleex28−31t3−6TemporariesCaller\begin{array}{|c|c|c|c|} \hline \text{Register} & \text{ABI Name} & \text{Description} & \text{Saver} \\ \hline x0 & zero & \text{Hard-wired zero} & - \\ x1 & ra & \text{Return address} & \text{Caller} \\ x2 & sp & \text{Stack pointer} & \text{Callee} \\ x3 & gp & \text{Global pointer} & - \\ x4 & tp & \text{Thread pointer} & - \\ x5 & t_{0} & \text{Temporary/alternative link register} & \text{Caller} \\ x6-7 & t_{1-2} & \text{Temporaries} & \text{Caller} \\ x8 & s_{0}/fp & \text{Saved register/frame pointer} & \text{Callee} \\ x9 & s1 & \text{Saved register} & \text{Callee} \\ x10-11 & a_{0-1} & \text{Function arguments/return values} & \text{Caller} \\ x12-17 & a_{2-7} & \text{Function arguments} & \text{Caller} \\ x18-27 & s_{2-11} & \text{Saved registers} & \text{Callee} \\ x28-31 & t_{3-6} & \text{Temporaries} & \text{Caller} \\ \hline \end{array}Registerx0x1x2x3x4x5x6−7x8x9x10−11x12−17x18−27x28−31​ABI Namezeroraspgptpt0​t1−2​s0​/fps1a0−1​a2−7​s2−11​t3−6​​DescriptionHard-wired zeroReturn addressStack pointerGlobal pointerThread pointerTemporary/alternative link registerTemporariesSaved register/frame pointerSaved registerFunction arguments/return valuesFunction argumentsSaved registersTemporaries​Saver−CallerCallee−−CallerCallerCalleeCalleeCallerCallerCalleeCaller​​

פקודות ריסק-5

לכל פקודה יש opcodeopcodeopcode שהוא המזהה של הפקודה ו-operandsoperandsoperands שהם מה שהפקודה מקבלת(כמו פונקציה ב-CCC)

add x1, x2, x3 # x1 = <x2> + <x3>

בואו ננתח את השורה הזו:

  • מתחילים עם שם הפקודה, ה-opcodeopcodeopcode שבמקרה הזה הוא add. זוהי פקודה שמוסיפה.
  • אחרי זה מופיע רגיסטר היעד שאליו נכנס הערך המחושב (כמעט תמיד רגיסטר היעד הוא האופרנד הראשון שנכתוב)
  • לאחריו יש את שני האופרנדים החשובים: המחובר הראשון והמחובר השני.
  • לאחר מכן יש #\## שהוא הערה ומה שנכתב לאחריו לא מחושב (כמו ////// ב-CCC)

והנה רשימת הפקודות:

פקודות בסיסיות

  • פקודת ההוספה addaddadd:
    add x1,x2,x2
    
    מוסיפה את x2x2x2 ו-x3x3x3 ושמה את הערך ב-x1x1x1.
  • פקודת החיסור subsubsub:
    sub x3,x4,x5
    
    מחסרת את x5x5x5 מ-x4x4x4 ומציבה את הערך ב-x3x3x3.
דוגמה

עבור קוד ה-CCC:

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
הערה

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

add x1,x3,x0

ואז זה ישים את הערך של x3x3x3 ברגיסטר x1x1x1 (כמובן שהערך ב-x3x3x3 לא משתנה)

  • פקודה לחיבור עם קבועים addiaddiaddi:
    addi x3,x4,-10
    
    הפקודה הזו מחברת את הערך ב-x4x4x4 לקבוע −10-10−10 ושמה את הערך ב-x3x3x3.
    הפקודה תמיד תפעל עם רגיסטר יעד שלאחריו רגיסטר מקור ולאחריו ערך קבוע.

פקודות זיכרון

כעת אנו יכולים לבצע פעולות בין רגיסטרים, אך יש לנו רק מעט מהם ונדרש לשמור בזיכרון רחוק יותר ואיטי יותר ערכים שנרצה לשמור לאורך זמן. פקודות הפנייה לזיכרון הן איטיות ונביט בהן כעת.
אנחנו מתייחסים לכתובות של הזיכרון בקבוצות של 8 ביטים (שזה בית(bytebytebyte) אחד) - ניתן לדמיין את הזיכרון כווקטור חד מימדי ארוך של מטריצות של 1×81\times 81×8.

Bit  (b)ביטסיבית 1Byte  (B)בית8 סיביותHalf  wordחצי-מילה סיביות16Wordמילה סיביות32\begin{array}{|ccc|} \hline Bit\; (b) & \R{\text{ביט}} & \R{\text{סיבית 1}} \\ Byte\; (B) & \R{\text{בית}} & \R{\text{8 סיביות}} \\ Half\; word & \R{\text{חצי-מילה}} & \R{\text{ סיביות}}16 \\ Word & \R{\text{מילה}} & \R{\text{ סיביות}}32 \\ \hline \end{array}Bit(b)Byte(B)HalfwordWord​ביטביתחצי-מילהמילה​סיבית 18 סיביות סיביות16 סיביות32​​

כדי לכתוב לזיכרון משהו באורך של יותר מביט, נכתוב תמיד לביט הכי פחות חשוב(LSBLSBLSB) - הימני ביותר.
תמיד מידע עובר מהזיכרון לרגיסטר או מהרגיסטר לזיכרון
אנו לא יכולים לעביר מידע ישר למעבד או ישר לזיכרון מהמעבד

  • הפקודה lwlwlw תשמש אותנו כדי להביא מידע מהזיכרון לרגיסטר.
    נניח ויש לנו מערך של 100100100 אינטים (כל אחד 444 בתים) בזיכרון ואנו רוצים להביא אותו לרגיסטר x10x10x10 נניח כדי שנוכל להשתמש בו. נבצע:
    lw x10, 12(x13)
    
    מה זה 12(x13)12(x13)12(x13) המוזר הזה? x13x13x13 מייצג את הרגיסטר הבסיסי שמצביע ל-A[0]A[0]A[0] ו-121212 זה מספר הבתים שאנחנו זזים כדי לקחת את A[3]A[3]A[3].
    אזי, בעצם אנחנו לוקחים את הערך הנמצא ב-A[3]A[3]A[3] ושמים אותו ברגיסטר x10x10x10.
  • הפקודה swswsw תשמש אותנו כדי לשמור מידע מרגיסטר בזיכרון
    נניח ונרצה לקחת את הערך מ-A[3]A[3]A[3] שנמצא כרגע ב-x10x10x10 לבצע עליו איזושהי פעולה ואז לשים אותו ב-A[10]A[10]A[10] - נעשה את פעולה ההחזרה כך:
    sw x10,40(x13)
    
    מכיוון שכל איבר במערך הוא 4 בתים, נכפיל 4 ב-10 כדי להגיע מ-x13x13x13 שמאחסן פויינטר לתחילת המערך, למיקום במערך שאנחנו רוצים.
  • פקודה לטעינת בית יחיד lblblb שעובדת באותו האופן שעבדה lwlwlw עבדה כך:
    lb x10,3(x11)
    
    כשמעתיקים בית מהזיכרון אל 32 בתים של רגיסטר, מתבצע תהליך של הרחבת הסימן על מנת שהסימן ישאר זהה, נניח אם הסימן הוא 000 למספר חיובי, כל שאר הסיביות של הרגיסטר יהיו אפסים ואז בסוף יופיע המספר.
  • פקודה לשמירה של בית יחיד בזיכרון sbsbsb עובדת גם היא באותו האופן שעבדה swswsw כך:
    sb x10,3(x11)
    
  • יש בנוסף פקודה לטעינת בית unsignedunsignedunsigned - כלומר, ללא סימן. הפקודה היא lbulbulbu והיא מתנהגת באופן זהה ל-lwlwlw רק שאז הסימן לא ישנה.
  • אין פקודה לשמירת בית unsignedunsignedunsigned שכן אין לזה משמעות.
דוגמה

מה יקרה כשנבצע את הקוד הזה:

addi x11,x0,0x3f5
sw x11,0(x5)
lb x12,1(x5)

תחילה, אנחנו שמים ברגיסטר x11x11x11 את הערך ההקסה 3f53f53f5 ששווה ל-0011,1111,01010011,1111,01010011,1111,0101 בבינארי.
כעת, אנחנו שומרים את הערך שיש ב-x11x11x11 (כלומר את 3f53f53f5 בהקסה) במיקום x5x5x5 בזיכרון.
כעת, אנו טוענים לרגיסטר x12x12x12 את הערך שיש בבית השני של x5x5x5 - כלומר, אנחנו טוענים את 0000,00110000,00110000,0011 שהוא המספר 333 בבסיס עשרוני ובבסיס הקסה.

הערה

אם נרצה לתאר מספר בבסיס עשרוני נכתוב 0d0d0d לפניו.
אם נרצה לתאר מספר בבסיס הקסה נכתוב 0x0x0x לפניו.
אם נרצה לתאר מספר בבסיס בינארי נכתוב 0b0b0b לפניו.

פקודות לוגיות

Logical operationsC operatorsRISC-V operationsBit-by-bit AND&andBit-by-bit OR∣orBit-by-bit XOR^xorShift left logical<<sllShift right logical>>srl\begin{array}{|ccc|} \hline \text{Logical operations} & \text{C operators} & \text{RISC-V operations} \\ \hline \text{Bit-by-bit AND} & \& & and \\ \text{Bit-by-bit OR} & | & or \\ \text{Bit-by-bit XOR} & \hat{} & xor \\ \text{Shift left logical} & << & sll \\ \text{Shift right logical} & >> & srl \\ \hline \end{array}Logical operationsBit-by-bit ANDBit-by-bit ORBit-by-bit XORShift left logicalShift right logical​C operators&∣^<<>>​RISC-V operationsandorxorsllsrl​​

ניתן להשתמש בהן כך:

opcode <result>,<compare1>,<compare2>

הפעולה הלוגית נעשית על שני האופרנדנים ומושמת לרגיסטר הנמצא ב-resultresultresult.
לפקודות ההזזה, כמובן שהערך מגיע מרגיסטר אחר להזזה ומושם אחרי ההזזה ברגיסטר התוצאה.

בנוסף, פקודות נוספות הינן:

  • פקודת ההזזה שמאלה עם קבוע כך: (מכפילה ב-2k2^k2k)
    slli x11,x12,2
    
  • פקודת הזזה ימינה עם קבוע כך: (מחלקת ב-2k2^k2k)
    srli x11,x12,2
    
  • פקודת הזזה ימינה של המספר עם תיקון למען שמירת הסימן:
    srai x10,x10,4
    
הערה

הפקודה sraisraisrai היא לא חלוקה ב-2k2^{k}2k.
זה עובד לרוב, אך לא עובד עם מספרים שליליים אי-זוגיים.

הערה

הפקודות sraisraisrai ו-srlisrlisrli זהות במספרים חיוביים.

בקרת זרימה

כעת, נרצה משהו הדומה ל-ififif אחרת לא נוכל לכתוב תוכניות טובות. בנוסף, נרצה דרך לעבור ממקום למקום בשביל שנוכל לעשות לולאות בנוסף לדברים אחרים.

הפקודה הראשונה שאנו נראה היא beqbeqbeq שמשווה בין שני רגיסטרים ואם הם שווים קופצת להצהרה בשם שכתבנו כך:

beq register1,register2,L1

ישנן עוד פקודות שעובדות כך והן:

  • הפקודה beqbeqbeq(שראינו הרגע) משווה בין שני רגיסטרים ואם הם שווים קופצת להצהרה הרשומה.
  • הפקודה bnebnebne משווה בין שני רגיסטרים ואם הם לא שווים קופצת להצהרה הרשומה.
  • הפקודה BltBltBlt משווה בין שני רגיסטרים ואם הרגיסטר הראשון קטן מהרגיסטר השני אז היא קופצת להצהרה הרשומה.
  • הפקודה bgebgebge משווה בין שני רגיסטרים ואם הרגיסטר הראשון גדול או שווה לרגיסטר השני אזי היא קופצת להצהרה הרשומה.
  • הפקודה JJJ קופצת תמיד להצהרה הרשומה (היא מקבלת רק הצהרה)

בואו נראה דוגמות:

דוגמה (תוכנית ififif פשוטה)

נניח התוכנית:

if (i == j)
	f = g + h;

ב-CCC תראה כך באסמבלי:

bne x13,x14,Exit
add x10,x11,x12

Exit:
דוגמה (תוכנית if−elseif-elseif−else פשוטה)

התוכנית:

if (i == j)
	f = g + h;
else 
	f = g - h;

ב-CCC תראה כך באסמבלי:

bne x13,x14, Else
add x10,x11,x12
j Exit

Else: sub x10,x11,x12
Exit:
דוגמה (לולאה)

נניח קוד ה-CCC הבא שמכיל לולאה:

int A[20];
int sum = 0;
for (int i = 0; i < 20; i++)
	sum += A[i];

תראה כך באסמבלי כאשר x8x8x8 הוא הרגיסטר שמצביע על תחילת המערך:

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

קריאות לפונקציות

עוד לפני שנדבר על איך אנו יוצרים פונקציות, נדבר על איך משתמשים בהן.
יש לנו שישה שלבים בקריאה לפונקציה:

  1. נשים את הארגומנטים במקום שהפונקציה יכולה לגשת אליו.
  2. נעביר שליטה לפונקציה
  3. נאסוף את כל הזכרון שנדרש עבור הפונקציה
  4. נבצע את הפעולה של הפונקציה
  5. נשים את התוצאה איפה שהקוד שקרא יכול לגשת אליה ושחזור הרגיסטרים שהשתמשנו בהם בפונקציה.
  6. החזרת שליטה למקור.

כדי לשמור את המיקום אליו צריך לחזור לאחר הפונקציה, נשתמש ברגיסטר rarara אשר הינו קיצור ל-return address.
בסוף פונקציה תמיד נבצע jr ra (הפקודה jr היא פסודו פקודה אשר מאפשרת קפיצה למיקום המאוחסן ברגיסטר המסויים שנקרא, במקרה הזה rarara)

דוגמה

נממש את הפונקציה:

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

יש לנו פקודה שימושית על מנת לשמור את ה-rarara ולקרוא לפונקציה באותו הזמן והיא jal. היא שומרת ב-rarara את הכתובת הבאה מהשורה שהיא נרשמה בה וקוראת לפונקציה הרשומה.

דוגמה

בדוגמה שראינו קודם נוכל לכתוב:

1000 mv a0, s0
1004 mv a1, s1
1008 jal sum
....

ישנו בנוסף, קיצור עבור jr    rajr\;\; rajrra עם הרגיסטר rarara ספציפית והוא: ret שהוא זהה בדיוק.

דוגמה

נראה בדוגמה ממקודם:

2000 sum: add a0, a0, a1
2004 ret

המחסנית

לפעמים נרצה לאחסן דברים נוספים אשר לא יתאימו ברגיסטרים.
לשם כך, נוכל להשתמש במחסנית, אשר פועלת בתצורת LIFOLIFOLIFO. על מנת להשתמש במחסנית, נדרש מצביע אליה כדי לגשת אליה. לכן, יש לנו את הרגיסטר בשם spspsp אשר תמיד מצביע לנקודה הכי נמוכה במחסנית (כאשר המחסנית גדלה למטה ⟸ כאשר מכניסים איבר מכניסים אותו ללמטה

דוגמה

נרצה לממש את הפונקציה הבאה באסמבלי.

int leaf(int g, int h, int i, int j)
{
	int f;
	f = (g + h) - (i + j);
	return f;
}

נניח ואנו משום מה חייבים להשתמש ב-s0,s1s_{0},s_{1}s0​,s1​ שכפי שאנו יודעים הם רגיסטרים שחייבים לשמור על הערך שלהם.
נרצה לראות כיצד מגבים אותם:

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

קריאה לפונקציה מתוך פונקציה

ראינו כי אנו משתמשים ברגיסטר rarara לכתובת החזרה, אך אם אנו קוראים לפונקציה בתוך פונקציה, אנו צריכים לשמור שני ערכים עבור כתובת ההחזרה: כתובת ההחזרה מהפונקציה המקוננת אל הפונקציה וכתובת ההחזרה מהפונקציה אל המקור.
נעשה זאת באמצעות שמירה של rarara למחסנית כפי שראינו עכשיו.

דוגמה

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