Bölüm 2:Temeller
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
Bu kitabı yazarken beni tekrar tekrar şaşırtan şey, bilgisayarların ne kadar basit olduğuydu. İnsanın aklı bilgisayarların içinde, gerçekte olduğundan daha karmaşık ve daha soyut bir yapı arıyor; öyle bir yapı yok. Devam etmeden önce aklına kazımanı istediğim tek bir şey varsa o da şu: basit görünen birçok şey gerçekten de basittir. Bu sadelik hem çok güzel hem de zaman zaman epey lanetlidir.
Bilgisayarının özünde nasıl çalıştığına dair temel resimle başlayalım.
Bu bölümde neyi çözüyoruz?
- CPU’nun talimatları nasıl okuyup yürüttüğünü göreceğiz.
- RAM, disk, register ve instruction pointer arasındaki farkı netleştireceğiz.
- “Program” ile “process” arasındaki farkı yerine oturtacağız.
- Bilgisayar açıldığında ilk talimatın nereden geldiğini çözeceğiz.
Bilgisayarlar Nasıl Tasarlanır?
Bir bilgisayarın merkezi işlem birimi (CPU), bütün hesaplamalardan sorumludur. İşin patronu odur. Makine açıldığı anda çalışmaya başlar ve talimat üstüne talimat yürüterek durmadan devam eder.
Bir programı başlatmadan önce o programın dosyası kalıcı depoda durur: sabit diskte ya da SSD’de. Kernel onu çalıştırmaya karar verdiğinde dosyanın gerekli parçalarını RAM’e taşır. Şimdilik temel resmi şöyle tut: disk kalıcı depo, RAM çalışma masası, CPU da masadaki talimatları uygulayan motor.
Bu resmin ileride iki yerinden inceleceğini şimdiden söyleyeyim. Birincisi, CPU talimatları çoğu zaman RAM’e hiç gitmeden, ona çok daha yakın duran küçük ara depolardan alır. İkincisi, bir programın gördüğü adresler ile RAM’deki gerçek adresler aynı şey değildir. İkisini de yeri geldiğinde ayrı ayrı açacağız; şimdilik basit resim yeterli.
Bu fikrin ne kadar eski olduğunu görmek için bir örnek vereyim: Intel’in Kasım 1971’de duyurduğu Intel 4004. Ne kadar küçük olduğunu bir rakamla anlatayım — 4004 aynı anda yalnızca 4 bit işleyebiliyordu, yani senin bugün kullandığın işlemcinin tek hamlede çevirdiği verinin on altıda birini. Ama içindeki fikir bugünküyle birebir aynıydı: programlanabilir bir çip, talimatları sırayla yürütür. Bu kitapta anlatacağım her şey o tek fikrin üstüne kuruldu.
CPU’nun yürüttüğü “talimatlar” sadece ikili veridir: önce hangi talimatın çalıştırıldığını belirten bir ya da birkaç baytlık opcode gelir, ardından o talimatın ihtiyaç duyduğu veri yer alır. Makine kodu dediğimiz şey, aslında bu ikili talimatların art arda dizilmesinden ibarettir. Assembly, insanların ham bitlere göre çok daha rahat okuyup yazabildiği bir gösterimdir; ama sonuçta her zaman CPU’nun anlayacağı ikili biçime derlenir.

Bir not: Talimatlar makine kodunda her zaman yukarıdaki örnekteki gibi 1:1 görünmez. Örneğin
add eax, 512,05 00 02 00 00şeklinde kodlanır.İlk bayt (
05), özellikle EAX register’ına 32-bit bir sayı ekleme işlemini ifade eden opcode’dur. Kalan baytlar ise little-endian sıra ile yazılmış 512 (0x200) değeridir.Defuse Security, assembly ile makine kodu arasındaki çeviriyle oynamak için yararlı bir araç hazırlamış.
RAM, bilgisayarının ana belleğidir; çalışan programların kullandığı verilerin tutulduğu büyük ve genel amaçlı alan budur. Buna program kodunun kendisi de, işletim sisteminin kernel kodu da dahildir. CPU’nun yürüteceği talimatların RAM’de erişilebilir olması gerekir; yalnızca diskte duran bir dosya kendi kendine çalışmaz.
Burada, kitabın ilerleyen bölümlerinde birkaç kez karşımıza çıkacak bir ayrım var: RAM uçucudur, disk kalıcıdır.
Sebebi fizikte. RAM’in her biti, minik bir kondansatörde tutulan elektrik yüküdür ve o yük kendiliğinden boşalır — o kadar hızlı boşalır ki, makine çalışırken bile her hücrenin saniyede binlerce kez yeniden doldurulması gerekir. Buna refresh denir ve RAM’in “D”si (Dynamic) tam olarak buradan gelir. Elektrik kesildiği anda refresh durur, yükler kaçar, veri gider. Disk ise veriyi manyetik yönelim ya da hapsedilmiş elektron olarak saklar; elektriğe ihtiyaç duymaz.
Gündelik hayatta bunun iki tanıdık sonucu var. Birincisi, kaydetmediğin dosyanın elektrik kesintisinde uçup gitmesi. İkincisi, her açılışta bilgisayarın “yeniden yükleniyor” olması — çünkü RAM’de gerçekten hiçbir şey kalmamıştır ve her şeyin diskten baştan taşınması gerekir. Bölümün ilerleyen kısmında, “peki RAM boşsa ilk talimat nereden geliyor?” sorusuna da döneceğiz.
CPU, sıradaki talimatın nerede olduğunu takip etmek için instruction pointer adında bir gösterge tutar. Burada karıştırması çok kolay bir ayrım var, o yüzden baştan netleştirelim:
Göstergenin kendisi CPU’nun içindedir; gösterdiği adres RAM’dedir. İkisi aynı yerde değil. Bir kitap okurken parmağını satırların üzerinde gezdirmen gibi düşün — parmak sende, satırlar kitapta. CPU bir talimatı bitirince parmağını bir sonrakine kaydırır ve aynı işi baştan yapar. Buna fetch-execute cycle denir.
Bu ayrım küçük bir ayrıntı gibi görünüyor ama kitabın geri kalanının yarısı buradan çıkıyor. Göstergenin CPU’nun içinde olması, ona erişmenin bedava sayılacak kadar ucuz olması demek. Gösterdiği yerin RAM’de olması ise, oraya gitmenin pahalı olması demek. Aradaki bu uçurum, 5. bölümde göreceğimiz bütün bellek hiyerarşisinin ve 8. bölümdeki hız hilelerinin var olma sebebidir.

Bir talimat yürütüldükten sonra instruction pointer, RAM’de o talimatın hemen sonrasına ilerler; böylece sıradaki talimatı işaret eder. Kodun çalışması dediğimiz şey tam olarak budur. Talimatlar bellekte hangi sıradaysa CPU onları o sırayla yürütür. Bazı talimatlar ise instruction pointer’a başka bir adrese atlamasını söyler; böylece dallanma, koşullu mantık ve tekrar kullanılabilir kod mümkün olur.
Döngünün dört adımını tek tek izlemek istersen:
- Getir (fetch). CPU, instruction pointer'ın gösterdiği adresteki talimatı RAM'den okur.
- Çöz (decode). Okunan byte'lar çözülür: hangi işlem, hangi register'lar, hangi sabit?
- Yürüt (execute). İşlem gerçekten yapılır; sonuç bir register'a ya da belleğe yazılır.
- İlerlet. Instruction pointer bir sonraki talimata kayar ve döngü baştan başlar. Bir atlama talimatı bunu başka bir adrese de çevirebilir.
Pratikte CPU çekirdeği ile DRAM arasında L1 → L2 → L3 gibi cache katmanları bulunur. Programcıya görünen basit model “bellekten oku/yaz” olsa da donanım sık kullanılan veri ve talimatları CPU’ya yakın tutarak gecikmeyi azaltır; bu ayrıntı temel fetch-execute resmini değiştirmez.
Burada, fark etmesi kolay olmayan ama her şeyin üstünde durduğu bir tasarım kararı var: talimatlar ve veriler aynı bellekte duruyor. CPU’nun okuduğu makine kodu ile üzerinde çalıştığı sayılar aynı RAM’de, aynı adres uzayında, aynı byte’lardan yapılmış. Bir programın dosya olarak diskte durabilmesi, kopyalanabilmesi, indirilebilmesi tamamen bu karara bağlı.
DerinleşmeVon Neumann kararı ve bugüne kadar uzanan faturası
Talimat ile veriyi aynı belleğe koymak bir zorunluluk değil, bir tercihti. 1945’te John von Neumann’ın raporuyla anılan bu modele karşı bir alternatif de vardı ve hâlâ var: Harvard mimarisi, talimat belleği ile veri belleğini fiziksel olarak ayırır.
İkisinin sonuçları taban tabana zıt:
- Von Neumann esnektir. Program bir veridir; derleyici üretebilir, indirebilirsin, çalışırken üretebilirsin. JIT derleyiciler, dinamik yükleme ve bu kitabın anlattığı her şey bu esneklikten doğar.
- Harvard hızlı ve güvenlidir. Talimat ile veri ayrı yollardan gittiği için aynı anda ikisine birden erişilebilir; ve bir veri hatası kodun üzerine yazamaz. Mikrodenetleyiciler ve sinyal işlemcilerinde bugün de tercih edilir.
Modern bir x86 ya da ARM çekirdeği ikisinin arasında durur: mimari düzeyde von Neumann’dır (tek adres uzayı), ama çekirdeğin içinde ayrı bir talimat cache’i (L1i) ve ayrı bir veri cache’i (L1d) vardır. Buna modified Harvard denir. Bunun gözlemlenebilir bir sonucu şudur: kendi kodunu çalışırken üreten bir program (bir JIT gibi), yazdığı byte’ların L1d’de olup L1i’de olmadığını hesaba katmak ve talimat cache’ini açıkça geçersiz kılmak zorundadır. Aksi hâlde CPU eski talimatları çalıştırmaya devam eder.
İki büyük fatura daha var:
Von Neumann darboğazı. Talimat ve veri aynı yolu paylaştığı için, işlemcinin ne kadar hızlı çalışabileceğini o tek yolun kapasitesi sınırlar. Terimi 1977’de John Backus ortaya attı ve şu gözleme dayanıyordu: CPU ne kadar hızlanırsa hızlansın, veriyi belleğe götürüp getiren tek bir borudan geçmek zorundadır. Bunun üstüne DRAM’in işlemciler kadar hızlanamaması eklenince aradaki uçurum daha da açıldı. 5. bölümdeki cache hiyerarşisi ve 8. bölümdeki pipeline hilelerinin tamamı, bu iki basıncın etrafından dolaşma çabasıdır.
Kod enjeksiyonu diye bir şeyin var olabilmesi. Bir saldırgan veri alanına byte yazıp CPU’yu oraya atlatabiliyorsa, bunun mümkün olmasının sebebi verinin de talimat olabilmesidir. 15. bölümde göreceğimiz NX biti ve W^X politikası, tam olarak Harvard’ın ayrımını von Neumann belleğine sonradan geri takma çabasıdır. Seksen yıl sonra hâlâ o kararın bedelini ödüyoruz.
Bu instruction pointer bir register içinde tutulur. Register’lar, CPU’nun çok hızlı okuyup yazabildiği küçük depolama alanlarıdır — ve “küçük” derken gerçekten küçük kastediyorum. Birkaç sayıyla bakalım:
- Sayı ve boyut. x86-64’te 16 genel amaçlı register vardır ve her biri 64 bit, yani 8 bayt tutar. Toplamı 128 bayt. Aynı makinede RAM gigabaytlarla ölçülüyor — arada milyarda bir mertebesinde bir oran var.
- Neden bu kadar hızlı? Çünkü register CPU’nun içinde, hesap yapan devrelerin hemen yanındadır; aynı silikon parçasının üzerinde. RAM ise anakart üzerinde ayrı bir yongadır ve oraya gidip gelmek, elektrik sinyalinin fiziksel olarak yol katetmesi demektir. Register erişimi bir saat çevrimi sürer, RAM erişimi yüzlerce.
- İsimler neden bu kadar tuhaf?
rax,eaxveaxüç ayrı register değil; aynı register’ın farklı boylardaki görünümleridir.rax64 bitin tamamı,eaxalttaki 32 bit,axalttaki 16 bit,alise en alttaki 8 bit. Bu iç içe geçmişlik geriye uyumluluktan geliyor: 16-bit çağında yazılmış kod, 64-bit bir işlemcide hâlâ çalışsın diye.
Yukarıdaki assembly örneğinde gördüğün eax ve ebx gibi genel amaçlı register’lara makine kodundan doğrudan erişilebilir.
Bazı register’lar ise CPU tarafından daha içsel amaçlarla kullanılır; yine de çoğu zaman özel talimatlarla okunabilir veya güncellenebilirler. Instruction pointer buna iyi bir örnektir: doğrudan okunmaz ama örneğin bir jump talimatı ile değiştirilebilir.
İşlemciler Naiftir
Asıl soruya dönelim: Bilgisayarında çalıştırılabilir bir programı başlattığında ne oluyor? Önce onu çalıştırmaya hazırlamak için bir sürü kurulum yapılır, bunların hepsini birazdan göreceğiz, ama sonunda dosyanın içindeki makine kodu RAM’e yerleştirilir. Ardından işletim sistemi CPU’ya instruction pointer’ı o konuma ayarlamasını söyler. CPU da fetch-execute cycle’ı normal şekilde sürdürür ve program çalışmaya başlar.
Burada bir saniye durmanı istiyorum. Bu satırları okumak için kullandığın program da tam olarak böyle çalışıyor. Tarayıcının makine kodu şu anda RAM’de duruyor; CPU’n instruction pointer’ı o kodun üzerinde bir adım bir adım ilerletiyor ve okuduğun cümle, saniyede milyarlarca kez tekrarlanan bu tek döngünün çıktısı. Sihir yok; sadece çok hızlı tekrarlanan çok basit bir iş var.

Burada bir terimi de yerine oturtalım, çünkü kitabın geri kalanında sürekli kullanacağız. Program, diskte duran dosyadır; kimse çalıştırmadıkça hiçbir şey yapmaz. Process ise o dosyanın çalışan hâlidir: kendi belleği, kendi açık dosyaları ve kendi instruction pointer değeri olan canlı bir örnek. Aynı programı iki kez açtığında ortada tek bir dosya ama iki process olur.
Şimdi hükmü verebiliriz: CPU’ların dünya görüşü inanılmaz derecede dardır. Yalnızca o andaki instruction pointer’ı ve biraz da iç durumu görürler. Process tamamen bir işletim sistemi soyutlamasıdır; CPU’nun doğal olarak bildiği ya da takip ettiği bir kavram değildir.
process diye bir şey yok; işletim sistemi geliştiricileri daha çok bilgisayar satılsın diye uydurulmuş bir hikâye bu
Bu kadar dar bir dünya görüşü, hemen üç soru doğuruyor. Kitabın geri kalanı büyük ölçüde bunların cevabı:
- CPU çoklu işlemeyi bilmiyorsa ve sadece talimatları sırayla yürütüyorsa, neden tek bir programın içinde sıkışıp kalmıyor? Birden fazla program aynı anda nasıl çalışabiliyor?
- Programlar doğrudan CPU üzerinde çalışıyorsa ve CPU doğrudan RAM’e erişebiliyorsa, neden başka process’lerin belleğine, hele hele kernel belleğine erişemiyorlar?
- Madem konu açıldı: Her process’in istediği talimatı çalıştırıp bilgisayarına istediğini yapmasını engelleyen şey ne? Ve syscall denen şey tam olarak nedir?
Bellek meselesi kendi bölümünü hak ediyor; onu Bölüm 13’da ele alacağız. Kısa versiyon şu: user-space programı kendi sanal adres alanına erişir; fiziksel RAM’e, kernel belleğine ve ayrıcalıklı donanım giriş/çıkışına doğrudan el uzatamaz. Şimdilik bellek koruması yokmuş ve bilgisayar aynı anda sadece tek bir process çalıştırıyormuş gibi sadeleştirelim. Bu varsayımların ikisini de birazdan bozacağız.
Şimdi ilk derin dalışımızın zamanı: syscall’lar ve ring’ler.
Bu arada kernel nedir?
Bilgisayarındaki macOS, Windows ya da Linux gibi işletim sistemi, temel işlerin yürümesini sağlayan bütün yazılım katmanıdır. “Temel işler” çok muğlak bir ifade ve “işletim sistemi” terimi de kime sorduğuna göre değişir; kimi insanlar buna varsayılan uygulamaları, font’ları ve ikonları da katar.
Ama kernel, işletim sisteminin çekirdeğidir. Bilgisayarı açtığında çalışan ilk şey kernel değildir: önce anakart üzerindeki firmware (BIOS ya da UEFI) donanımı ayağa kaldırır, sonra bir bootloader (GRUB gibi) kernel’ı diskten bulup RAM’e yükler ve kontrolü ona devreder. O andan sonra makinenin sahibi kernel’dır. Belleğe, çevre birimlerine ve sistem kaynaklarına neredeyse tam erişimi vardır ve user-space programlarını çalıştırmak onun işidir. Kitap boyunca kernel’ın bu erişime nasıl sahip olduğunu, programların neden olmadığını göreceğiz.
Linux tek başına bir kernel’dır; kullanılabilir bir sistem olması için shell’ler, görüntü sunucuları ve başka birçok user-space yazılımına ihtiyaç duyar. macOS’un kernel’ının adı XNU’dur ve Unix benzeridir; modern Windows kernel’ı ise NT Kernel olarak bilinir.
Bu boot zincirini okurken çoğu kişinin aklına çok yerinde bir itiraz gelir, o yüzden hemen cevaplayalım.
DerinleşmeRAM kapanınca siliniyorsa, ilk talimat nereden geliyor?
İtiraz şu: bilgisayarı kapattığında RAM’deki her şey silinir. Bu doğru — hem de tahmin ettiğinden daha sert biçimde doğru. DRAM veriyi minik kapasitörlerde tutar, kapasitörler de saniyenin küçük bir kesrinde kendiliğinden boşalır; makine açıkken bile bu hücrelerin saniyede binlerce kez tazelenmesi gerekir. Güç gidince tazeleme durur, yük kaçar, veri gider. CPU’nun içindeki register’lar için de aynısı geçerli.
O hâlde ortada gerçek bir bilmece var: açılış anında RAM boş, register’lar boş. CPU ilk talimatı nereden okuyor?
Cevap, “bellek adresi” ile “RAM” kelimelerini eş anlamlı kullanmayı bırakmakla başlıyor. CPU’nun ürettiği her adres RAM’e gitmez. CPU’nun tek bir adres uzayı vardır, ama bu uzayın hangi diliminin nereye bağlanacağına chipset karar verir: bir aralık DRAM’e gider, başka bir aralık ekran kartının belleğine, bir başkası ağ kartının kontrol register’larına, bir dilim de anakart üzerindeki küçük bir flash yongasına. Adres uzayını bir binanın kat planı gibi düşün — RAM, o binadaki dairelerden yalnızca biri.
Firmware işte o flash yongasında durur. Anakartın üzerinde, tipik olarak 8-32 MiB kapasiteli, SPI flash denen kalıcı bir yonga vardır ve içeriği güç kesilince silinmez; telefonundaki depolama gibi düşünebilirsin. “BIOS güncellemek” dediğin şey tam olarak bu yongayı yeniden yazmaktır.
Geri kalanı artık düz mantık. CPU’nun reset devresi, güç geldiği anda instruction pointer’ı donanımsal olarak sabit bir değere kurar; x86-64’te bu adres 0xFFFFFFF0, yani 4 GiB sınırının on altı bayt altıdır. CPU o adresi ister, chipset adresi flash yongasına yönlendirir, ilk talimat oradan gelir. CPU açısından olağanüstü hiçbir şey olmadı: her zamanki gibi bir adresten talimat okudu. Olağanüstü olan, o adresin arkasında RAM değil flash olması.
Resmin son parçası da en şaşırtıcı olanı. O ilk anda DRAM henüz kullanılabilir durumda bile değildir: bellek denetleyicisinin takılı çipleri tanıması, zamanlamalarını okuması ve hatları kalibre etmesi gerekir. Bu işleme memory training denir ve onu yapan kod firmware’in kendisidir. Yani firmware, RAM’i hazırlayan koddur — dolayısıyla çalışırken RAM’e güvenemez. Peki değişkenlerini ve çağrı yığınını nerede tutar? Cevap zarif: L1/L2 cache’i geçici olarak sıradan bir bellek gibi davranmaya zorlar. Bu numaranın adı Cache-As-RAM (CAR). Bilgisayarın hayatının ilk milisaniyelerinde bütün dünyası, birkaç yüz kilobaytlık cache’ten ibarettir.
Zinciri tek satırda toplarsak: reset vector → flash’taki firmware → memory training → RAM artık kullanılabilir → bootloader diskten RAM’e → kernel RAM’e → kontrol kernel’a.
Bölümü kapatmadan elimizdekini toparlayalım.
Peki, ne öğrendik?
- CPU sonsuz bir fetch-execute cycle içinde çalışır: talimatı oku, yürüt, göstergeyi ilerlet, baştan başla.
- Instruction pointer CPU’nun içindedir, gösterdiği adres RAM’dedir. Aradaki bu uzaklık kitabın geri kalanının yarısını açıklar.
- Register’lar CPU’nun içindeki çok küçük ve çok hızlı depolardır; RAM ile aralarında yüzlerce kat gecikme farkı vardır.
- RAM uçucudur, disk kalıcıdır. Bilgisayarın her açılışta her şeyi baştan yüklemesinin sebebi budur.
- Program diskteki dosyadır, process onun çalışan hâlidir. CPU process diye bir şey bilmez; process tamamen bir işletim sistemi soyutlamasıdır.
Bölümü üç soruyla kapatmıştık. Birincisinin cevabı 7. bölümde bizi bekliyor. Diğer ikisi ise doğrudan bir sonraki bölümün konusu: aynı çip, aynı talimatı neden bazen çalıştırıp bazen reddediyor?
3. bölüme devam et: Kernel, User Mode ve Syscall