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 kod0x92→ kernel veri0xFA→ user kod0xF2→ 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 0x282. 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();
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