Selam ben Tuna, GDT ve TSS yazılarından sonra asıl şeye geldik. bu yazının sonunda C ile yazılmış bi program ring 3'te çalışıp ekrana "Hello from Userspace!" yazıcak
GDT ve TSS yazılarını okumadıysan önce onları oku
Ders 1: Ring 3 nedir
Şu ana kadar Narc'taki her şey ring 0'da yani kernel modunda çalıştı. ring 0'daki kod her şeyi yapabiliyo her belleği okuyabiliyo interrupt'ları kapatabiliyo vs
ring 3 user modu. ring 3'teki kod:
PTE_USERbiti olmayan sayfalara dokunamıyoclihltgibi komutları çalıştıramıyo- kernel'den bişey isterse syscall yapmak zorunda
yani bi program saçmalarsa kernel'i bozamıyo. işletim sistemi dediğimiz şeyin olayı bu
Yapacaklarımız:
- kernel'de syscall (int 0x80)
- küçük bi libc (sadece print yetecek kadar)
- C ile hello world
- programı belleğe koyup ring 3'e zıplamak
Ders 2: Syscall
user programı ekrana direkt yazamaz (framebuffer kernel'de map'li user'a açık değil). o yüzden kernel'den "şunu ekrana yaz" diye istemesi lazım. buna syscall diyoruz
İki yolu var syscall komutu ya da int 0x80. int 0x80 normal bi interrupt yani IDT'ye bi gate eklemek yetiyo, o yüzden onu yaptım
1. IDT
idt.c'ye:
extern void syscall_entry(void);idt_set_gate(0x80, (void *)syscall_entry);
idt[0x80].type_attr = 0xEE;diğer gate'ler 0x8E bu 0xEE. aradaki fark DPL yani hangi ring'in bu interrupt'ı çağırabileceği. 0x8E DPL 0 0xEE DPL 3. DPL 0 bırakırsak user int 0x80 yapınca CPU izin vermiyo general protection fault atıyo
2. syscall_entry
Diğer handler'lar gibi __attribute__((interrupt)) kullanamadım çünkü syscall'ın numarası ve argümanları register'larda geliyo (rax rdi rsi) ve gcc'nin interrupt attribute'u bize register'ları vermiyo. o yüzden giriş kısmını assembly yazdım. bütün register'ları stack'e push edip C fonksiyonuna stack'in adresini veriyo:
__asm__(
".global syscall_entry\n"
"syscall_entry:\n"
" push %rax\n"
" push %rbx\n"
" push %rcx\n"
" push %rdx\n"
" push %rsi\n"
" push %rdi\n"
" push %rbp\n"
" push %r8\n"
" push %r9\n"
" push %r10\n"
" push %r11\n"
" push %r12\n"
" push %r13\n"
" push %r14\n"
" push %r15\n"
" mov %rsp, %rdi\n"
" call syscall_handler\n"
" pop %r15\n"
...
" pop %rax\n"
" iretq\n"
);(pop'lar push'ların tersi sırada uzun olmasın diye kestim)
C tarafında stack'teki register'ları struct olarak görüyoruz:
struct syscall_regs {
uint64_t r15, r14, r13, r12, r11, r10, r9, r8;
uint64_t rbp, rdi, rsi, rdx, rcx, rbx, rax;
};en son push ettiğimiz r15 en düşük adreste o yüzden struct r15 ile başlıyo
3. syscall_handler
linux'taki numaraları kullandım write 1 exit 60:
#define SYS_WRITE 1
#define SYS_EXIT 60
__attribute__((used))
void syscall_handler(struct syscall_regs *regs) {
if (regs->rax == SYS_WRITE) {
const char *buf = (const char *)regs->rdi;
uint64_t len = regs->rsi;
if (regs->rdi >= 0x800000000000 || len > 4096) {
regs->rax = -1;
return;
}
for (uint64_t i = 0; i < len; i++)
terminal_putchar(buf[i]);
regs->rax = len;
} else if (regs->rax == SYS_EXIT) {
kprint("USER: program exited with code ");
kprint_dec(regs->rdi);
kprint("\n");
kernel_idle();
} else {
regs->rax = -1;
}
}- write: rdi'deki adresten rsi kadar karakteri ekrana basıyo.
regs->rax'a yazdığımız şey asm'de pop edilip user'a dönüş değeri olarak gidiyo - user bize kernel'in adresini verip "bunu ekrana yaz" diyebilir o yüzden adres user yarısında mı (
0x800000000000'dan küçük mü) diye bakıyorum. çok basit bi kontrol ama şimdilik idare eder - exit: kodu yazıp
kernel_idle'a gidiyo
used koydum çünkü bu fonksiyonu sadece assembly çağırıyo gcc kullanılmıyo sanıp silmesin
4. kernel_idle
Eskiden kmain'in sonunda N tuşuna basınca yazı yazan bi döngü vardı. program exit olunca kernel'in bi yere gitmesi lazım o yüzden o döngüyü fonksiyona çevirdim:
void kernel_idle(void) {
__asm__ volatile ("sti");
uint8_t previous = 0;
for (;;) {
uint8_t current = keyboard_get_last_scancode();
...
__asm__ volatile ("hlt");
}
}başta sti var çünkü syscall'dan buraya geldiğimizde interrupt'lar kapalı oluyo (interrupt gate CPU'ya girerken interrupt'ları kapatıyo)
Ders 3: Küçük libc
Şimdi user tarafı. programı C ile yazmak istiyorum ama C'nin standart kütüphanesi yok elimizde. printf falan hiçbişey. o yüzden kendi minik libc'mizi yazıyoruz sadece print yetecek kadar
user kodu kernel'den ayrı derlenmeli o yüzden projenin köküne user/ klasörü açtım
1. libc.h
#ifndef LIBC_H
#define LIBC_H
#include <stddef.h>
size_t strlen(const char *s);
long write(const char *buf, size_t len);
void print(const char *s);
void exit(int code) __attribute__((noreturn));
#endif2. libc.c
#define SYS_WRITE 1
#define SYS_EXIT 60
int main(void);
static long syscall2(long num, long arg1, long arg2) {
long ret;
__asm__ volatile ("int $0x80" : "=a"(ret) : "a"(num), "D"(arg1), "S"(arg2) : "memory");
return ret;
}asm'deki harfler register demek: a rax D rdi S rsi. yani syscall numarasını rax'a argümanları rdi ve rsi'ye koyup int $0x80 yapıyo, dönüşte rax'taki değeri alıyo. kernel'deki handler'ın beklediği şey tam bu
size_t strlen(const char *s) {
size_t len = 0;
while (s[len] != '\0')
len++;
return len;
}
long write(const char *buf, size_t len) {
return syscall2(SYS_WRITE, (long)buf, (long)len);
}
void print(const char *s) {
write(s, strlen(s));
}
void exit(int code) {
syscall2(SYS_EXIT, code, 0);
for (;;) {
}
}print = strlen + write o kadar
3. _start
C programları aslında main'den başlamıyo. önce _start çalışıyo o main'i çağırıyo, main dönünce de exit yapıyo:
__attribute__((section(".text._start")))
void _start(void) {
exit(main());
}section(".text._start") kısmı _start'ın binary'nin en başında olması için. kernel programı 0x400000'a koyup direkt oraya zıplıyacak yani ilk byte'ta _start olmalı
4. hello.c
ve programın kendisi:
#include "libc.h"
int main(void) {
print("Hello from Userspace!\n");
return 0;
}5. user/linker.ld
ENTRY(_start)
SECTIONS
{
. = 0x400000;
.text : { *(.text._start) *(.text .text.*) }
.rodata : { *(.rodata .rodata.*) }
.data : { *(.data .data.*) *(.bss .bss.*) *(COMMON) }
/DISCARD/ : { *(.comment) *(.note .note.*) *(.eh_frame*) }
}
- program
0x400000'da çalışacak .text._starten başa.bss'i.data'nın içine koydum. çünkü birazdan binary'yi düz dosyaya çeviricez ve objcopy.bss'i dosyaya koymuyo (bss normalde sadece "şu kadar sıfır lazım" bilgisi). data'nın içine koyunca sıfırları dosyaya yazıyo, şimdilik kolay yol
6. Makefile
user programı kernel'den farklı flag'lerle derlenmeli (mesela -mcmodel=kernel olmamalı):
USER_CFLAGS := -m64 -std=gnu11 -O2 -Wall -Wextra -ffreestanding -fno-pie -fno-pic -fno-stack-protector -fno-builtin -fno-asynchronous-unwind-tables -mgeneral-regs-only -fno-tree-loop-distribute-patterns
USER_SRCS := user/libc.c user/hello.c
USER_OBJS := $(patsubst user/%.c,$(BUILD)/user/%.o,$(USER_SRCS))
USER_BIN := $(BUILD)/user/hello.bin
$(BUILD)/user/%.o: user/%.c user/libc.h
@mkdir -p $(dir $@)
$(CC) $(USER_CFLAGS) -c $< -o $@
$(BUILD)/user/hello.elf: $(USER_OBJS) user/linker.ld
$(LD) -m elf_x86_64 -nostdlib -static -T user/linker.ld -o $@ $(USER_OBJS)
$(USER_BIN): $(BUILD)/user/hello.elf
objcopy -O binary $< $@
$(BUILD)/kernel/user/usermode.o: $(USER_BIN)
-mgeneral-regs-only önemli. Limine SSE'yi açmıyo (CR4'te OSFXSR kapalı), gcc user kodunda SSE komutu üretirse program "geçersiz komut" ile patlar. kernel'de de aynı flag zaten vardı
objcopy -O binary ELF'in bütün header'larını atıp sadece içindeki kod ve veriyi düz dosya yapıyo. ELF yükleyicimiz yok o yüzden şimdilik düz binary
en sondaki satır: usermode.o binary'yi içine gömüyo, binary değişirse usermode.o tekrar derlensin diye
objdump ile baktım:
0000000000400000 <_start>:
400000: 48 83 ec 08 sub $0x8,%rsp
400004: e8 b7 00 00 00 call 4000c0 <main>
_start tam 0x400000'da. bütün program 236 byte :D
Ders 4: Programı yükleyip ring 3'e zıplamak
1. VMM'de küçük bi düzeltme
VMM yazısında demiştim CPU izinleri bütün seviyelerde kontrol ediyo. bi sayfayı user'a açmak için PML4 PDPT PD PT'nin hepsinde U biti lazım. ama get_next_table yeni tablo açarken U bitini koymuyodu. flags'ı parametre olarak ekledim:
static uint64_t *get_next_table(uint64_t *table, uint64_t index, uint64_t flags) {
if (table[index] & PTE_PRESENT) {
if (table[index] & PTE_HUGE)
return NULL;
if (flags & PTE_USER)
table[index] |= PTE_USER;
return phys_to_table(table[index] & PTE_ADDR_MASK);
}
...
table[index] = page | PTE_PRESENT | PTE_WRITE | (flags & PTE_USER);
return phys_to_table(page);
}bunu yapmasaydık PT'de U olurdu ama üst tablolarda olmazdı ve program ilk komutta page fault yerdi
2. Binary'yi kernel'e gömmek
wallpaper'ı gömdüğümüz gibi .incbin ile:
__asm__(
".section .rodata\n"
"user_program_start:\n"
".incbin \"build/user/hello.bin\"\n"
"user_program_end:\n"
".previous\n"
);
extern uint8_t user_program_start[];
extern uint8_t user_program_end[];3. Sayfaları map etmek
#define USER_CODE_ADDR 0x400000
#define USER_STACK_TOP 0x800000
#define USER_STACK_PAGES 4static void map_user_page(uint64_t virt, uint64_t flags) {
uint64_t phys = pmm_alloc_page();
if (phys == 0 || !vmm_map_page(virt, phys, flags | PTE_USER)) {
kprint("USER: could not map page!\n");
khalt();
}
}void usermode_start(void) {
uint64_t size = user_program_end - user_program_start;
uint64_t pages = (size + PAGE_SIZE - 1) / PAGE_SIZE;
for (uint64_t i = 0; i < pages; i++)
map_user_page(USER_CODE_ADDR + i * PAGE_SIZE, PTE_WRITE);
for (uint64_t i = 1; i <= USER_STACK_PAGES; i++)
map_user_page(USER_STACK_TOP - i * PAGE_SIZE, PTE_WRITE | PTE_NX);
memset((void *)USER_CODE_ADDR, 0, pages * PAGE_SIZE);
memcpy((void *)USER_CODE_ADDR, user_program_start, size);
kprint("USER: program is ");
kprint_dec(size);
kprint(" bytes, jumping to userspace...\n");
jump_to_usermode(USER_CODE_ADDR, USER_STACK_TOP - 8);
}- kod sayfaları yazılabilir ve çalıştırılabilir. binary'de kod ve veri aynı sayfada olduğu için şimdilik ikisi de lazım
- stack 16 KiB
0x800000'ın hemen altında, yazılabilir ama çalıştırılamaz - sayfaları önce sıfırlıyoruz sonra programı kopyalıyoruz. sıfırlamazsak PMM'den gelen sayfada başkasının eski verisi kalabilir
memcpy'yi direkt user adresine yapabiliyoruz çünkü CR3 aynı ve kernel her sayfaya erişebiliyo
stack'i neden -8 verdim? C fonksiyonları başladığında rsp % 16 = 8 olmasını bekliyo (çünkü normalde call 8 byte'lık dönüş adresi push ediyo). _start'ı call ile değil iretq ile çağırdığımız için o 8'i kendimiz çıkarıyoruz
4. iretq ile ring 3
ring 3'e geçmek için direkt bi komut yok. ama interrupt'tan dönerken iretq stack'ten rip cs rflags rsp ss alıyo. cs ring 3'ü gösteriyosa CPU ring 3'e iniyo. biz de stack'e sahte bi interrupt frame koyup iretq yapıyoruz CPU "user'dan gelen interrupt'tan dönüyorum" sanıyo:
static void jump_to_usermode(uint64_t entry, uint64_t stack) {
__asm__ volatile (
"pushq $0x1B\n"
"pushq %0\n"
"pushq $0x202\n"
"pushq $0x23\n"
"pushq %1\n"
"iretq\n"
: : "r"(stack), "r"(entry) : "memory"
);
}sırayla:
0x1Bss = user veri (0x18) | 3- stack adresi
0x202rflags. bit 9 interrupt'lar açık (yoksa user'dayken timer bile çalışmaz) bit 1 hep 1 olmalı0x23cs = user kod (0x20) | 3- programın başlangıç adresi
selector'ların sonundaki | 3 "ring 3 olarak istiyorum" demek. GDT yazısında user segmentlerini DPL 3 yapmıştık o yüzden izin veriliyo
5. rsp0
TSS yazısında rsp0 koymuştuk işte burda işe yarıyo. user int 0x80 yapınca (ya da timer gelince) CPU ring 3'ten ring 0'a geçiyo ve stack'i TSS'deki rsp0'a çeviriyo. rsp0 olmasaydı CPU kernel'e hangi stack'le gireceğini bilemezdi
6. kernel.c
kmain'in en sonunda heap testlerinden sonra:
usermode_start();Ders 5: Test

HELLO FROM USERSPACEEEEE AHAHAHAHAHAHAHAHAH
NARC'TA İLK DEFA RING 3'TE KOD ÇALIŞTI. o yazıyı kernel yazmadı C ile yazdığım program kendi libc'siyle syscall yapıp yazdırdı. main 0 döndü _start exit(0) yaptı kernel de exit kodunu yazdı
exit'ten sonra kernel_idle'a geçiyoduk bakalım gerçekten yaşıyo mu N'ye bastım:

KERNEL YAŞIYOOO program bitti kernel klavyeyi dinlemeye devam ediyo
Bi de kötü program deneyelim
user gerçekten kernel'e dokunamıyo mu? hello.c'yi geçici olarak şöyle yaptım:
int main(void) {
print("Hello from Userspace!\n");
print("trying to read kernel memory...\n");
volatile char *kernel = (volatile char *)0xFFFFFFFF80000000;
char c = *kernel;
print("I read the kernel lol\n");
return c;
}
OKUYAMADIIII AHAHAHAH "I read the kernel lol" hiç yazılmadı
- address
0xFFFFFFFF80000000kernel'in başı - error
0x5= bit 0 (sayfa var) + bit 2 (user'dan geldi). yani sayfa var ama user'ın izni yok, U biti yok - rip
0x4000D5yani user programının içi
şimdilik user hata yapınca kernel'in page fault handler'ı her şeyi durduruyo, ilerde sadece programı öldürecek şekilde düzeltirim. testten sonra hello.c'yi eski haline aldım
Son
Narc'ta artık:
- kendi GDT'si
- TSS ve double fault için ayrı stack
- int 0x80 ile write ve exit syscall'ları
- küçük bi libc (strlen write print exit)
- C ile yazılmış bi program ring 3'te çalışıp ekrana yazı yazabiliyo
eksikler çok:
- user hata yapınca bütün kernel duruyo
- tek program var ve kernel'e gömülü, ELF yükleyici lazım
- her programın kendi page table'ı yok
- scheduler yok
sırada büyük ihtimalle bunlar var