Bölüm 4:Mimariler: x86, ARM ve Diğerleri
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
Buraya kadar birkaç kez “x86-64’te şöyle olur”, “ARM tarafında böyle” diye cümleler kurdum ve bu isimlerin ne anlama geldiğini açıklamadan geçtim. Şimdi o borcu ödeyelim.
Bu isimler marka adı değil. Bilgisayarın hangi dili konuştuğunu söylüyorlar — ve o dili bilmeden, bir programın neden bir makinede çalışıp diğerinde çalışmadığını açıklamak mümkün değil.
Bu bölümde neyi çözüyoruz?
- “Talimat kümesi” ile “işlemci modeli” arasındaki farkı netleştireceğiz.
- 32 bit ile 64 bit arasındaki farkın tam olarak neyi ölçtüğünü göreceğiz.
- x86-64 ile ARM’ın nereden geldiğini ve neden ikisinin de var olduğunu anlayacağız.
- Aynı programın neden her makinede çalışmadığını cevaplayacağız.
Talimat Kümesi: Donanımla Yazılım Arasındaki Sözleşme
2. bölümde CPU’nun makine kodu yürüttüğünü, makine kodunun da opcode’lardan oluştuğunu gördük. Şimdi bir soru soralım: 05 00 02 00 00 baytlarının “EAX register’ına 512 ekle” demek olduğuna kim karar verdi?
Cevap bir belge. Her işlemci ailesinin, hangi bayt dizisinin hangi işi yapacağını satır satır tanımlayan bir sözleşmesi vardır. Bu sözleşmenin adı talimat kümesi mimarisi — kısaca ISA (Instruction Set Architecture).
Bir ISA şunları söyler:
- Hangi talimatlar var ve her biri hangi bayt dizisiyle yazılıyor.
- Kaç register var, adları ne, kaç bit genişliğinde.
- Bellek nasıl adreslenir, byte’lar hangi sırayla dizilir.
- Ayrıcalık seviyeleri nasıl çalışır, kernel’a nasıl girilir.
Yani ISA, donanım ile yazılım arasındaki sınırdır. Üstünde kalan her şey — derleyicin, işletim sistemin, tarayıcın — bu sözleşmeye göre yazılır. Altında kalan her şey de bu sözleşmeyi yerine getirmek zorundadır.
Burada karıştırılması çok kolay bir ayrım var, o yüzden altını çizeyim:
ISA, işlemcinin ne yaptığını söyler; mikro-mimari ise nasıl yaptığını.
Bir Intel Core i9 ile bir AMD Ryzen aynı ISA’yı konuşur: ikisine de aynı .exe dosyasını verirsin, ikisi de çalıştırır. Ama içeride bambaşka makinelerdir — farklı pipeline derinliği, farklı cache düzeni, farklı dal tahmincisi. 8. bölümde anlattığım hilelerin hepsi mikro-mimari tarafındadır ve üreticiden üreticiye, hatta nesilden nesile değişir.
Bu ayrımın günlük hayattaki karşılığı şu: yeni bir işlemci aldığında programlarını yeniden derlemen gerekmez. Çünkü değişen mikro-mimaridir, sözleşme aynı kalır. Sözleşme değiştiğindeyse — ARM’a geçen bir Mac’te olduğu gibi — her şeyin yeniden derlenmesi gerekir.
“32 bit” ve “64 bit” Tam Olarak Neyi Ölçer?
Bu ikili neredeyse her yerde karşına çıkıyor ama neyi ölçtüğü çoğu zaman havada kalıyor. Aslında tek bir şeyi değil, birbirine bağlı iki şeyi birden ölçüyor.
Birincisi: register genişliği. 64-bit bir işlemcide genel amaçlı register’lar 64 bit, yani 8 bayt tutar. 32-bit bir işlemcide 4 bayt. Bu, CPU’nun tek hamlede işleyebildiği sayının büyüklüğü demek. 32 bitle en fazla 4.294.967.295’e kadar sayabilirsin; daha büyük sayılarla çalışmak için işi parçalara bölmek gerekir.
İkincisi, ve asıl önemlisi: adres genişliği. Bir bellek adresi de bir sayıdır ve o sayı register’a sığmak zorundadır. 32 bitlik bir adresle en fazla 2³² farklı bayt gösterebilirsin — yani 4 GiB. Kaç çubuk RAM taktığının önemi yok; adresi yazacak yerin yoksa oraya erişemezsin.
2000’lerin başında 32-bit Windows’un 4 GB’ın üstünü göremiyor olmasının sebebi tam olarak buydu. Sorun RAM’de değil, adresi tutan sayının genişliğindeydi. 64 bite geçince tavan 2⁶⁴’e, yani 16 exbibyte’a çıktı — öngörülebilir gelecek için fazlasıyla yeterli bir sayı.
Pratikte x86-64 işlemciler bu 64 bitin tamamını kullanmaz; sanal adreslerin yalnızca alttaki 48 biti anlamlıdır (yeni nesillerde 57). Sebebini 14. bölümde göreceğiz: adresin her biti bir sayfa tablosu seviyesi demek ve her seviye çeviriye ek maliyet bindiriyor. 48 bit bile 256 tebibyte eder; kimse bu tavana yaklaşmıyor.
Bunun bir de ters yönlü bedeli var: 64-bit bir programda her işaretçi 4 bayt yerine 8 bayt yer kaplar. İşaretçi bakımından yoğun yapılarda (bağlı listeler, ağaçlar) bu, aynı verinin cache’te iki kat yer tutması demektir. Aynı program 64 bitte bazen daha yavaş çalışabilir; nedeni 6. bölümde konuştuğumuz cache basıncıdır.
x86’nın Hikâyesi: Bir Geriye Uyumluluk Anıtı
Bugün masaüstü ve sunucu dünyasına hâkim olan ISA’nın adı x86-64. Bu tuhaf ismin nereden geldiğini anlamak için elli yıl geriye gitmek gerekiyor.
2. bölümde andığım Intel 4004 ile başlayan zincir 1978’de 8086’ya ulaştı: 16-bit bir işlemci. Ondan sonra gelen modeller 80186, 80286, 80386, 80486 diye devam etti. Sektör bu aileyi kısaca “86 ile biten işlemciler”, yani x86 diye anmaya başladı. 80386 ile birlikte aile 32 bite geçti ve bu 32-bit sürüm IA-32 adını aldı.
Sonra beşinci nesil geldi ve Intel bir sorunla karşılaştı: sayılar ticari marka olarak tescil edilemiyordu, yani rakipleri de “586” diye çip satabiliyordu. Çözüm, sayı yerine bir isim koymak oldu: Pentium. Yani Pentium yeni bir mimari değil, x86 ailesinin beşinci neslinin pazarlama adıdır. Sonraki Core, Core i7, Ryzen gibi isimler de aynı şey — hepsi aynı sözleşmeyi konuşan farklı mikro-mimarilerdir.
64 bite geçiş ise Intel’in beklemediği bir yerden geldi. Intel, geleceği tamamen yeni ve x86 ile uyumsuz bir mimaride (Itanium) gördü. AMD ise farklı düşündü: mevcut x86’yı 64 bite genişletip eski programların çalışmaya devam etmesini sağladı. Piyasa, elindeki yazılımı çöpe atmak istemedi. AMD’nin tasarımı fiilen standart oldu; Intel de onu benimsemek zorunda kaldı.
İşte bu yüzden bugün kullandığın mimarinin adı x86-64 (ya da AMD64, ya da Intel 64 — üçü de aynı şey).
DerinleşmeModern bir işlemci hâlâ 1978 modunda açılıyor
Geriye uyumluluğun bedeli soyut bir şey değil; her açılışta ödüyorsun.
Bir x86-64 işlemci güç geldiğinde 64-bit modda başlamaz. Real mode denen, 8086’nın 1978’deki modunda başlar: 16-bit, koruma yok, 1 MiB adres sınırı. Firmware önce onu 32-bit korumalı moda, sonra 64-bit “long mode”a taşımak zorundadır. Yani modern bir sunucu, kırk yıl önceki bir işlemcinin taklidini yaparak uyanır ve ancak birkaç adım sonra kendisi olur.
Aynı yükün başka izleri de var: değişken uzunluklu talimat kodlaması (bir talimat 1 ile 15 bayt arasında olabilir), hâlâ duran ama kimsenin kullanmadığı segment register’ları, x87 gibi terk edilmiş ondalık sayı birimleri.
Buna karşılık aynı yük, x86’yı bu kadar başarılı yapan şeyin ta kendisi. 1990’larda yazılmış bir programı bugünkü bir işlemcide çalıştırabiliyor olmak, sektörün kırk yıllık yatırımını korumak demekti. Temizlik uğruna bu bağı kesmeyi deneyen mimariler — Itanium bunlardan biriydi — genellikle kaybetti.
ARM: Aynı Soruya Verilen Başka Bir Cevap
x86 masaüstünde kazanırken bambaşka bir yerde, tamamen farklı bir baskı altında ikinci bir aile büyüyordu.
ARM, 1985’te İngiliz Acorn Computers’ta doğdu. Açılımı Acorn RISC Machine’di. Hedefi en hızlı işlemciyi yapmak değil, işi en az güçle yapmaktı — çünkü ucuz ev bilgisayarlarında ne büyük bir soğutucu ne de büyük bir güç bütçesi vardı.
Bu kısıt, cep telefonu çağı geldiğinde altın değerinde çıktı. Pilden çalışan bir cihazda önemli olan tepe hız değil, watt başına iş. Bugün elindeki telefonun içinde neredeyse kesinlikle bir ARM çekirdeği var.
ARM’ın ikinci sıra dışı yanı iş modeli: ARM şirketi çip üretmez. Tasarımı lisanslar. Apple, Qualcomm, Samsung, Amazon ve daha onlarcası bu lisansı alıp kendi çipini üretir. Bu yüzden “ARM işlemci” dediğimizde tek bir üründen değil, aynı sözleşmeyi konuşan çok geniş bir aileden söz ediyoruz.
64-bit sürümün adı AArch64 (ya da ARM64). 2011’de duyuruldu ve bugün telefonlardan Apple Silicon Mac’lere, oradan bulut sunucularına kadar her yerde.
Apple’ın 2020’de Mac’leri Intel’den kendi ARM çiplerine taşıması, bu bölümün en somut örneği. Sözleşme değişti, dolayısıyla bütün programların yeniden derlenmesi gerekti. Apple bu geçişi Rosetta 2 ile yumuşattı: x86-64 programlarını kurulum sırasında AArch64 makine koduna çeviriyor. Çeviri işe yarıyor ama bedava değil; yerel derlenmiş bir uygulama her zaman daha hızlı.
CISC ve RISC: Kavga Neydi, Bugün Ne Kaldı?
Bu iki kısaltma seksenlerin büyük tartışmasıdır ve isimlerinden tahmin edebileceğinden daha az şey ifade ederler.
CISC (Complex Instruction Set Computer) yaklaşımı şuydu: talimatlar zengin ve güçlü olsun, tek bir talimat çok iş yapsın. Bellek hem pahalı hem küçükken, programı kısa tutmak gerçek bir kazançtı. x86 bu okuldan.
RISC (Reduced Instruction Set Computer) tersini savundu: talimatlar az sayıda, sabit uzunlukta ve basit olsun; karmaşık işi derleyici birkaç basit talimatla kursun. Böylece donanım sadeleşir, hızlanır ve daha az güç harcar. ARM, RISC-V ve MIPS bu okuldan.
Aradaki en görünür fark talimat uzunluğu. AArch64’te her talimat tam olarak 4 bayttır; işlemci sıradaki talimatın nerede bittiğini bilmek için onu çözmek zorunda değildir. x86-64’te ise 1 ile 15 bayt arasında değişir; nerede bittiğini anlamak için başından okumaya başlaman gerekir. 8. bölümde gördüğümüz “aynı anda birden fazla talimatı çözme” işi, sabit uzunlukta çok daha kolaydır.
Peki kim kazandı? Dürüst cevap: ikisi de, çünkü tartışma ortada kalktı.
Modern bir x86-64 işlemci dışarıdan CISC görünür ama içeride RISC gibi çalışır. Aldığı karmaşık talimatı, mikro-op denen küçük ve düzenli parçalara böler; pipeline’ın geri kalanı bu parçaları yürütür. Yani x86, RISC fikirlerini kendi kabuğunun içine taşıdı. Geriye kalan asıl fark, o çeviriyi yapan decoder’ın maliyeti: sabit uzunluklu bir ISA’da bu iş neredeyse bedavayken, x86’da gerçek bir transistör ve güç bütçesi tüketir.
Bu bölümdeki bir davranış farkını da buraya bağlayabiliriz. 3. bölümde syscall giriş yollarından bahsetmiştik. x86 tarafında bu iş için ayrı ayrı talimatlar eklendi — Intel 32-bit dönem için SYSENTER’ı, AMD 64-bit için SYSCALL’ı tasarladı ve 64-bit dünyada AMD’ninki standart oldu. AArch64 ise böyle özel bir talimat eklemedi; hem syscall’lar hem de software interrupt’lar için tek bir SVC talimatı kullanılır. Bu, iki okulun tavrını tek bir örnekte özetliyor: CISC ihtiyaç gördüğü yere yeni talimat ekler, RISC var olanı yeniden kullanır.
RISC-V: Sözleşmenin Kendisi Açık Olsa?
Son yılların yeni ismi RISC-V. Teknik olarak sıra dışı bir şey vaat etmiyor; sıra dışı olan lisansı. x86 ve ARM’ın aksine RISC-V’yi kullanmak için kimseye para ödemen ya da izin almana gerek yok — ISA açık bir standart.
Bunun pratik sonucu, bir üniversitenin, bir startup’ın ya da bir devletin kendi işlemcisini kimseye sormadan tasarlayabilmesi. Şu an ağırlıklı olarak gömülü sistemlerde ve araştırma tarafında; masaüstünde ciddi bir varlığı yok. Ama bu bölümün asıl dersini iyi gösteriyor: ISA bir belgedir, ve belgenin kime ait olduğu teknik bir mesele değil, stratejik bir meseledir.
Peki Aynı Program Neden Her Yerde Çalışmıyor?
Artık bu sorunun cevabı elimizde. Derleyici kaynak kodunu makine koduna çevirirken belirli bir ISA’yı hedefler. Ürettiği baytlar başka bir sözleşmede ya bambaşka bir anlama gelir ya da hiçbir anlama gelmez.
İşletim sistemi de bu dosyayı çalıştırmadan önce hangi mimariye ait olduğuna bakar. 12. bölümde göreceğimiz ELF başlığında bunun için ayrılmış bir alan vardır; kernel oraya bakıp uyumsuzsa dosyayı hiç açmaz.
Bu tek gerçek, gündelik hayatta karşına çıkan bir sürü ayrıntıyı açıklıyor:
- Bir program indirirken neden “Intel” ve “Apple Silicon” diye iki ayrı sürüm sunuluyor.
- Docker imajlarının neden
amd64vearm64etiketleri var ve yanlış olanı çektiğinde neden çalışmıyor. - Telefonun uygulamasının bilgisayarında doğrudan çalışmamasının sebebinin işletim sisteminden önce mimari olduğu.
Bu duvarı aşmanın yolu her zaman aynı: çeviri. Apple’ın Rosetta’sı, QEMU gibi öykünücüler ve Windows’un ARM üzerinde x86 programlarını çalıştıran katmanı, hepsi bir ISA’nın talimatlarını diğerininkine çevirir. Çalışır, ama arada bir çevirmen olduğu için hep bir bedel ödersin.
Kendi makinenin hangi sözleşmeyi konuştuğunu merak ediyorsan tek komut yeter:
uname -m.x86_64görürsen bu bölümün ilk yarısındasın,aarch64ya daarm64görürsen ikinci yarısında.
Özet
- ISA, donanım ile yazılım arasındaki sözleşmedir: hangi bayt hangi işi yapar, kaç register vardır, kernel’a nasıl girilir.
- Mikro-mimari o sözleşmenin nasıl yerine getirildiğidir ve üreticiden üreticiye değişir. Aynı ISA, bambaşka içeriye sahip olabilir.
- 32/64 bit hem register genişliğini hem de adres genişliğini ölçer. 32 bitin 4 GiB tavanı, RAM’den değil adresin kendisinden gelir.
- x86-64, 1978’deki 8086’ya kadar uzanan kesintisiz bir geriye uyumluluk zincirinin bugünkü hâlidir; her açılışta hâlâ real mode’dan geçer.
- AArch64, güç verimliliği baskısı altında büyümüş ayrı bir ailedir ve lisanslama modeli sayesinde çok sayıda üreticide karşımıza çıkar.
- CISC/RISC ayrımı bugün büyük ölçüde tarihseldir: x86 içeride talimatları mikro-op’lara bölerek RISC gibi çalışır.
- Bir binary tek bir ISA’ya aittir. Başka bir mimaride çalıştırmanın tek yolu çeviridir ve çevirinin bedeli vardır.
Sözleşmeyi tanıdığımıza göre artık bir kademe aşağı inebiliriz. Sıradaki soru şu: CPU bu talimatları yürütürken ihtiyaç duyduğu veriyi nereden alıyor ve neden bunu almak sandığından çok daha pahalı?
5. bölüme devam et: Bellek Hiyerarşisi