Bölüm 13:Bellek Aslında Sanal
CPU'ya “Sen”i Katmak yazısının parçası: bilgisayarının programları nasıl çalıştırdığına doğru inen uzun bir teknik tavşan deliği.
Tüm bölümler
- Giriş
- Başlamadan Önce
- Temeller
- Kernel, User Mode ve Syscall
- Mimariler: x86, ARM ve Diğerleri
- Bellek Hiyerarşisi
- Cache Nasıl Çalışır
- Zamanı Dilimle
- İşlemciyi Hızlandıran Hileler
- Tahmin, Spekülasyon ve Spectre
- Bir Program Nasıl Çalıştırılır?
- Shell'den Kernel'e
- Bir ELF Ustasına Dönüşmek
- Bellek Aslında Sanal
- Adres Çevirisi ve TLB
- Bellek Güvenliği ve Sertleştirme
- Fork'lar ve COW'lar Hakkında Konuşalım
- Dosya Sistemi ve I/O
- Sanal Makineler ve Container'lar
- Son Söz
Şimdiye kadar bellek konusunu kasten basitleştirerek anlattım; bu bölümde perdeyi kaldırıyoruz. ELF dosyaları verilerin yükleneceği belirli bellek adreslerini söylüyor. Peki farklı process’lerde aynı adresleri kullanmaya çalışan yapılar neden çakışmıyor? Neden her process’in kendine ait ayrı bir bellek ortamı varmış gibi görünüyor?
Bir de buraya nasıl geldik? execve’nin mevcut process’i yeni bir programla değiştiren bir syscall olduğunu artık biliyoruz, ama bu hâlâ birden fazla process’in nasıl ortaya çıktığını açıklamıyor. Daha da önemlisi, ilk programın nasıl başladığını hiç açıklamıyor. Diğer bütün yumurtaları yumurtlayan ilk tavuk hangi process?
Bu soruların cevabını 16. bölümde bulacaksın:
forkile process klonlama, Copy-on-Write ve bilgisayarın açılışındainit process’in nasıl doğduğunu orada ele alıyoruz. Şimdi önce bellek meselesini halledelim.
Yolculuğun sonuna yaklaşıyoruz. Bellek konusunu çözdükten sonra bilgisayarının açılıştan şu anda kullandığın yazılıma kadar nasıl geldiğine dair neredeyse tam bir resmimiz olacak.
Bu bölümde neyi çözüyoruz?
- RAM’in neden her process’e ayrı bir dünya gibi göründüğünü anlayacağız.
- Page table, MMU, TLB ve page fault kavramlarını aynı hikâyeye bağlayacağız.
- Bellek izolasyonu, ASLR, demand paging ve swap’ın hangi probleme çözüm olduğunu göreceğiz.
Bellek Aslında Sanal
Gelelim belleğe. Bunu önce adres defteri gibi düşün: Program “0x400000 adresindeki veriyi oku” dediğinde, bu adres çoğu zaman RAM çipindeki gerçek konum değildir. Process’in elinde kendine ait bir adres defteri vardır; MMU bu deftere bakıp “bu sanal adres fiziksel RAM’de şu sayfaya karşılık geliyor” çevirisini yapar.
CPU bir bellek adresinden okuma ya da o adrese yazma yaptığında aslında doğrudan physical memory’deki (RAM’deki) o noktaya gitmez; önce virtual memory alanındaki bir konuma erişir.
CPU, memory management unit (MMU) denen bir birimle konuşur. MMU, sanal adresleri RAM’deki fiziksel adreslere çeviren bir sözlük gibi davranır. CPU’ya 0xfffaf54834067fe2 adresinden okuma yap denildiğinde, CPU önce MMU’ya gidip “bunu çevir” der. MMU eşleşen fiziksel adresin 0x53a4b64a90179fe2 olduğunu bulur ve bu sonucu CPU’ya verir. CPU da artık RAM’deki gerçek konuma erişebilir.

Bilgisayar ilk açıldığında bellek erişimleri doğrudan fiziksel RAM’e gider. Çok geçmeden işletim sistemi bu çeviri sözlüğünü kurar ve CPU’ya MMU üzerinden çalışmaya başlamasını söyler.
Her process’in sanal bellek alanı, farklı amaçlara hizmet eden bölgelere ayrılır. Aşağıdaki diyagram tipik bir process’in bellek düzenini gösteriyor: en altta text (program kodu) ve data (global/sabit değişkenler) segmentleri bulunur; yukarı doğru heap dinamik olarak büyür (malloc/mmap ile); en üstte ise stack yerel değişkenler ve fonksiyon çağrıları için kullanılır. Stack ve heap birbirine doğru büyür ama asla çakışmaz — aralarında geniş bir sanal adres boşluğu vardır. Kernel allocator, heap için mmap veya sbrk syscall’larını kullanarak sanal sayfaları fiziksel RAM’e eşler.

Kaynak: Silberschatz, Galvin, Gagne — Operating System Concepts, Ch. 3 — Processes, Slide 6.
Bu sözlüğe aslında page table denir; her bellek erişimini çeviren mekanizmanın adına da paging denir. Page table içindeki girdiler PTE (Page Table Entry — Sayfa Tablosu Girdisi) olarak adlandırılır ve her biri sanal bellek alanındaki belirli bir parçanın RAM’de nereye karşılık geldiğini söyler. Bu parçalar sabit boyuttadır ve boyut mimariye göre değişir. x86-64’ün varsayılan page boyutu 4 KiB’dir; yani her PTE, 4096 baytlık bir blok için eşleme tutar. Page = belleğin 4 KiB’lık bloğu; PTE = page table girdisi; Page Frame = fiziksel RAM’deki karşılık.
Başka bir deyişle, 4 KiB paging kullanıldığında bir adresin en düşük 12 biti MMU çevirisinden önce de sonra da aynı kalır. Bunun sebebi basit: 4096 baytlık bir page içindeki konumu göstermek için 12 bit gerekir.
x86-64 ayrıca işletim sistemlerinin 2 MiB veya 1 GiB gibi daha büyük page boyutlarını etkinleştirmesine de izin verir. Bu, adres çevirisini hızlandırabilir ama bellek parçalanmasını ve israfı artırabilir. Page ne kadar büyükse, adresin MMU tarafından çevrilmesi gereken kısmı o kadar küçülür.

Bir Adresi Bit Bit Sökelim
Yukarıdaki şema fikri veriyor ama asıl iş sayılarda. Somut bir adres alalım ve gerçekten parçalayalım. x86-64’ün 4 seviyeli düzeninde bir sanal adresin 48 biti kullanılır ve şu şekilde bölünür:
| Bit aralığı | Genişlik | Adı | Ne yapar |
|---|---|---|---|
| 47-39 | 9 bit | PML4 indeksi | En üst tabloda hangi girdi? |
| 38-30 | 9 bit | PDPT indeksi | İkinci seviyede hangi girdi? |
| 29-21 | 9 bit | PD indeksi | Üçüncü seviyede hangi girdi? |
| 20-12 | 9 bit | PT indeksi | Son tabloda hangi girdi? |
| 11-0 | 12 bit | Offset | Bulunan sayfanın içinde kaçıncı bayt? |
Toplam: 9 + 9 + 9 + 9 + 12 = 48 bit.
Şimdi asıl güzel soru: neden 9 bit? Rastgele seçilmiş bir sayı değil, kendi kendini doğuran bir tasarım.
9 bit ile 2⁹ = 512 farklı girdiyi adresleyebilirsin. Her page table girdisi 8 bayt tutar. 512 × 8 = 4096 bayt — yani tam olarak bir sayfa. Sonuç şu: her page table’ın kendisi tam olarak bir sayfaya sığar. Kernel, page table’ları da sıradan sayfalar gibi tahsis edebilir, sayfalayabilir, hatta gerekirse diske atabilir. Sayfa boyutu tablo boyutunu, tablo boyutu da indeks genişliğini belirliyor; üçü birbirine kilitlenmiş durumda.
Offset’in 12 bit olması da aynı hesabın diğer ucu: 2¹² = 4096, bir sayfanın içindeki her baytı adreslemek için tam olarak yeterli. Bu yüzden bir adresin en düşük 12 biti çeviriden hiç etkilenmez; MMU yalnızca üstteki 36 biti çevirir, alttaki 12 bit olduğu gibi sonuca yapışır.
Yürüyüşü Adım Adım İzleyelim
Peki MMU bu beş parçayla ne yapıyor? İşleme page table walk deniyor ve gerçekten bir yürüyüş.
Bir kütüphanede kitap arıyormuşsun gibi düşün. Önce girişteki kat planına bakarsın, doğru katı bulursun. O katta koridor levhasına bakarsın, doğru koridoru bulursun. Koridorda raf numarasına bakarsın. Rafta kitabın sırasını bulursun. Ve nihayet kitabı açıp doğru sayfaya gidersin. Beş bakış, tek kitap.
MMU’nun yürüyüşü birebir aynı:
- CR3’ten başla. CPU’da
CR3adında özel bir register vardır ve içinde o anki process’in en üst tablosunun (PML4) fiziksel adresi durur. Yürüyüşün başlangıç noktası burasıdır. - Bit 47-39 ile PML4’ü indeksle. Çıkan girdi, bir sonraki tablonun (PDPT) fiziksel adresini verir.
- Bit 38-30 ile PDPT’yi indeksle. Çıkan girdi PD’nin adresini verir.
- Bit 29-21 ile PD’yi indeksle. Çıkan girdi PT’nin adresini verir.
- Bit 20-12 ile PT’yi indeksle. Çıkan girdi nihayet aradığın fiziksel sayfa çerçevesinin adresini verir.
- Offset’i ekle. Çerçeve adresinin sonuna, hiç değişmemiş olan 12 bitlik offset eklenir. Fiziksel adres hazır.
Buradaki maliyeti fark ettin mi? Bu tabloların hepsi RAM’de duruyor. Yani tek bir bayt okumak için önce dört ayrı RAM erişimi yapman gerekiyor; asıl veriyle birlikte toplam beş. Her bellek erişimini beşe katlayan bir sistem hiçbir işe yaramazdı — birazdan geleceğimiz TLB’nin var olma sebebi tam olarak bu.
CR3 nedir? Tek cümleyle: process’in adres defterinin kapak sayfasının adresi. 7. bölümde context switch sırasında “CPU’ya farklı bir page table kullanmasını söylemek” derken kastettiğim şey buydu — kernel yalnızca bu tek register’ı değiştirir ve process’in gördüğü bütün bellek haritası bir anda değişir.
Çevirinin tamamını tek bir akış hâlinde izlemek istersen:
- Program bir adres ister. Kod sanal bir adrese erişir. Bu adres process'e özeldir; fiziksel RAM'de nerede olduğunu program bilmez.
- Adres ikiye ayrılır. Üst bitler hangi sayfaya ait olduğunu, alt 12 bit ise o sayfa içindeki konumu (offset) söyler.
- TLB'ye bakılır. Bu çeviri yakın zamanda yapıldıysa TLB'de hazırdır ve iş burada biter. Isabet edilemezse page table yürünür.
- Page table yürünür. MMU, sayfa numarasını kullanarak tabloyu okur ve karşılık gelen fiziksel çerçeveyi bulur.
- Fiziksel adres kurulur. Bulunan çerçeve numarasının sonuna, hiç değişmemiş olan offset eklenir. Erişim artık gerçek RAM üzerinde yapılır.
Page table’ın kendisi de RAM’de tutulur. Milyonlarca girdi içerebilse bile her bir girdi yalnızca birkaç bayt civarında olduğundan, page table tek başına korkunç boyutlara ulaşmaz.
Boot sırasında paging’i etkinleştirmek için kernel önce RAM’de page table’ı kurar. Ardından page table’ın başlangıç adresini, page table base register (PTBR) denen register’a yazar. Son adımda da tüm bellek erişimlerinin MMU üzerinden çevrilmesi için paging’i etkinleştirir. x86-64’te bu yapı büyük ölçüde CR3 ve ilgili kontrol bitleri üzerinden yönetilir.
Paging’in asıl büyüsü, bilgisayar çalışırken page table’ın değiştirilebilmesidir. Her process’in izole bir bellek alanına sahip olması tam da böyle mümkün olur. İşletim sistemi context switch yaparken yaptığı kritik işlerden biri, sanal bellek alanını fiziksel bellekte başka bir yere yeniden eşlemektir. Diyelim iki process var: A process’inin kodu ve verileri 0x0000000000400000 adresinden erişiliyor olabilir; B process’i de kendi kodunu ve verisini aynı sanal adresten görüyor olabilir. Bu iki process aslında aynı adres aralığı için kavga etmez, çünkü bu sanal adresler fiziksel bellekte farklı yerlere çözülür. Kernel, process değişiminde bu eşlemeyi değiştirir.

Bir not: lanetli bir ELF gerçeği
Belirli koşullarda
binfmt_elf, belleğin ilk page’ini sıfırlarla eşlemek zorunda kalır. ELF’yi destekleyen ilk sistemlerden biri olan 1988 tarihli UNIX System V Release 4.0 (SVr4) için yazılmış bazı programlar, null pointer’ın okunabilir olmasına dayanır. Ve bir şekilde bazı programlar hâlâ bu davranışı bekliyor.Görünüşe göre bunu uygulayan Linux kernel geliştiricisi pek de mutlu değildi:
“Bunun nedenini soruyorsunuz??? Çünkü SVr4, page 0’ı salt okunur şekilde eşliyor ve bazı uygulamalar buna ‘bağımlı’. Bunları yeniden derleme şansımız olmadığı için SVr4 davranışını taklit ediyoruz. İç çek.”
Evet. İç çek.
Paging ile Güvenlik
Paging’in sağladığı process izolasyonu yalnızca kod ergonomisini iyileştirmez; aynı zamanda güçlü bir güvenlik katmanı oluşturur. Process’lerin birbirinin belleğine erişememesi, kitabın başındaki önemli sorulardan birini cevaplar:
Programlar doğrudan CPU üzerinde çalışıyorsa ve CPU doğrudan RAM’e erişebiliyorsa, neden başka process’lerin belleğine ya da aman kernel belleğine erişemiyorlar?
Bunu sanki haftalar önce sormuşuz gibi geliyor, değil mi?
Peki kernel belleği ne olacak? Öncelikle kernel’ın, çalışan tüm process’leri ve page table’ın kendisini takip etmek için kendi verilerine ihtiyacı var. Bir hardware interrupt, software interrupt ya da syscall CPU’yu kernel mode’a geçirdiği anda, kernel kodunun bu belleğe erişebiliyor olması gerekir.
Linux’un yaklaşımı, sanal bellek alanının üst yarısını kalıcı olarak kernel’a ayırmaktır; bu yüzden Linux için higher-half kernel ifadesi kullanılır. Windows da benzer bir yaklaşım izler. macOS tarafı ise Mach mirası yüzünden belirgin biçimde daha karmaşık; temel fikir aynı olsa da katman sayısı fazla.

User-space process’lerin kernel belleğini okuyabilmesi ya da yazabilmesi çok kötü olurdu; bu yüzden paging ikinci bir güvenlik katmanı daha sağlar: her page için izin bayrakları tutulur. Bir bayrak, page’in yazılabilir mi yoksa yalnızca okunabilir mi olduğunu söyler. Başka bir bayrak ise bu page’e yalnızca kernel mode’dan erişilebileceğini belirtir. Kernel space’in tamamı, işte bu izinler sayesinde user-space programları için fiilen erişilemez hâle gelir. Teknik olarak bazı sistemlerde eşleme vardır, ama izin yoktur. NX (No-eXecute) biti, belirli bellek sayfalarında kod çalıştırılmasını engeller — W^X (write XOR execute) güvenlik politikasının temelidir.
DerinleşmeMeltdown sonrası: kernel sayfa tablosu izolasyonu
Eskiden performans için kernel adreslerinin user process page table’larında görünmesi yaygındı; izin bitleri user mode erişimini engelliyordu. Meltdown gibi donanım açıklarından sonra Linux tarafında KPTI/PTI gibi tekniklerle user ve kernel page table’ları daha sert ayrılabildi. Ana fikir değişmiyor: user programı kernel belleğini normal yollarla okuyamaz; sadece implementasyon ayrıntısı ve performans maliyeti değişiyor.

Bellek güvenliğinin bir diğer temel taşı da ASLR’dir (Address Space Layout Randomization). Stack, heap ve kütüphanelerin başlangıç adresleri her çalıştırmada rastgele kaydırılır; böylece bir saldırganın bellekteki belirli adresleri tahmin etmesi zorlaşır.
Page table’ın kendisi de aslında kernel belleğinde durur. Timer chip bir hardware interrupt tetikleyip context switch başlattığında, CPU ayrıcalık seviyesini kernel mode’a çıkarır ve Linux kernel koduna atlar. Kernel mode’da olduğu için CPU artık korumalı kernel bellek bölgesine erişebilir. Kernel, page table’ı güncelleyip sanal belleğin alt yarısını yeni process için yeniden eşler. User mode’a geri dönüldüğünde bu erişim tekrar kapanır.
Kısacası, neredeyse her bellek erişimi MMU’dan geçer. 2. bölümde kernel’ın hazırladığı o numaralı olay listesindeki adresler bile fiziksel adres değil, kernel’ın sanal adres alanındaki adreslerdir.
Burada bir nefes alalım.
Peki, ne öğrendik?
- Bir program hiçbir zaman fiziksel RAM’i görmez. Gördüğü her adres sanaldır ve MMU tarafından çevrilir.
- Bellek sabit boyutlu sayfalara bölünür; RAM tarafındaki karşılıklarına çerçeve denir. İkisi arasındaki eşleme sayfa tablosunda durur.
- İki process aynı sanal adresi kullanabilir, çünkü o adresler farklı çerçevelere çözülür. Process izolasyonunun temeli budur.
- Her eşlemenin izin bitleri vardır: yazılabilir mi, çalıştırılabilir mi, user mode’dan erişilebilir mi.
- Linux, sanal adres alanının üst yarısını kalıcı olarak kernel’a ayırır (higher-half kernel). User programı oraya eşlenmiş olsa bile izin bitleri yüzünden erişemez.
- ASLR, adresleri her çalıştırmada kaydırarak tahmin edilebilirliği kırar.
Şimdiye kadar çeviriyi bir kutu gibi anlattım: sanal adres giriyor, fiziksel adres çıkıyor. Sıradaki bölümde o kutuyu açacağız — ve içeride tek bir tablo olmadığını göreceğiz.
14. bölüme devam et: Adres Çevirisi ve TLB