Tuna Kılıç
All posts

Narc Devlog: GDT nasıl yazılır?

Narc'ta Limine'nin GDT'sini bırakıp kendi GDT'mizi kuruyoruz.

November 2026PublishedOSDev · Narc
İçindekiler

Selam ben Tuna, heap'ten sonra hedefim userspace yani ring 3'te bi program çalıştırıp ekrana "Hello from Userspace!" yazdırmak. ama ondan önce iki şey lazım GDT ve TSS. bu yazı GDT

Kısa bi yazı olucak çünkü 64 bitte GDT neredeyse hiçbişey yapmıyo ama yazmazsan userspace'e geçemiyosun

Ders 1: GDT nedir

32 bit zamanında segmentation diye bi olay vardı bellek segmentlere bölünüyodu her segmentin başlangıç adresi boyutu ve izinleri vardı. GDT (Global Descriptor Table) bu segmentlerin listesi

64 bitte segmentation neredeyse tamamen kapalı. başlangıç adresi ve boyut görmezden geliniyo. peki niye hala lazım?

  • CPU şu an kernel'de mi (ring 0) user'da mı (ring 3) çalıştığını CS register'ındaki segment'ten anlıyo
  • TSS'yi (sonraki yazı) CPU'ya GDT üzerinden veriyoruz

Yani bize sadece 4 tane segment lazım: kernel kod, kernel veri, user kod, user veri

Şu an kimin GDT'si kullanılıyo

Limine'nin. Limine kernel'i başlatmadan önce kendi GDT'sini yüklüyo ama onda user segmenti yok o yüzden kendimizinkini yazmamız lazım

Ders 2: GDT entry'si

Her entry 8 byte. 32 bitten kalma olduğu için içi çok saçma dağılmış:

struct gdt_entry {
    uint16_t limit_low;
    uint16_t base_low;
    uint8_t base_mid;
    uint8_t access;
    uint8_t granularity;
    uint8_t base_high;
} __attribute__((packed));

base 3 parçaya limit 2 parçaya bölünmüş neden bilmiyorum intel'e sorun. packed koymazsak derleyici araya boşluk koyabiliyo ve CPU yanlış okur

base hep 0 limit hep max (0xFFFFF) zaten 64 bitte bakılmıyo. bizim için önemli olan iki şey access ve granularity

1. Access byte

bit 7    present, 1 olmalı
bit 6-5  ring (DPL) 0 = kernel 3 = user
bit 4    1 = kod ya da veri segmenti
bit 3    1 = kod 0 = veri
bit 1    kodsa okunabilir verideyse yazılabilir

bundan 4 tane değer çıkıyo:

  • 0x9A → kernel kod
  • 0x92 → kernel veri
  • 0xFA → user kod
  • 0xF2 → user veri

9 ile F arasındaki fark sadece ring bitleri. 9 = 1001 F = 1111 yani ring 0 → ring 3

2. Granularity

granularity byte'ının üst 4 biti flag alt 4 biti limit'in üst kısmı. flag'ler:

bit 7  G   limit 4 KiB birimlerinde
bit 6  DB  32 bit segment
bit 5  L   64 bit kod segmenti
  • kod segmentleri için 0xA0 (G + L) yani 64 bit kod
  • veri segmentleri için 0xC0 (G + DB)

kod segmentinde L ve DB aynı anda 1 olmamalı yoksa CPU kızıyo

Ders 3: Kod

1. Entry yazan fonksiyon

static struct gdt_entry gdt[7];
static struct gdt_ptr gp;

static void gdt_set_entry(int i, uint32_t base, uint32_t limit, uint8_t access, uint8_t gran) {
    gdt[i].limit_low = limit & 0xFFFF;
    gdt[i].base_low = base & 0xFFFF;
    gdt[i].base_mid = (base >> 16) & 0xFF;
    gdt[i].access = access;
    gdt[i].granularity = ((limit >> 16) & 0x0F) | (gran & 0xF0);
    gdt[i].base_high = (base >> 24) & 0xFF;
}

base'i ve limit'i parça parça yerlere dağıtıyo o kadar

7 entry var çünkü:

0  null (CPU ilk entry'nin boş olmasını istiyo)
1  kernel kod   → 0x08
2  kernel veri  → 0x10
3  user veri    → 0x18
4  user kod     → 0x20
5  TSS          → 0x28
6  TSS (TSS 16 byte iki entry kaplıyo)

her entry 8 byte olduğu için selector'lar 8'er 8'er gidiyo. user veriyi user koddan önce koydum çünkü ilerde syscall/sysret kullanırsam CPU bu sırayı bekliyomuş. TSS'yi sonraki yazıda dolduruyoruz

gdt.h'a selector'ları ekledim:

#define KERNEL_CODE_SEG 0x08
#define KERNEL_DATA_SEG 0x10
#define USER_DATA_SEG   0x18
#define USER_CODE_SEG   0x20
#define TSS_SEG         0x28

2. GDT'yi yüklemek

CPU'ya GDT'nin nerde olduğunu lgdt ile söylüyoruz. ona da adres ve boyut veren küçük bi struct lazım:

struct gdt_ptr {
    uint16_t limit;
    uint64_t base;
} __attribute__((packed));
void gdt_init(void) {
    gdt_set_entry(0, 0, 0, 0, 0);
    gdt_set_entry(1, 0, 0xFFFFF, 0x9A, 0xA0);
    gdt_set_entry(2, 0, 0xFFFFF, 0x92, 0xC0);
    gdt_set_entry(3, 0, 0xFFFFF, 0xF2, 0xC0);
    gdt_set_entry(4, 0, 0xFFFFF, 0xFA, 0xA0);

    gp.limit = sizeof(gdt) - 1;
    gp.base = (uint64_t)&gdt;

    __asm__ volatile ("lgdt %0" : : "m"(gp));

    __asm__ volatile (
        "pushq $0x08\n"
        "leaq 1f(%%rip), %%rax\n"
        "pushq %%rax\n"
        "lretq\n"
        "1:\n"
        "movw $0x10, %%ax\n"
        "movw %%ax, %%ds\n"
        "movw %%ax, %%es\n"
        "movw %%ax, %%ss\n"
        "movw $0, %%ax\n"
        "movw %%ax, %%fs\n"
        "movw %%ax, %%gs\n"
        : : : "rax", "memory"
    );

    kprint("GDT initialized!\n");
}

lgdt yaptık ama iş bitmedi. CPU segment register'larında hala Limine'nin selector'larını tutuyo. onları da değiştirmemiz lazım

veri segmentleri kolay mov ile 0x10 yazıyoruz. fs ve gs'yi 0 yaptım şimdilik kullanmıyoruz

CS ise mov ile değiştirilemiyo. bunun için bi hile var: stack'e önce yeni CS (0x08) sonra dönülecek adresi koyup lretq yapıyoruz. lretq normalde "uzak bi fonksiyondan dön" demek, stack'ten adresi ve CS'yi alıp oraya zıplıyo. biz de ona 1: etiketinin adresini veriyoruz yani aslında hiçbi yere gitmiyoruz bi sonraki satıra geçiyoruz ama CS değişmiş oluyo :D

3. Nerde çağırıyoruz

ÖNEMLİ: idt_init'ten önce çağırmamız lazım. çünkü idt.c'de gate'leri kurarken CS'de o an ne varsa onu yazıyoruz:

uint16_t cs;
__asm__ volatile ("mov %%cs, %0" : "=r"(cs));

GDT'yi IDT'den sonra yüklersek IDT'deki gate'ler Limine'nin eski selector'ını gösterir ve ilk interrupt'ta her şey patlar

kernel.c'de idt_init'in hemen üstüne koydum, öncesi ve sonrası CS'yi de basıyorum:

uint16_t old_cs;
__asm__ volatile ("mov %%cs, %0" : "=r"(old_cs));

gdt_init();

uint16_t new_cs;
__asm__ volatile ("mov %%cs, %0" : "=r"(new_cs));
kprint("CS before: ");
kprint_hex(old_cs);
kprint(" CS after: ");
kprint_hex(new_cs);
kprint("\n");

idt_init();

alt text

GDT YÜKLENDİİİİ AHAHAHAHA CS 0x28'den 0x08'e geçti

Limine'ninki neden 0x28? Limine'nin GDT'sinde önce 16 bit ve 32 bit segmentler var 64 bit kod segmenti 5. sırada

altındaki INT3 testi de çalıştı yani interrupt'lar yeni CS ile sorunsuz çalışıyo (bu ss için geçici olarak orda khalt() koydum yoksa ekran kayıyo)

Sonraki yazıda TSS var GDT'deki 5 ve 6. entry'leri orda dolduracağız