Selam ben Tuna, GDT yazısında TSS için iki boş yer bırakmıştık bu yazıda onu dolduruyoruz
Ders 1: TSS nedir
TSS (Task State Segment) 32 bitte donanımla task değiştirmek için kullanılıyodu. 64 bitte o kısım tamamen kalktı geriye sadece stack adresleri kaldı:
struct tss {
uint32_t reserved0;
uint64_t rsp0;
uint64_t rsp1;
uint64_t rsp2;
uint64_t reserved1;
uint64_t ist1;
uint64_t ist2;
uint64_t ist3;
uint64_t ist4;
uint64_t ist5;
uint64_t ist6;
uint64_t ist7;
uint64_t reserved2;
uint16_t reserved3;
uint16_t iopb;
} __attribute__((packed));bize lazım olanlar:
- rsp0: program ring 3'teyken interrupt gelirse CPU kernel'e geçerken bu stack'i kullanıyo. user'ın stack'ini kullanamaz çünkü user oraya ne koyduğunu bilmiyoruz. userspace yazısında bu çok önemli olucak
- ist1-ist7: Interrupt Stack Table. IDT'de bi interrupt'a "sen ist1'i kullan" dersen o interrupt ne zaman gelirse gelsin CPU direkt o stack'e geçiyo
- iopb: I/O izin bitmap'i, kullanmıyoruz. TSS'nin boyutunu verince "bitmap yok" demek oluyo
rsp1 rsp2 ring 1 ve 2 için kimse kullanmıyo
Ders 2: Neden ist lazım: Double Fault
Diyelim kernel'in stack'i bozuldu (taştı ya da rsp saçma bi yere gitti). bi şeyi stack'e push etmeye çalışınca page fault oluyo. ama page fault handler'ı çağırmak için CPU'nun interrupt frame'ini stack'e push etmesi lazım. hangi stack'e? aynı bozuk stack'e. yine page fault
exception'ı teslim ederken bi exception daha olunca CPU double fault atıyo. double fault için de frame push etmesi lazım, aynı bozuk stack, yine patlıyo. üçüncüsünde CPU pes ediyo ve triple fault = makine resetleniyo. ekranda hiçbişey göremiyosun
Bunu gerçekten görmek için önce double fault handler'ı yazdım ama ist vermeden test ettim. testi şöyle yaptım:
kprint("Breaking the stack...\n");
__asm__ volatile ("mov $0xdead0000, %rsp; push %rax");rsp'yi map edilmemiş saçma bi adrese koyup push yapıyoruz. QEMU direkt resetlendi. ne olduğunu görmek için -d int,cpu_reset -no-reboot ile açtım (her exception'ı logluyo):
check_exception old: 0xffffffff new 0xe
1: v=0e e=0002 i=0 cpl=0 IP=0008:ffffffff80002092 pc=ffffffff80002092 SP=0010:00000000dead0000 CR2=00000000deacfff8
check_exception old: 0xe new 0xe
2: v=08 e=0000 i=0 cpl=0 IP=0008:ffffffff80002092 pc=ffffffff80002092 SP=0010:00000000dead0000 env->regs[R_EAX]=0000000000000000
check_exception old: 0x8 new 0xe
Triple fault
tam anlattığım gibi: page fault (0xe) CR2 = 0xdeacfff8 yani push'un yazmaya çalıştığı yer, sonra yine page fault, sonra double fault (0x8), sonra yine page fault ve Triple fault
ist ile double fault handler'a ayrı sağlam bi stack verirsek en azından ne olduğunu ekrana yazabiliriz
Ders 3: Kod
1. Stack'ler
Şimdilik basit tuttum iki tane statik dizi:
static struct tss tss;
static uint8_t kernel_stack[16384] __attribute__((aligned(16)));
static uint8_t double_fault_stack[16384] __attribute__((aligned(16)));16 KiB'lık iki stack. biri rsp0 için biri double fault için. 16'ya hizalı çünkü x86_64'te stack 16'ya hizalı olmalı
2. tss_init
void tss_init(void) {
tss.rsp0 = (uint64_t)kernel_stack + sizeof(kernel_stack);
tss.ist1 = (uint64_t)double_fault_stack + sizeof(double_fault_stack);
tss.iopb = sizeof(tss);
gdt_set_tss((uint64_t)&tss, sizeof(tss) - 1);
__asm__ volatile ("ltr %0" : : "r"((uint16_t)TSS_SEG));
kprint("TSS initialized!\n");
}stack aşağı doğru büyüdüğü için (push yapınca rsp azalıyo) dizinin başını değil sonunu veriyoruz
ltr (load task register) CPU'ya "TSS'n GDT'de 0x28'de" diyo
3. GDT'ye TSS koymak
TSS descriptor normal entry'lerden farklı 16 byte. çünkü TSS'nin adresi 64 bit ve normal entry'de base için sadece 32 bit yer var. o yüzden iki entry kaplıyo, gdt.c'ye:
void gdt_set_tss(uint64_t base, uint32_t limit) {
gdt_set_entry(5, (uint32_t)base, limit, 0x89, 0x00);
uint32_t *high = (uint32_t *)&gdt[6];
high[0] = base >> 32;
high[1] = 0;
}- entry normal bi entry gibi: base'in alt 32 biti, limit, access
0x89(present + "64 bit TSS" tipi)
- entry normal bi entry gibi: base'in alt 32 biti, limit, access
- entry'nin ilk 4 byte'ı base'in üst 32 biti, son 4 byte'ı 0
- entry'yi struct gibi değil iki tane uint32 gibi yazdım çünkü içindeki alanların hiçbir anlamı yok
4. Double fault handler
idt.c'ye:
__attribute__((interrupt))
static void double_fault_handler(
interrupt_frame_t *frame,
uint64_t error_code
) {
(void)error_code;
uint64_t rsp;
__asm__ volatile ("mov %%rsp, %0" : "=r"(rsp));
kprint("\nDouble Fault!\n");
kprint(" rip: ");
kprint_hex(frame->rip);
kprint("\n rsp when it happened: ");
kprint_hex(frame->rsp);
kprint("\n rsp in handler: ");
kprint_hex(rsp);
kprint("\n");
for (;;) {
__asm__ volatile ("cli; hlt");
}
}double fault'tan geri dönülmez o yüzden durduruyoruz. hata olduğu andaki rsp'yi ve handler'ın şu an kullandığı rsp'yi basıyorum ikisi farklı olmalı
idt_init'te:
idt_set_gate(8, (void *)double_fault_handler);
idt[8].ist = 1;ist = 1 diyince bu gate gelince CPU TSS'deki ist1'e geçiyo. 0 "ist kullanma" demek o yüzden ist0 diye bişey yok ist1'den başlıyo
5. kernel.c
gdt_init'ten sonra idt_init'ten önce:
tss_init();GDT'den sonra olmak zorunda çünkü ltr GDT'deki TSS'yi okuyo
Ders 4: Test
aynı 0xdead0000 testini bu sefer ist açıkken çalıştırdım:

DOUBLE FAULT YAKALANDIIIIII AHAHAHAHAH TRIPLE FAULT YOK RESET YOK
rsp when it happened=0xDEAD0000yani bizim bozduğumuz stackrsp in handler=0xFFFFFFFF800E0020.nm build/kernel.elfile baktımdouble_fault_stack0xFFFFFFFF800DC0A0'dan başlıyo 16 KiB sonrası0xFFFFFFFF800E00A0. handler tam o dizinin içinde çalışıyo
yani CPU bozuk stack'i bırakıp ist1'e geçti ve handler rahatça ekrana yazabildi. eskiden makine resetleniyodu şimdi neyin nerde patladığını görüyoruz
testten sonra 0xdead0000 satırını sildim tabii
Sıradaki yazı userspace. rsp0 orda işe yarıcak