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 ביטים, כאשר היא בנויה כך:
- הIndex – הוא המצביע על שורה ב‑GDT
- הTI (Table Indicator) – הוא מציין אם הטבלה היא LDT או GDT (נדבר בהמשך על ההבדלים)
- הRPL (Requestor Privilage) – רמת ההרשאה שהמשתמש בסלקטור רוצה (כרגע זה לא רלוונטי, התעלמו מזה.)
מערכת הפעלה מודרנית¶
אז איך נממש את זה במערכת ההפעלה?
נחלק את מערכת ההפעלה לשתים.
הקרנל, והיוזר מוד.
הקרנל היא הקוד שרץ בהרשאות גבוהות, שאחראי לממש את הפסיקות בivt, הקוד שאחראי לבצע את כל פעולות הin ו- out כדי לדבר עם התקני התוכנה- ושמבצע את כל הפעולות שאפשר לעשות רק בReal Mode.
חשבו על הקרנל כהמנוע של מערכת ההפעלה- הקרנל אחראי לנהל את הקבצים מול הדיסק, אחראי לנהל את התוכנות השונות שרצות בRAM, ואת כל החומרה של המחשב.
היוזר מוד, הוא כל הקוד שרץ בהרשאות נמוכות, בדרך כלל תוכנות עוטפות שמשתמשות בממשק של הקרנל (דרך פסיקות- interrupt-ים) כדי להשתמש במנוע.
לדוגמה, הטרמינל- התוכנות שאנחנו כותבים, תוכנות שאנחנו מורידים, הUI של מערכת ההפעלה וכו.

כאשר קרנל בסיסי מתחיל לרוץ הוא מגדיר 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" שמבטלת את הפסיקות-
לאחר שסיימנו, נוכל להחזיר את האופציה למעבד לקבל פסיקות באמצעות "sti"
מעולה! טענו למעבד שלנו את טבלת הgdt! עכשיו המעבד ידע איזה הרשאות להביא כתלות בסלקטור.
עכשיו, כדי לעבור לprotected mode, עלינו לשנות את הביט הראשון של הרגיסטר cr0, שאחראי לציין אם הprotected mode דלוק.
אפשר לבצע זאת באמצעות פעולת logical or פשוטה.
עכשיו כל מה שנותר לנו זה לקפוץ לסגמנט הקוד של הuser mode שהגדרנו.
קודם כל, עליינו לחשב את הסלקטור המתאים.
אני מזכיר שככה סלקטור בנוי:
נקח את הindex, שהוא 4 בטבלה שהגדרנו.
נעשה לו shift left 3 פעמים, ונוסיף את הRPL בהתחלה (שהוא 3, כי אנחנו רוצים להגיע לring 3 שהוא הProtected Mode)
וכמובן הTI יהיה 0, כי אנחנו מדברים על טבלת GDT ולא טבלת LDT (נדבר בהמשך על ההבדלים)
כך שיוצא לנו
שבחישוב פייתון זריז, יוצא 35 בדצימלית.

נמיר להקסה, כי כתובות מייצגים בהקסה.

נהדר! נבצע 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 בטבלה.
נחשב את הסלקטור

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