לדלג לתוכן

2.2 טבלת הGDT הרצאה

טבלת התיאורים הגלובלית – GDT

מבוא

במעבד עם מצבים שונים, כמו Real Mode ו- Protected Mode אנחנו צריכים להגדיר בדיוק, איזה קטעי קוד יש (איזה סגמנטים קיימים), ובאיזה הרשאות הם יכולים לרוץ.
כדי לפתור את הבעיה, קיים הGDT.
טבלת הGDT - ה global descriptior table היא טבלה שמכילה רשימה של כל הסגמנטים שקיימים, מה הגודל שלהם, מה הכתובת בסיס שלהם (כלומר איפה הם מתחילים ואיפה הם נגמרים בRAM), ההרשאה שלהם, והאם מותר לכתוב, לקרוא או להריץ מהם.
בטבלה הזו, אנחנו יכולים בעצם להגדיר סגמנטים חדשים למעבד, וממש להגדיר את גדולן, ואת ההרשאות שלהם.
כלומר, אם אני אגדיר סגמנט מסוים, במיקום, וגודל מסוים עם הרשאת Protected Mode- זה אומר שכל הקוד שמוגדר בחלק הזה של הזכרון, כאשר המעבד יריץ אותו- הוא יהיה רק קוד שרץ בהרשאות Protected Mode.
כל סגמנט כזה בטבלה, אנחנו קוראים לו Descriptor (מלשון description- תיאור)
ובכל Descriptor יש:

  • היכן הסגמנט מתחיל (Base)
  • מה אורכו (Limit)
  • לאיזו רמת Privilege הוא שייך (DPL) (כלומר Protected Mode או Real Mode)
  • האם מותר להריץ/לכתוב/לקרוא ממנו

ללא GDT אי‑אפשר להבדיל בין קוד מערכת לקוד משתמש (תוכנות רגילות), ולכן היא אבן‑יסוד בכל מערכת הפעלה מודרנית.

מבנה Descriptor

כל רשומה בטבלת ה‑GDT היא בדיוק 8  בייט:

שדה ביטים תפקיד עיקרי
Limit 15‑0 16 הגבלת אורך הסגמנט (עד 1 MB או 4 GB עם Granularity)
Base 15‑0 16 16 הסיביות הנמוכות של כתובת הבסיס
Base 23‑16 8 המשך כתובת בסיס
Type + S 5 קוד/נתונים/מערכת + דגל S (0=System, 1=Code/Data)
DPL 2 Descriptor Privilege Level (0–3)
P (Present) 1 1 = טעון בזיכרון
Limit 19‑16 4 ארבעת הסיביות הגבוהות של Limit
AVL, L, D/B, G 4 דגלי עזר:AVL – כללי למערכתL – 64‑bitD/B – גודל ברירת מחדלG – Granularity
זה לא כזה חשוב שתבינו את כל הביטים בכל עמודה בטבלה, רק שתכירו שיש פורמט מסוים שאיתו אנחנו עובדים בטלאות כאלו.
חשוב שתכירו את שדה הDPL בטבלה, הDescriptor Privilege Level הוא השדה שמציין לכל סגמנט באיזה הרשאה הוא רץ, (Real Mode או Protected Mode)- כאשר Real Mode הוא 0, ו- Protected Mode הוא 3.
יש גם מספרים בניהם, בדרך כלל לא משתמשים בהם- אבל כמו שיכלתם להניח, זה משהו בניהם.
המספרים האלו, נקראים גם ring-ים. (טבעות)

סלקטורים (Selectors)

אז Selector הוא בעצם מצביע לעמודה בטבלה.
כלומר, הselector היא "אינדקס" בטבלת הGDT, באמצעותו המעבד יודע לאיזה זכרון התוכנה צריכה לגשת, מה הגודל שלו ומה ההרשאות שלו.
אפשר להגיד שהselector היא כמו כתובת, והיא מקבילה ל"סגמנטים" (כמו ב8086 ה16 ביט)
הסלקטור היא ערך של 16 ביטים, כאשר היא בנויה כך:

15          3   2   0
+-----------+---+---+
| Index     |TI |RPL|
+-----------+---+---+

  • הIndex – הוא המצביע על שורה ב‑GDT
  • הTI (Table Indicator) – הוא מציין אם הטבלה היא LDT או GDT (נדבר בהמשך על ההבדלים)
  • הRPL (Requestor Privilage) – רמת ההרשאה שהמשתמש בסלקטור רוצה (כרגע זה לא רלוונטי, התעלמו מזה.)

מערכת הפעלה מודרנית

אז איך נממש את זה במערכת ההפעלה?
נחלק את מערכת ההפעלה לשתים.
הקרנל, והיוזר מוד.

הקרנל היא הקוד שרץ בהרשאות גבוהות, שאחראי לממש את הפסיקות בivt, הקוד שאחראי לבצע את כל פעולות הin ו- out כדי לדבר עם התקני התוכנה- ושמבצע את כל הפעולות שאפשר לעשות רק בReal Mode.

חשבו על הקרנל כהמנוע של מערכת ההפעלה- הקרנל אחראי לנהל את הקבצים מול הדיסק, אחראי לנהל את התוכנות השונות שרצות בRAM, ואת כל החומרה של המחשב.

היוזר מוד, הוא כל הקוד שרץ בהרשאות נמוכות, בדרך כלל תוכנות עוטפות שמשתמשות בממשק של הקרנל (דרך פסיקות- interrupt-ים) כדי להשתמש במנוע.
לדוגמה, הטרמינל- התוכנות שאנחנו כותבים, תוכנות שאנחנו מורידים, הUI של מערכת ההפעלה וכו.
Pasted image 20250622180006.png

כאשר קרנל בסיסי מתחיל לרוץ הוא מגדיר 4 Descriptors בסיסיים בGDT.

Index שם תיאורי Base Limit Type DPL
0 Null Descriptor 0 0
1 Kernel Code (Ring 0) 0 4 GB Code 0
2 Kernel Data (Ring 0) 0 4 GB Data 0
3 User Mode Code (Ring 3) 0 4GB Code 3
4 User Mode Data (Ring 3) 0 4GB Data 3
העמודה הראשונה בטבלה תמיד תהיה ריקה כך שתמיד הindex יתחיל ב1.
ב4 העמודות הבאות נגדיר את הסגמנטים שמסמנים את הcode segement, ו- data segement של הקרנל והיוזר מוד, כאשר אנחנו מציינים שהDPL של הקרנל הוא 0- (Real Mode) והDPL של היוזר מוד הוא 3 (Protected Mode).
כך אנחנו מגדירים את אזורי הזכרון של הקרנל והיוזר מוד.
### טעינת GDTR

רגיסטר הGDTR הוא רגיסטר מיוחד שמצביע על הטבלה ומציין את האורך שלה.
כך יראה הקוד אסמבלי של הגדרת הטבלה וטעינתה לריגסטר הGDTR

gdt_start:

gdt_null: 
    dq 0 ; Descriptor 0 - חובה שיהיה אפס

gdt_code: 
    dw 0xFFFF             ; Limit (15:0)
    dw 0x0000             ; Base (15:0)
    db 0x00               ; Base (23:16)
    db 10011010b          ; Access Byte: Code, Readable, Present, DPL=0
    db 11001111b          ; Flags: 4KB granularity, 32-bit segment
    db 0x00               ; Base (31:24)

gdt_data: 
    dw 0xFFFF             ; Limit (15:0)
    dw 0x0000             ; Base (15:0)
    db 0x00               ; Base (23:16)
    db 10010010b          ; Access Byte: Data, Writable, Present, DPL=0
    db 11001111b          ; Flags
    db 0x00               ; Base (31:24)

gdt_user_code: 
    dw 0xFFFF             ; Limit (15:0)
    dw 0x0000             ; Base (15:0)
    db 0x00               ; Base (23:16)
    db 11111010b          ; Access Byte: Code, Readable, Present, DPL=3
    db 11001111b          ; Flags
    db 0x00               ; Base (31:24)

gdt_user_data: 
    dw 0xFFFF             ; Limit (15:0)
    dw 0x0000             ; Base (15:0)
    db 0x00               ; Base (23:16)
    db 11110010b          ; Access Byte: Data, Writable, Present, DPL=3
    db 11001111b          ; Flags
    db 0x00               ; Base (31:24)

gdt_end:

gdt_ptr:
    dw gdt_end - gdt_start - 1      ; Limit
    dd gdt_start                    ; Base

הכתובת gdt_ptr מחזיקה את גודל הטבלה (בגודל של word) וגם את הכתובת של תחילת הטבלה (בגודל של double word) - מזכיר כתובות באסמבלי 32 ביט הם 32 ביט.

לאחר שהגדרנו את gdt_ptr, נוכל לטעון אותו לרגיסטר הGDTR באמצעות פעולת ה"lgdt"
רק לפני שנבצע את הפעולה- שימו לב: אנחנו צריכים לוודא שלא יקרה שום פסיקה באמצע בטעות, כי זה עלול לגרום לבעיות, אז נשתמש בפקודה "cli" שמבטלת את הפסיקות-

cli                                 ; לא רוצים פסיקות באמצע הטעינה
lgdt [gdt_ptr]                      ; טוען ל-GDTR

לאחר שסיימנו, נוכל להחזיר את האופציה למעבד לקבל פסיקות באמצעות "sti"
sti                                 ; מחזיר את הפסיקות לפעולה

מעולה! טענו למעבד שלנו את טבלת הgdt! עכשיו המעבד ידע איזה הרשאות להביא כתלות בסלקטור.

עכשיו, כדי לעבור לprotected mode, עלינו לשנות את הביט הראשון של הרגיסטר cr0, שאחראי לציין אם הprotected mode דלוק.
אפשר לבצע זאת באמצעות פעולת logical or פשוטה.

mov eax, cr0
or eax, 1
mov cr0, eax

עכשיו כל מה שנותר לנו זה לקפוץ לסגמנט הקוד של הuser mode שהגדרנו.
קודם כל, עליינו לחשב את הסלקטור המתאים.
אני מזכיר שככה סלקטור בנוי:

15          3   2   0
+-----------+---+---+
| Index     |TI |RPL|
+-----------+---+---+

נקח את הindex, שהוא 4 בטבלה שהגדרנו.
נעשה לו shift left 3 פעמים, ונוסיף את הRPL בהתחלה (שהוא 3, כי אנחנו רוצים להגיע לring 3 שהוא הProtected Mode)
וכמובן הTI יהיה 0, כי אנחנו מדברים על טבלת GDT ולא טבלת LDT (נדבר בהמשך על ההבדלים)
כך שיוצא לנו
0x04 << 3 | 0x03

שבחישוב פייתון זריז, יוצא 35 בדצימלית.
Pasted image 20250622183940.png
נמיר להקסה, כי כתובות מייצגים בהקסה.
Pasted image 20250622184017.png
נהדר! נבצע far jmp לסלקטור 0x23, (הסגמנט) ועם הoffset של label שנגדיר. (למשל protected_mode_entry)

jmp 0x23:pm_entry   ; 0x08 = (Index 4 << 3) | RPL 0
proteced_mode_entry:
    mov ax, 0x10    ; Data selector (Index 2)
    mov ds, ax
    mov es, ax
    mov ss, ax

וזהו! אנחנו בProtected Mode, עכשיו:
- לא נוכל לבצע פעולות in, out
- לא לממש interrupt-ים משלנו
- לא לגשת לכל זכרון שנרצה- למשל לא נוכל לגשת לסקלטורים של הקרנל שהגדרנו קודם.
אבל מה מבטיח לנו שלא נוכל לגשת לסלקטור של הקרנל?
המעבד לא יתן לנו, כאשר נבצע למשל far jmp לסלקטור של קרנל, הוא יבדוק בטבלה את הDPL של הסלקטור, וכמובן יראה שהוא 0, לעומת הCPL שלנו (current privilege) שהוא 3. וזה לא יעבוד.

אז זהו! הקוד שלנו תקוע בprotected mode ועכשיו המעבד בשום מצב לא יכול לבצע שום פעולה עם הרשאות גבוהות!

לעומת זאת, נוכל לגשת למשל לdata segement של הusermode שהגדרנו, כי הCPL שלנו תואם את הDPL בטבלה.

נחשב את הסלקטור

0x05 << 3 | 0x03

Pasted image 20250622184802.png
נטען את הסלקטור לרגיסטר הdata segement (הds)
mov ax, 0x2b    ; Data selector (Index 5)
mov ds, ax

כמו שציינתי, אוגרי הסגמנטים לא גדלו, נשארו 16 ביט- כי מטרת האוגרים עכשיו היא לשמור סלקטורים- לא סגמנטים.

באמצעות סלקטורים אנחנו יכולים להחליף סגמנטים, הסלקטורים מאפשרים לנו לחלק את הזכרון לחלקים, כאשר כל חלק יכול להיות באיזה גודל שנבחר, מאיזה כתובת שנרצה (בכל ה32 ביט) ויכול להיות לה איזה הרשאה שנרצה.
למעשה, בגלל שרגיסטרים עכשיו בגודל של 32 ביט, כמו הגודל שהזכרון- אנחנו לא חייבים להשתמש בכלל ברגיסטרי הסגמנטים.
כלומר, אין שום התחייבות להשתמש בCS, DS, או SS בכלל. כי אנחנו יכולים לגשת לזכרון ישירות באמצעות [].

הנה דוגמה לשימוש במשתנה בכתובת 32 ביט בלי הds

mov eax, [0x004100AB]

דוגמה לקפיצה לכתובת מסוימת בכתובת 32 ביט בלי הcs

jmp [0x004100AA]

כלומר, אפשר לשכוח מהקונספט של far jmp.