לדלג לתוכן

2.3 - יציאה מProteced Mode - הרצאה

יציאה מ-Protected Mode

ה-IDT - Interrupt Descriptor Table

ב-Real Mode (16 ביט), השתמשנו ב-IVT - טבלת interrupts בגודל קבוע של 1KB, שמכילה 256 רשומות של 4 בתים כל אחת (offset + segment). כל interrupt הייתה בעצם קפיצה אוטומטית לכתובת מסוימת בזיכרון.

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

כדי לפתור את זה - בדיוק כמו שהיה לנו GDT לסגמנטים, יש לנו IDT - טבלת תיאורים של Interruptים.

מבנה ה-IDT

הIDT היא קיצור של Interrupt Descriptor Table, והיא הivt של מעבדים חדשים.
כל שורה ב-IDT נקראת Gate, והיא בעצם "תיאור של קפיצה לקוד של interrupt". הטבלה מכילה 256 רשומות (כמו IVT), אבל כל רשומה הרבה יותר מתוחכמת. היא מכילה:

  • Offset (כתובת ב-32 ביט)
  • Selector (מאיזה סגמנט לבצע את הinterrupt - קוד קרנל למשל)
  • Type (איזה סוג interrupt: Interrupt, Trap, Task)
  • DPL (הרשאה שהקורא צריך כדי לקרוא לinterrupt הזו)
  • Present Bit

למעשה, המעבד מקבל את ה-IDT דרך רגיסטר בשם IDTR - בדיוק כמו שיש GDTR.
גם כאן נגדיר idt_ptr שמכיל את הגודל והמיקום של הטבלה, ואז נטען אותו ל-IDTR:
בדיוק כמו שעשינו עם lgdt בשיעור הקודם.

lidt [idt_ptr] ; loads the interrupt table

הInterrupt Gate - מה הוא עושה בפועל?

כאשר מתרחשת interrupt (למשל int 0x80) המעבד עובר על השלבים הבאים:

  1. ניגש לרשומת ה-Gate המתאימה ב-IDT (למשל רשומה מספר 0x80).
  2. בודק הרשאה מול ה-Gate: המעבד משווה את ה-CPL - רמת ההרשאה שבה הקוד הקורא רץ כרגע - מול ה-DPL שרשום ב-Gate עצמו. כדי שהקריאה תתקבל צריך ש-CPL <= DPL של ה-Gate (זכרו: מספר גבוה = הרשאה נמוכה, כך ש-Ring 3 הוא הנמוך ביותר). אם ההרשאה לא מספיקה, המעבד לא מתעלם מהקריאה בשקט - הוא זורק חריגה (#GP - General Protection Fault). זו בדיוק הסיבה שאת ה-Gate של int 0x80 מגדירים עם DPL=3: כך גם קוד שרץ ב-Ring 3 (יוזר מוד) מורשה להפעיל אותו.
  3. אם ההרשאה תקינה, המעבד טוען את ה-Selector מה-Gate - הוא מצביע לסגמנט של קוד הקרנל (ב-Ring 0) - ומוודא שהוא חוקי מול ה-GDT, ואז קופץ ל-Offset שרשום ב-Gate בתוך אותו סגמנט.

מכיוון שעברנו מהרשאה נמוכה (Ring 3) לגבוהה (Ring 0), המעבד גם:

  • שומר אוטומטית את הרגיסטרים החשובים (EIP, אוגרי סגמנט, EFLAGS, ESP)
  • מחליף את ה-SS ו-ESP למחסנית של הקרנל (יש לה מחסנית משלה, שונה מהיוזר מוד)
  • קופץ לפונקציית הinterrupt (שבקרנל)

ההבדל בין CPL ל-DPL:
ה-CPL (Current Privilege Level) הוא רמת ההרשאה שבה הקוד רץ כרגע (0 לקרנל, 3 ליוזר מוד) - זהו הערך ה"חי". ה-DPL (Descriptor Privilege Level) הוא רמת ההרשאה שכתובה מראש בתיאור - ב-Gate שב-IDT, או בסגמנט שב-GDT.
הבדיקה בשלב 2 היא בעצם "האם לקוד שרץ עכשיו (CPL) מותר להשתמש ב-Gate הזה (DPL)". לכן קודם בודקים הרשאה, ורק אם היא תקינה טוענים את ה-Selector וקופצים - טעינת ה-Selector בשלב 3 היא מה שקורה אחרי שההרשאה אושרה, לא דילוג עליה.

והכי חשוב - המעבר הזה מאובטח!
הדרך היחידה להריץ קוד קרנל (קוד שרץ ב-Ring 0) כשאנחנו ביוזר מוד היא רק באמצעות קריאה לinterrupts מוגדרות מראש!
כלומר, כשאנחנו פותחים את המחשב- הקרנל מגדיר את כל הinterrupts שהיוזר מוד יוכל לעשות בIDT, ואז הקרנל קופץ ליוזר מוד.
כך, שהיוזר מוד- יוכל לקרוא רק לinterrupts מוגדרות שהקרנל הגדיר לו שהוא יכול לעשות, ובכך אנחנו יכולים לוודא שביוזר מוד תוכנות ירצו בהרשאות נמוכות, אבל יוכלו לבצע פעולות שדורשות הרשאות גבוהות, כמו כתיבת לקובץ בדיסק.

כך אנחנו מאפשרים לקוד Protected Mode להריץ קוד קרנלי

נניח שיש לנו קוד שרץ ביוזר מוד (Ring 3) - תוכנה כלשהי, או אפילו תוכנית ב-Terminal. הקוד הזה רוצה למשל לקרוא לקובץ, או לצייר על המסך.
האם ניתן לו ישירות גישה לדיסק או למסך? ברור שלא - זה תפקידו של הקרנל.

אז איך כן ניגשים?

בדיוק כמו ש-DOS עבדה עם interrupts (כמו int 21h), גם כאן אנחנו נשתמש ב-interrupts מותאמות אישית.

לדוגמה:

int 0x80

הקרנל שלנו יגדיר ב-IDT את רשומה מספר 0x80 כך שהיא תצביע לפונקציה שהוא הגדיר, נגיד kernel_dispatcher, שרצה ב-Ring 0.
כאשר ה-INT מתבצע, המעבד אוטומטית עובר ל-Ring 0, טוען את ה-CS ו-EIP של הקרנל, ומריץ את הקוד.

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

דוגמה לקוד

קרנל:

; kernel - defining an interrupt gate
idt_80:
    dw kernel_dispatcher        ; Offset 0:15
    dw 0x08                     ; Selector (Kernel Code)
    db 0                        ; reserved
    db 10001110b                ; Present, DPL=3, Interrupt Gate
    dw kernel_dispatcher >> 16  ; Offset 16:31

משתמש (User Mode):

mov eax, 4         ; operation number (e.g. print to screen)
mov ebx, msg
int 0x80           ; call to the kernel

קרנל:

kernel_dispatcher:
    cmp eax, 4
    je print_text
    ...
iret ; returns control to user mode

תרשים המראה את המסלול של הפקודה int 0x80 מקוד היוזר מוד אל השער בטבלת ה-IDT, משם אל קוד הקרנל שרץ ב-Ring 0, וחזרה לתוכנה באמצעות iret

סיכום

  • ה-IDT הוא טבלת הinterrupts ב-Protected Mode
  • כל שורת Gate מגדירה מעבר מאובטח לקוד קרנל
  • המעבר נעשה ע"י int - המעבד מבצע את כל ה-context switch (חילופי האוגרים) לבד (ממש חומרתית)
  • זו הדרך שבה תוכנות ביוזר מוד "מריצות" קוד קרנל - דרך השערים האלו שמוגדרים בקרנל
  • זה לב-ליבה של כל מערכת הפעלה מודרנית