Tuna Kılıç
All posts

Narc Devlog: Fiziksel Bellek Yöneticisi (PMM) nasıl yazılır?

kendi işletim sistemim Narc için sıfırdan fiziksel bellek yöneticisi (PMM) yazıyoruz.

December 2026PublishedOSDev · Narc
İçindekiler

Selam ben Tuna, bu benim blog'umdaki ilk Türkçe yazı. Bu yazı için hiç bir SEO kuralı, yazım kuralı vb. şeyi önemsememeyi düşünüyorum bunu sırf keyif işi için yazıyorum.

Ufak ufak ders ders olarak gidecğeim. Bu yazıyı yazarken aynı anda Narc projesinin PMM'ini yazacağım ve direkt Narc üzerinden kod örnekleri vereceğim.

PMM'i Limine'ye bağlı olarak yazacağım. Araya bootloader'dan bağımsız bir bellek haritası katmanı daha sonra koyarım.

Ders 1: Fiziksel bellek nedir?

Bilgisayarınızda 16 GB RAM olduğunu düşünün.

Kernel'in bu RAM'i yönetmesi gerekiyor. Örneğin yeni bir işlem başlatacağın zaman ona bellek ayırabilmelisin.

Ama rastgele bir RAM adresini kullanamazsın. Çünkü o adreste kernel'in kodu veya başka önemli veriler bulunabilir.

İŞŞTEE PMM'İN GÖREVİ HANGI RAM BÖLGESİNİN KULLANILABİLİR OLDUĞUNU TAKİP ETMEK

1. Page kavramı

x86_64 sistemlerde yaygın olarak kullanılan bellek sayfası boyutu 4096 byte

0x0000 0x1000 0x2000 (Her biri bir kutu, ve hepsi 4096 byte fiziksel bellek temsil ediyor)

Kernel bir sayfa istediğinde PMM boş sayfalardan birini verecek

Örneğin şöyle bir fonksiyonumuz olacak:

uint64_t pmm_alloc_page(void);

2. Peki hangi RAM bölgelerinin boş olduğunu nereden bileceğiz?

İşte burada Limine Memory Map devreye girecek

Limine, kernele sistem belleğinin hangi bölümlerinin kullanılabilir veya ayrılmış olduğunu bildirebiliyor.

Kernel de önce şu isteği tanımlayacağız:

__attribute__((used, section(".limine_requests")))
static volatile struct limine_memmap_request memmap_request = {
    .id = LIMINE_MEMMAP_REQUEST_ID,
    .revision = 0
};

Framebuffer isteğine çok benziyor vallahi :D hatta aralarındaki farkı göstereyim

__attribute__((used, section(".limine_requests")))
- static volatile struct limine_framebuffer_request framebuffer_request = {
+ static volatile struct limine_memmap_request memmap_request = {
-   .id = LIMINE_FRAMEBUFFER_REQUEST_ID,
+   .id = LIMINE_MEMMAP_REQUEST_ID,
    .revision = 0
};

Neyse sonrasında

struct limine_memmap_response *response =
    memmap_request.response;

ile Limine'nin verdiği bilgielre erişeceğiz çnce response != NULL kontrolünü yapmamız gerekiyor.

Burada bulunan bellek bölgelerinin hepsi boş RAM değildir. İlk PMM'mizde yalnızca LIMINE_MEMMAP_USABLE bölgelerini kullanacağız.

3. Kernel boş sayfaları nasıl takip edecek?

Bunun için bitmap denen basit bir yöntem kullanabiliriz.

Bitmap, her sayfa için tek bir bit tutar.

Sayfa:   0 1 2 3 4 5 6 7
Bitmap:  1 1 0 0 1 0 0 0

Bizim tasarımımızda:

  • 1 → Kullanılıyor veya ayrılmış.

  • 0 → Boş, kullanılabilir.

Örneğin kernel yeni bir sayfa ayırırsa ilgili biti 1 yapacak. Sayfa serbest bırakıldığında tekrar 0 yapacak.

4. C ile bellek haritasını okuyalım

kernel.c dosyasında şu kodu ekliyorum.

if (memmap_request.response == NULL) {
    kprint("Memory map unavailable!\n");
    khalt();
}
struct limine_memmap_response *memmap =
    memmap_request.response;
for (uint64_t i = 0; i < memmap->entry_count; i++) {
    struct limine_memmap_entry *entry = memmap->entries[i];
    if (entry->type == LIMINE_MEMMAP_USABLE) {
        kprint("Usable RAM region found!\n");
    } else {
        kprint("Reserved/non-usable region found!\n");
    }
}

Şimdi kodun en önemli yerlerini anlayalım.

1. entry_count ne demek?

memmap->entry_count

Limine'in kaç tane bellek bölgesi bildirdiğini söyler.

Örneğin 12 bölge varsa döngümüz 12 defa çalışır.

2. entries[i] ne demek?

struct limine_memmap_entry *entry = memmap->entries[i];

Listedeki i numaralı bellek bölgesine erişiyoruz.

Buradaki *, entry değişkeninin bir pointer olduğunu söylüyor. Yani bellek bölgesinin bilgilerini tutan yapının adresini saklıyor. (EĞER POINTER NE DEMEK BILMIYORSAN SANA BU BLOG POSTUNU OKUMAYI ÖNERMİYORUM YAPMA!!!!)

3. entry->type ne demek?

entry->type == LIMINE_MEMMAP_USABLE

Bellek bölgesinin kullanılabilir RAM olup olmadığını kontrol ediyoruz.

-> operatörünü, bir struct'a pointer üzerinden erişmek için kullanıyoruz.

alt text

VE TEBRIKLER NARC ARTIK LIMINE FIZIKSEL RAM HARITASINI OKUYABILIYOR

Ders 2: RAM'i 4 KiB'lık sayfalara ayırmayı öğreniyoruz

Şimdi hedefimiz şu: Bilgisayarımızda kullanabileceğimiz kaç fiziksel bellek sayfası var?

1. Önce sayfa sayısını hesaplayalım

Bir sayfa 4096 byte demiştik.

Diyelim Limine bize şöyle bir bölge verdi:

Başlangıç adresi: 0x100000
Uzunluk:          16384 byte

Bu bölgede kaç sayfa var?

16384 / 4096 = 4 sayfa

Yani dört tane 4 KiB sayfa bulunuyor.

Şimdi C tarafında şu sabiti tanımlayalım:

#define PAGE_SIZE 4096

2. Memory map döngümüzü geliştirelim

Şimdi kernel.c içerisinde yazdığımız önceki döngüyü değiştireceğiz.

#define PAGE_SIZE 4096
uint64_t total_pages = 0;
uint64_t usable_bytes = 0;
for (uint64_t i = 0; i < memmap->entry_count; i++) {
    struct limine_memmap_entry *entry = memmap->entries[i];
    if (entry->type != LIMINE_MEMMAP_USABLE) {
        continue;
    }
    usable_bytes += entry->length;
    uint64_t start =
        (entry->base + PAGE_SIZE - 1) & ~(PAGE_SIZE - 1);
    uint64_t end =
        (entry->base + entry->length) & ~(PAGE_SIZE - 1);
    if (end > start) {
        total_pages += (end - start) / PAGE_SIZE;
    }
}

Biraz yeni şey var. Hepsini anlatıyorum dostinyolar.

Şuraya bak:

if (entry->type != LIMINE_MEMMAP_USABLE) {
    continue;
}

Burada diyoruz ki:

"Eğer bu bölge kullanılabilir değilse, döngünün bu turunu geç ve sonraki bölgeye bak."

Böylece sadece kullanabileceğimiz RAM'i sayıyoruz.

4. Neden start ve end hesaplıyoruz?

Şu kod biraz karışık görünebilir:

uint64_t start =
    (entry->base + PAGE_SIZE - 1) & ~(PAGE_SIZE - 1);

Bunun adı alignment (hizalama).

Fiziksel sayfaların başlangıç adresleri 4096'nın katı olmalı.

Örneğin:

0x1000 → Sayfa başlangıcı
0x2000 → Sayfa başlangıcı
0x3000 → Sayfa başlangıcı

Ama kullanılabilir bölge 0x1003 adresinden başlıyorsa, ilk tam sayfamız 0x2000 adresinden başlayabilir.

Bu yüzden başlangıcı yukarı, bitişi aşağı yuvarlıyoruz. Böylece yalnızca bölgenin tamamen içinde kalan sayfaları saymış oluyoruz.

Bu hesap, Limine'in verdiği geçerli bellek bölgelerinin adres aralıklarının taşmadığını varsayıyor.

Artık iki önemli bilgiye sahibiz:

uint64_t usable_bytes;
uint64_t total_pages;
  • usable_bytes → Kullanılabilir RAM'in toplam byte miktarı.

  • total_pages → Ayırabileceğimiz tam 4 KiB fiziksel sayfa sayısı.

Ders 3: PMM özel sayfa dosyalarını yazma

PMM kodlarını kernel.c içine doldurmak yerine ayrı bir pmm.c dosyasında tutmalıyız.

  • pmm.c: Fiziksel RAM sayfalarını yönetme.

  • pmm.h: PMM'nin diğer dosyalara sunduğu fonksiyonlar.

Önce pmm.h yazalım:

#ifndef PMM_H
#define PMM_H
#include <stdint.h>
#include <limine.h>
#define PAGE_SIZE 4096
void pmm_init(struct limine_memmap_response *memmap);
uint64_t pmm_get_total_pages(void);
uint64_t pmm_get_usable_bytes(void);
#endif

Burada pmm_init() fonksiyonuna bellek haritasını vereceğiz.

Neden?

Çünkü Limine isteğimiz kernel.c içerisinde tanımlı. PMM'nin aynı isteği tekrar oluşturmasına gerek yok. Kernel bellek haritasını alacak ve PMM'ye iletecek.

Şimdi pmm.c yazalım

Bu aşamada henüz sayfa ayırmayacağız. Önce RAM'i sayan kodları pmm.c içerisine taşıyoruz.

#include <stddef.h>
#include "pmm.h"
static uint64_t total_pages = 0;
static uint64_t usable_bytes = 0;
void pmm_init(struct limine_memmap_response *memmap) {
    total_pages = 0;
    usable_bytes = 0;
    if (memmap == NULL) {
        return;
    }
    for (uint64_t i = 0; i < memmap->entry_count; i++) {
        struct limine_memmap_entry *entry = memmap->entries[i];
        if (entry->type != LIMINE_MEMMAP_USABLE) {
            continue;
        }
        usable_bytes += entry->length;
        total_pages += entry->length / PAGE_SIZE;
    }
}
uint64_t pmm_get_total_pages(void) {
    return total_pages;
}
uint64_t pmm_get_usable_bytes(void) {
    return usable_bytes;
}

Birkaç önemli şeyi anlayalım

Neden static kullandık?

static uint64_t total_pages = 0;

Çünkü bu değişkeni yalnızca pmm.c dosyasının kullanmasını istiyoruz. Diğer dosyalar doğrudan değiştiremesin.

Bunun yerine:

uint64_t pmm_get_total_pages(void) {
    return total_pages;
}

fonksiyonunu çağırarak değerini öğrenebilecekler.

Neden entry->length / PAGE_SIZE?

Çünkü bölgenin kaç tane 4096 byte'lık sayfa içerdiğini buluyoruz.

Limine, USABLE bellek bölgelerinin başlangıç adresi ve uzunluğunun 4096 byte hizalı olduğunu garanti ediyor. Bu yüzden burada ayrıca hizalama yapmamıza gerek yok.

kernel.c dosyasını düzenleyelim

En üste:

#include "pmm.h"

ve eski eklediğimiz şeyleri kaldırıp şunları koyacağız

if (memmap_request.response == NULL) {
    kprint("Memory map unavailable!\n");
    khalt();
}
pmm_init(memmap_request.response);
kprint("PMM memory map initialized!\n");

Ders 4: Bitmap

Şimdi önemli bir ayrım var.

Şu anda PMM yalnızca RAM'i sayıyor. Henüz bellek ayıramıyor.

Gerçek bir allocator oluşturmak için hangi fiziksel sayfaların boş olduğunu kaydetmemiz gerekiyor.

Örneğin:

Fiziksel sayfa:  0 1 2 3 4 5 6 7
Bitmap:         1 1 0 0 1 0 0 0

Ama bitmap'teki bitlerin fiziksel sayfa numaralarıyla eşleşmesi gerekiyor. Çünkü Limine'in verdiği kullanılabilir bölgeler RAM'de birbirinden ayrı olabilir.

Şimdi bitmap'i oluşturalım

Önce küçük bir örnek:

uint8_t bitmap = 0;

Bu değişken başlangıçta şöyle:

00000000

Yani sekiz sayfanın tamamı boş olarak işaretlenmiş.

Diyelim üçüncü sayfayı kullanıma ayırdık:

bitmap |= (1u << 2);

Sonuç:

00000100

Burada 2 numaralı sayfa artık dolu.

Sayfayı serbest bırakmak istersek:

bitmap &= ~(1u << 2);

Tekrar:

00000000

burada 1 dolu, 0 boş anlamına geliyor

Peki gerçek PMM'de nasıl olacak?

Tek bir uint8_t yeterli olmayacak. Çünkü binlerce, hatta milyonlarca fiziksel sayfamız olabilir.

Bu yüzden bitmap'i bir byte dizisi olarak tutacağız:

static uint8_t *bitmap;

Bitmap'te bir sayfanın hangi bite denk geldiğini bulalım

Diyelim 32 sayfamız var:

uint8_t bitmap[4];

Her byte 8 sayfayı tutuyor:

bitmap[0] → Sayfa  0–7
bitmap[1] → Sayfa  8–15
bitmap[2] → Sayfa 16–23
bitmap[3] → Sayfa 24–31

Mesela 10 numaralı sayfayı dolu işaretlemek istiyoruz.

Önce hangi byte içinde olduğunu bulalım:

uint64_t byte_index = 10 / 8; // 1

Sonra o byte'ın hangi bitinde olduğunu bulalım:

uint8_t bit_index = 10 % 8; // 2

Ve biti 1 yapalım:

bitmap[byte_index] |= (1u << bit_index);

Yani aslında şunu yapmış olduk:

bitmap[1] |= (1u << 2);

Böylece 10 numaralı sayfayı dolu olarak işaretledik.

Şimdi pmm.c için ilk yardımcı fonksiyon

static void bitmap_set(uint64_t page) {
    bitmap[page / 8] |= (uint8_t)(1u << (page % 8));
}

Bu fonksiyon bir sayfayı dolu olarak işaretleyecek. Tabii kullanmadan önce bitmap için gerçek bellek ayırmamız ve sınır kontrollerini kurmamız gerekecek.

bitmap_clear() yazıyoruz

bitmap_set() yazdık onun tam tersini yazıyoruz şuandada

static void bitmap_clear(uint64_t page) {
    bitmap[page / 8] &= (uint8_t)~(1u << (page % 8));
}
  • bitmap_set() → İlgili biti 1 yapar, sayfa dolu.

  • bitmap_clear() → İlgili biti 0 yapar, sayfa boş.

Bir de sayfanın dolu olup olmadığını öğrenmek istiyoruz.

static bool bitmap_test(uint64_t page) {
    return (bitmap[page / 8] & (1u << (page % 8))) != 0;
}

Bu fonksiyon true döndürürse sayfa doludur, false döndürürse boştur.

Bunun için pmm.c başına #include <stdbool.h> eklemelisin.

Şimdi bitmap'i RAM'e yerleştirelim

Diyelim bitmap'imiz için 8192 byte gerekiyor. Bir fiziksel sayfa 4096 byte olduğuna göre bitmap'e 2 sayfa ayırmalıyız.

Bunu C'de şöyle hesaplıyoruz:

uint64_t bitmap_pages =
    (bitmap_bytes + PAGE_SIZE - 1) / PAGE_SIZE;

Buradaki PAGE_SIZE - 1, sonuç yukarı yuvarlansın diye var.

Örneğin 5000 byte gerekiyorsa:

5000 / 4096 = 1  (normal tamsayı bölmesi)
(5000 + 4095) / 4096 = 2

Şimdi pmm.c içine ilk hazırlığımızı ekleyelim:

static uint8_t *bitmap = NULL;
static uint64_t bitmap_bytes = 0;
static uint64_t bitmap_pages = 0;

Bunlar ne tutacak?

  • bitmap → Bitmap'in RAM'deki sanal adresi.

  • bitmap_bytes → Bitmap'in toplam boyutu.

  • bitmap_pages → Bitmap'in kaplayacağı 4 KiB sayfa sayısı.

Limine'den HHDM adresini de alıp fiziksel adresi kernel'in erişebileceği sanal adrese dönüştürmemiz gerekiyor

Ders 5: HHDM ne işe yarıyor?

Bu, RAM'deki fiziksel bir konumu gösteriyor.

Ancak kernel'imiz sanal bellek kullanıyor. Fiziksel adresi doğrudan pointer'a dönüştürüp kullanamayız.

Limine bize HHDM (Higher Half Direct Map) adında bir eşleme sağlayabiliyor.

Bunun sayesinde:

uint64_t virtual_address = physical_address + hhdm_offset;

yazarak fiziksel RAM'in eşlenmiş sanal adresine ulaşabiliriz.

Örneğin:

uint8_t *bitmap =
    (uint8_t *)(physical_address + hhdm_offset);

Artık bitmap üzerinden RAM'e erişebiliriz.

Burada önemli bir şart var: Kullandığımız fiziksel adres HHDM içerisinde eşlenmiş olmalı. Limine'in sağladığı HHDM bilgisine göre hareket edeceğiz.

kernel.c içerisine HHDM isteği ekleyelim

__attribute__((used, section(".limine_requests")))
static volatile struct limine_hhdm_request hhdm_request = {
    .id = LIMINE_HHDM_REQUEST_ID,
    .revision = 0
};

Sonra kmain() içerisinde, pmm_init() çağırmadan önce:

if (hhdm_request.response == NULL) {
    kprint("HHDM unavailable!\n");
    khalt();
}

kontrolünü yapacağız.

alt text

HHDM unavailable demediğine göre NO PROBLEMMMMM

pmm.h dosyamızı güncelleyelim

Artık pmm_init() fonksiyonuna HHDM offset'ini de vereceğiz.

Bitmap'i koyabileceğimiz RAM bölgesini bulalım

Şimdi pmm.c dosyasına geçiyoruz.

Önce şu değişkenlerimiz olsun:

static uint8_t *bitmap = NULL;
static uint64_t bitmap_bytes = 0;
static uint64_t bitmap_pages = 0;
static uint64_t bitmap_phys = 0;

Sadece kullanılabilir sayfaları değil, fiziksel adres aralığındaki ayrılmış sayfaları da takip edeceğiz.

Örneğin 100 sayfamız kullanılabilir olsa bile en yüksek fiziksel adres 1000. sayfaya denk gelebilir.

Bu yüzden önce takip edeceğimiz en yüksek fiziksel adresi buluyoruz:

uint64_t highest_address = 0;
for (uint64_t i = 0; i < memmap->entry_count; i++) {
    struct limine_memmap_entry *entry = memmap->entries[i];
    uint64_t end = entry->base + entry->length;
    if (end > highest_address) {
        highest_address = end;
    }
}

Burada her bölgenin bittiği adresi hesaplıyoruz ve en büyüğünü buluyoruz. Geçerli memory map aralıklarında toplamanın taşmadığını varsayıyoruz; sağlam bir PMM'de bunu da kontrol edeceğiz.

Ardından:

uint64_t tracked_pages =
    (highest_address + PAGE_SIZE - 1) / PAGE_SIZE;
bitmap_bytes = (tracked_pages + 7) / 8;
bitmap_pages =
    (bitmap_bytes + PAGE_SIZE - 1) / PAGE_SIZE;

bitmap_bytes hesabında neden +7 var?

Çünkü 9 sayfamız olsaydı 9 bit gerekirdi. Bu da 1 byte'a sığmaz, 2 byte gerektirirdi.

Bitmap için uygun bölgeyi seçelim

Şimdi yeterince büyük bir kullanılabilir RAM bölgesi arayacağız.

for (uint64_t i = 0; i < memmap->entry_count; i++) {
    struct limine_memmap_entry *entry = memmap->entries[i];
    if (entry->type != LIMINE_MEMMAP_USABLE) {
        continue;
    }
    if (entry->length >= bitmap_pages * PAGE_SIZE) {
        bitmap_phys = entry->base;
        break;
    }
}

Burada şunu yapıyoruz:

"Bitmap'in kaplayacağı sayfaları içine sığdırabilecek ilk kullanılabilir bölgeyi bul."

bitmap_phys artık bitmap'in fiziksel başlangıç adresini gösteriyor. Gerçek uygulamada uygun bölge bulunamazsa hata vermeliyiz; 0 adresini başarısızlık işareti olarak kullanmak doğru değil, çünkü fiziksel adres 0 da geçerli olabilir.

Sonra:

bitmap = (uint8_t *)(bitmap_phys + hhdm_offset);

Artık bitmap pointer'ımız kullanılabilir RAM'deki bir adresi gösteriyor.

Ders 6: Gerçek fiziksel RAM sayfası ayırma

Pmm Alloc yazalım

uint64_t pmm_alloc_page(void) {
    if (!initialized) {
        return 0;
    }
    for (uint64_t page = 1; page < tracked_pages; page++) {
        if (!bitmap_test(page)) {
            bitmap_set(page);
            free_pages--;
            return page * PAGE_SIZE;
        }
    }
    return 0;
}

Burada yaptığımız şey basit:

1. Bitmap'teki sayfaları tek tek kontrol ediyoruz.

2. bitmap_test(page) ile sayfanın boş olup olmadığına bakıyoruz.

3. Boşsa bitmap_set(page) ile dolu işaretliyoruz.

4. O sayfanın fiziksel adresini döndürüyoruz.

Diyelim 256 numaralı sayfa boş çıktı:

256 × 4096 = 1048576 = 0x100000

Fonksiyon 0x100000 döndürür.

Şimdi kernel.c içinde ilk sayfamızı ayıralım

pmm_init() başarılı olduktan sonra şu kodu ekliyoryuz

uint64_t page = pmm_alloc_page();
if (page == 0) {
    kprint("PMM: allocation failed!\n");
    khalt();
}
kprint("PMM: physical page allocated!\n");

VE BUNU ÇALIŞTIRDIĞIMIZDA::::

alt text

Ancak bu mesaj sadece PMM'nin bir fiziksel adres ayırdığını gösteriyor. Henüz o belleğe veri yazmadık.

Ayırdığımız sayfaya veri yazalım

Burada önemli bir şeyi hatırlayacağız

pmm_alloc_page() bize fiziksel adres döndürüyor

C ile belleğe erişmek için bu fiziksel adresi HHDM üzerinden sanal adrese çevireceğiz.

uint64_t hhdm = hhdm_request.response->offset;
uint8_t *memory = (uint8_t *)(uintptr_t)(page + hhdm);

Bu iki satırın yaptığı şey:

  • page -> Ayırdığımız fiziksel sayfanın adresi

  • hhdm -> Limine'in fiziksel RAM için sağladığı sanal adres offset'i.

  • memory -> C ile kullanabileceğimiz sanal adres

Şimdi ilk byte'a bir değer yazalım:

memory[0] = 42;

ve okuyalım:

if (memory[0] == 42) {
    kprint("PMM: memory read/write successful!\n");
}

Tebrikler! Bu mesajı görürsen ayırdığın fiziksel sayfaya erişebildiğini doğrulamış olacaksın.

Sayfayı serbest bırakalım

İşimiz bitince:

pmm_free_page(page);
kprint("PMM: physical page freed!\n");

Böylece PMM ilgili sayfayı yeniden boş olarak işaretleyecek.

Tam test kodumuz

uint64_t page = pmm_alloc_page();
if (page == 0) {
    kprint("PMM: allocation failed!\n");
    khalt();
}
kprint("PMM: physical page allocated!\n");
uint64_t hhdm = hhdm_request.response->offset;
uint8_t *memory =
    (uint8_t *)(uintptr_t)(page + hhdm);
// fiziksel sayfanın ilk byte'ına veri yaz
memory[0] = 42;
// yazdığımız veriyi kontrol et
if (memory[0] == 42) {
    kprint("PMM: read/write successful!\n");
} else {
    kprint("PMM: read/write failed!\n");
    khalt();
}
// sayfayı serbest bırak
pmm_free_page(page);
kprint("PMM: physical page freed!\n");

alt text

EVET TEBRIK EDERIM HEPINIZI, ÜŞENMEZSEM VMM İÇİNDE YAZARIM ZATEN ÇOK OKUYUCUMUZ OLACAĞINI ZANNETMİYORUM