לדלג לתוכן

5.1 הקדמה הרצאה

הקדמה

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

הsyscall-ים

כמו שכבר דיברנו הרבה, התקשורת היחידה של תכונות יוזרמודית עם מערכת ההפעלה היא באמצעות interrupt-ים.
כל מערכת ההפעלה מודרנית מגדירה inerrrupt מסוים שבאמצעותו אפשר לבצע "קריאות מערכת" (תרגום של syscall-ים).
ברוב מערכות ההפעלה זה int 0x80, באמצעותו אפשר לגשת לכל ה"קריאות מערכת" המוגדרות במערכת ההפעלה.
בדרך כלל אוגר הrax נועד לציין איזה syscall אנחנו קוראים לקרוא, (איזה קריאת מערכת).

למשל

mov rax, 1
int 0x80

יבצע את syscall מספר אחת בטבלה
mov rax, 2
int 0x80

יבצע את syscall מספר שתים בטבלה
וכך הלאה.

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

טבלת syscall ו־sysenter

בעבר, רוב ההפצות השתמשו ב־int 0x80 כדי לבצע קריאות מערכת. הפקודה הזו יוצרת מעבר מפוקח מה־User Mode אל ה־Kernel Mode, אבל מדובר במנגנון איטי יחסית.

מעבדים מודרניים מספקים מנגנון מהיר יותר – הפקודה syscall (במעבדי AMD) או sysenter (במעבדי Intel). אלו הן הוראות חומרה שנועדו לבצע קריאות מערכת בצורה מהירה ויעילה יותר, מבלי להשתמש בפסיקות תוכנה.

מתחת לפני השטח, גם int 0x80 וגם syscall/sysenter משתמשים בטבלת syscall table – זוהי טבלה פנימית של הקרנל, שבה כל מספר מצביע לפונקציה מסוימת שמטפלת בבקשה. למשל, קריאה מספר 1 מפנה ל־sys_write, קריאה מספר 60 ל־sys_exit, וכן הלאה. (למשל שני syscall-ים בלינוקס)

ה־libc והקרנל דואגים לממש את הקריאות האלה בהתאם לטבלת הסיסטם־קולים. כשאנחנו כותבים ב־C את write(...), בעצם מתבצע מאחורי הקלעים מעבר ל־syscall דרך אחת מהשיטות האלה.

החיבור של libc

אז, המון מהפונקציות שהתמשנו בlibc, מאחורי הקלעים קראו לsyscall-ים.
חשבו על הפונקציות שלמדנו, ועל איזה פעולות רק הקרנל יכול לעשות (פעולות שקשורות בחומרה).
למשל, כל הפעולות של ההתעסקות עם קבצים כמו open, write, read הן פעולות בlibc שמאחורי הקלעים קוראות לsyscall-ים!
לעומת זאת, פעולות כמו memset, strcmp הן פעולות שלא קוראות לשום syscall, כי הן מבצעות פעולות שאפשר יוזרמוד כדי לבצע, ולא צריך שום התעסקות קרנלית.

הכלי strace

למעשה, כמו שיש לנו את ltrace כדי לקבל את כל הקריאות libc של תוכנה מסוימת, יש לנו את הכלי strace שעוזר לנו לקבל את כל הקריאות syscall של תוכנה מסוימת.
התקינו strace באמצעות:

sudo apt install strace -y

והריצו תוכנה שכתבתם עם strace
strace ./my_program

אתם תקבלו רשימה של המון syscall-ים, אל תדאגו- בהמשך הפרק נסביר את כולם.

קריאת syscall-ים בC

יפה מאוד שמשתמשים בsyscenter כדי לקרוא לsyscall באסמבלי, אבל כיצד אפשר לקרוא לsyscall בC?
ניתן לעשות זאת כך: הנה קוד בשפת C שמשתמש בקריאות מערכת (syscall) ישירות – בלי להשתמש ב־printf, ובלי לעבור דרך הפונקציות של libc. הקוד כותב מחרוזת למסך בעזרת write דרך syscall.

#define _GNU_SOURCE
#include <unistd.h>
#include <sys/syscall.h>

int main() {
    const char *msg = "hello!\n";
    syscall(SYS_write, 1, msg, 20);
    return 0;
}

- הפונקציה syscall היא פונקציה שמאפשרת לקרוא קריאות מערכת ישירות.
- כאשר SYS_write הוא הsyscall number של write.
- 1 זה מספר הקובץ של stdout (המסך).
- הפוינטר msg היא כתובת המחרוזת שרוצים להדפיס.
- 20 הוא מספר הבתים להדפסה (כולל תו השורה החדשה \n).

למעשה, אנחנו לא נשתמש בפונקציית הsyscall כדי לקרוא לsyscall-ים כי זה לא נוח.
במקום זאת, נשתמש בממשק הlibc של הsyscall-ים.
כלומר, במקום לקרוא לwrite כך:

const char *msg = "hello!\n";
syscall(SYS_write, 1, msg, 20);

נקרא לו עם הממשק של libc בשביל נוחות.
const char *msg = "hello!\n";
write(1, msg, 20);

בפרק זה נלמד על עוד המון syscall-ים שלינוקס נותנים לנו.