3.1 הקדמה הרצאה
פתיחה לפרק: מבוא לשפת C – השלב הבא בפיתוח מערכת הפעלה¶
כל הכבוד שהגעתם עד לכאן!
בפרקים הקודמים צללנו לעומק הארכיטקטורה של מעבד ה־8086, הכרנו את עקרונות העבודה של Protected Mode, וראינו איך קרנל מודרני נבנה מהיסוד – כולל ניהול זיכרון, פסיקות, ו־context switching.
השליטה שלך ב־Assembly ובחומרה כבר ברמה שלא מביישת מפתחים מנוסים בתעשייה.
עכשיו, הגיע הזמן לעשות את הצעד הבא: לכתוב קוד בשפת C.
למה ללמוד שפת C?¶
שפת C היא השפה המרכזית לפיתוח מערכות הפעלה, קרנלים, דרייברים, וממשקי חומרה.
היא מאפשרת לך לשמור על שליטה מלאה בזיכרון ובכתובות – בדיוק כמו ב־Assembly – אבל עם תחביר נוח יותר, יכולות מופשטות, ותמיכה בכלים מודרניים של תכנות.
אפשר לחשוב על C כעל "אסמבלי עם קיצור דרך":
-
היא מקמפלת ישירות לשפת מכונה – בדיוק כמו אסמבלי.
-
היא מאפשרת גישה ישירה לזיכרון כמו אסמבלי
-
היא עדיין מציעה דברים כמו משתנים, פונקציות, תנאים ולולאות בצורה קריאה וברורה.
איך נלמד?¶
במהלך הפרק נלמד את שפת C בצורה שונה מהרגיל:
אנחנו נעשה קורלציה ישירה בין קוד C לבין קוד האסמבלי שנוצר ממנו.
כל פקודת C שנכתוב – נבחן מה הקומפיילר מייצר באסמבלי מאחוריה, ונראה איך היא מתורגמת לפעולה ברמת החומרה.
כך תוכל להבין לעומק מה באמת קורה "מאחורי הקלעים" – ולכתוב קוד C בצורה מודעת, מדויקת ויעילה.
סביבת העבודה – לינוקס¶
כדי להתחיל, נעבוד בסביבת Linux.
לינוקס הוא קרנל פשוט יחסית, עם ממשק קריאות מערכת (syscalls) ברור, תיעוד רחב, והרבה מאוד כלים ללימוד ותכנות ברמת low-level.
בתוך לינוקס, נלמד כיצד:
-
לכתוב קוד C שמריץ תוכניות ביוזר מוד
-
להשתמש בקריאות מערכת כמו
write,exit, ו־malloc -
להבין איזה פעולות יש בלינוקס, כיצד הן עובדות בקרנל ומה ניתן לעשות.
מטרת הפרק¶
בפרק הזה נלמד:
-
את הבסיס של התחביר של שפת C
-
איך משתנים, פונקציות ותנאים נראים באסמבלי
-
איך קוד C מדבר עם מערכת ההפעלה דרך Syscalls
-
ואיך נוכל להתחיל לכתוב תוכנות אמיתיות – שירוצו על גבי הקרנל שבנינו
אם אתם שולטים באסמבלי – שפת C לא תרחיק אתכם מהחומרה.
להפך – היא תעניק לכם את הכלים לבנות מערכות גדולות ומורכבות יותר, ולהתמקד במה שחשוב באמת – בלי לאבד שליטה על הזיכרון, הביצועים או הרמות הנמוכות של המערכת.
מוכנים להתחיל?
בואו נצלול יחד לשפת C – השפה שבאמת בנתה את עולם המחשבים שאנחנו חיים בו.
התקנות¶
אם לא עשיתם עדיין את הקורס לינוקס שלנו, עשו אותו לפני!
בפרק הזה תוכלו להשתמש באיזה מערכת הפעלה מבוססת לינוקס שתרצו, אני אשתמש בubuntu.
בפרק זה בחרו עורך טקסט שנשתמש בו כדי לכתוב קוד C, אני משתמש בsublime.

שלום עולם¶
פתחו קובץ "main.c" עם העורך טקסט שלכם.

כתבו את הקוד הבא ולחצו על ctrl + s כדי לשמור את הקובץ בעורך

קמפלו את הקוד C לקובץ הרצה באמצעות הקומפיילר "gcc" (gnu cross compiler)

בפקודה למעלה, אנחנו מקמפלים את הקוד c שלנו לקובץ הרצה בשם "helloworld"
ועכשיו, תוכלו להריץ את קובץ ההרצה שלכם בצורה הבאה:

זוהי תוכנת ה"hello world" הראשונה שלכם בC, כל הכבוד!
מאחורי הקלעים¶
הקומפיילר הוא הכלי שיודע לקחת את קבצי הC שלנו, והוא יודע לקמפל אותם לשפת מכונה- נקרא גם קבצי אובייקט.
ולאחר שקומפיילר קימפל את כל הקבצי C שלנו לקבצי אובייקט, הוא מחבר את כל הקבצי אובייקט לתוך "קובץ הרצה" באמצעות הלינקר (קונספט דומה למה שראינו בMSDOS)
חשוב להבדיל שיש שוני בין קובץ אובייקט (קובץ של שפת מכונה) לבין קובץ הרצה (קובץ שלינוקס יודע להריץ).
כאשר מערכות הפעלה מריצה קובץ, יש פורמט מסוים שמערכת ההפעלה צריכה את הקובץ כדי שהיא תדע להריץ אותו.
קבצי הרצה בלינוקס הם קבצי "ELF", בהמשך נדבר יותר על הפורמט ולמה הוא חשוב ללינוקס.
אנחנו יכולים באמצעות GCC לקבל רק את הקובץ אובייקט (לא הקובץ הרצה) של תוכנה מסוימת (רק את השפת מכונה) באמצעות הדגל -c

כדי לראות את האסמבלי של האובייקט, ניתן להשתמש בפקודה "objdump" שיודעת לנתח בינארים.
באמצעות הדגל "-d" אנחנו יכולים לעשות "disassemble" (להפוך את השפת מכונה לאסמבלי) כדי לראות את הקוד אסמבלי שנוצר מקוד הC שלנו.

יש כמה דברים שעלינו לשים לב בתוצאה שקיבלנו.
- האסמבלי הזה הוא אסמבלי 64 ביט ולא 32 ביט כמו שלמדנו, (מעבד יותר מודרני)- וההבדל המהותי ביותר הוא בגודל האוגרים, שעכשיו הוא 64 ביט במקום 32 ביט. כל האוגרים שמתחילים בr, כמו rax הם הגרסה המוגדלת של האוגר (ה64 ביט).
- בתחביר הזה, סימן ה% נועד לסמן אוגרים.
- בתחביר הזה, בmov, האוגר/ערך השמאלי-> מועתק לאוגר/ערך הימני. (כלומר שמאל לימין)
- ניתן לראות בצד ימין את הקוד אסמבלי, ובצד שמאל את הקוד מכונה- ניתן לראות שלא כל ההוראות בשפת מכונה הן באותו הגודל. (זאת יחודיות של x86)
בואו נעבור שורה-שורה על הקוד האסמבלי שנוצר מהתוכנית הפשוטה ב־C:
קודם כל, שימו שבלינוקס כל תוכנה מתחילה בפונקציה בשם "main".
התוצאה של הקומפיילר היא פונקציית הmain המקומפלת
- הוראת endbr64 – הוראה חדשה יחסית למעבדי אינטל, שמטרתה להגן על קריאות לפונקציות מ־Indirect Branches. לא צריך להתעמק בה כרגע – אפשר לדלג.
-
אלה שתי השורות הקלאסיות שכל פונקציה ב־C מתחילה איתן.
-
אנחנו שומרים את כתובת הבסיס של המחסנית הקודמת (
push %rbp) - ואז מעדכנים את
rbpכך שיצביע למחסנית הנוכחית (mov %rsp, %rbp)
כך הקומפיילר יכול לנהל את משתני הפונקציה דרך כתובות יחסיות ל־
rbp. (כמו שלמדנו ב16 ביט)
-
זו פקודה שמטענה כתובת לתוך
rax. -
הפקודה הזו אומרת בעצם: קח את הכתובת של המחרוזת "Hello, world!" ושמור אותה ב־
rax. -
שימו לב: הכתובת לא מופיעה ישירות כאן – ככה זה בקבצי אובייקט, הכתובת של המחרוזת תטען רק אחרי שנריץ את התוכנה. (לינוקס עושה את זה כשהוא מריץ לנו את התוכנה)
-
כאן אנחנו מכינים את הפרמטר הראשון לפונקציה – בלינוקס, הפרמטר הראשון לפונקציות עובר דרך
rdi. -
כלומר:
rdi = כתובת המחרוזת
-
קריאה לפונקציה – ככל הנראה לפונקציה
putsאוprintf, שתדפיס את המחרוזת למסך. -
בגלל שהקישור (linking) עוד לא התרחש, הכתובת היא זמנית (placeholder) – הלינקר ימלא אותה אחר כך.
-
החזרת ערך
0מ־main– כלומרreturn 0;בקוד C. -
בלינוקס, הפונקציה
mainמחזירה ערך דרך האוגרeax. (ה32 ביט)
-
שורת סיום של הפונקציה:
-
מחזירה את
rbpהקודם (המסגרת הקודמת של המחסנית) -
מבצעת
ret– חזרה מהפונקציה
-