Bölüm 1:

Bu makaleyi yazarken beni tekrar tekrar şaşırtan şey, bilgisayarların ne kadar basit olduğuydu. Gerçekte olduğundan daha karmaşık ya da daha soyut bir yapı beklememek benim için hâlâ kolay değil. 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.

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.

İlk seri üretim CPU, 1960’ların sonunda İtalyan fizikçi ve mühendis Federico Faggin tarafından tasarlanan Intel 4004‘tü. Bugün kullandığımız 64-bit sistemler yerine 4-bit bir mimariye sahipti ve modern işlemcilerden çok daha az karmaşıktı, ama temel çalışma mantığının büyük kısmı bugün hâlâ aynı.

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.

Makine kodunun assembly ile ikili ve hex gösterimleri arasında nasıl çevrildiğini gösteren bir diyagram. Renk kodlaması, her parçanın birbirine nasıl karşılık geldiğini ortaya koyuyor.

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ığı tüm 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 makine kodunu doğrudan RAM’den okur; RAM’e yüklenmemiş bir kod çalıştırılamaz.

CPU, RAM’de sıradaki talimatın nerede olduğunu gösteren bir instruction pointer tutar. Her talimat çalıştırıldıktan sonra bu göstericiyi ilerletir ve aynı şeyi tekrar yapar. Buna fetch-execute cycle denir.

Fetch-execute cycle diyagramı. Önce bellekten talimat okunuyor, sonra talimat yürütülüyor, ardından gösterici ilerletilip döngü tekrarlanıyor.

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.

Bu instruction pointer bir register içinde tutulur. Register’lar, CPU’nun çok hızlı okuyup yazabildiği küçük depolama alanlarıdır. Her CPU mimarisinin, geçici değer tutmaktan işlemci yapılandırmasına kadar farklı işler için kullandığı sabit bir register kümesi vardır.

Önceki diyagramdaki ebx gibi bazı 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.

(Bu, benim için gerçekten afallatıcı anlardan biriydi. Bu makaleyi okumak için kullandığın program da tam olarak bu şekilde çalışıyor. CPU’n şu anda tarayıcının talimatlarını RAM’den sırayla okuyup yürütüyor.)

RAM içindeki makine kodu üzerinde ilerleyen bir instruction pointer diyagramı.

CPU’ların dünya görüşü aslında inanılmaz derecede dardır: yalnızca o andaki instruction pointer’ı ve biraz da iç durumu görürler. Process dediğimiz şey tamamen işletim sistemi soyutlamasıdır; CPU’nun doğal olarak bildiği ya da takip ettiği bir kavram değildir.

*ellerini sallayarak* process’ler, OS geliştiricileri büyük bayt lobisi tarafından daha fazla bilgisayar satmak için uydurulmuş soyutlamalardır

Benim için bu, cevap verdiğinden çok daha fazla soru doğurdu:

  1. 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?
  2. Programlar doğrudan CPU üzerinde çalışıyorsa ve CPU doğrudan RAM’e erişebiliyorsa, neden başka process’lerin belleğine ya da Allah korusun kernel belleğine erişemiyorlar?
  3. 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 5‘te ele alacağız. Kısa versiyon şu: bellek erişimlerinin çoğu, tüm adres uzayını yeniden eşleyen bir yönlendirme katmanından geçer. Şimdilik, programların tüm RAM’e doğrudan erişebildiğini ve bilgisayarın aynı anda sadece tek bir process çalıştırabildiğini varsayalım. Bu varsayımların ikisini de birazdan bozacağız.

İlk tavşan deliğimize, yani syscall’lar ve güvenlik halkaları dünyasına atlama zamanı.

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çıldığında instruction pointer bir yerdeki programa atlar; işte o program kernel’dır. Kernel, belleğe, çevre birimlerine ve sistem kaynaklarına neredeyse tam erişime sahiptir ve kullanıcı alanı programlarını çalıştırmaktan sorumludur. Bu makale boyunca kernel’ın bu erişime nasıl sahip olduğunu ve kullanıcı alanı programlarının neden sahip 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.

Hepsine Hükmedecek İki Halka

Bir işlemcinin bulunduğu mode (ayrıcalık seviyesi ya da ring olarak da geçer), neleri yapabildiğini belirler. Modern mimarilerde en az iki temel seçenek vardır: kernel/supervisor mode ve user mode. İkiden fazla mode desteklenebilir, ama pratikte bugün çoğunlukla bu ikisi kullanılır.

Kernel mode’da neredeyse her şey serbesttir: CPU desteklediği her talimatı çalıştırabilir ve her belleğe erişebilir. User mode’da ise yalnızca belirli talimatlara izin verilir, G/Ç ve bellek erişimi kısıtlanır ve birçok CPU ayarı kilitlenir. Genel olarak kernel ve sürücüler kernel mode’da, uygulamalar ise user mode’da çalışır.

İşlemciler açılışta kernel mode’da başlar. Bir programı çalıştırmadan önce kernel, user mode’a geçiş yapar.

Kernel mode ile user mode arasındaki koruma farkını anlatan iki sahte mesajlaşma ekranı.

Gerçek bir mimaride bunun nasıl göründüğüne örnek: x86-64’te mevcut ayrıcalık seviyesi (CPL), cs adlı code segment register’ından okunabilir. Özellikle CPL, cs register’ının en düşük anlamlı iki bitinde tutulur. Bu iki bit, x86-64’ün dört olası ring’ini temsil eder: ring 0 kernel mode’dur, ring 3 ise user mode’dur. Ring 1 ve 2 sürücüler için düşünülmüştür ama bugün yalnızca birkaç eski ve niş işletim sistemi tarafından kullanılır. Örneğin CPL bitleri 11 ise CPU ring 3, yani user mode’dadır.

Syscall Tam Olarak Nedir?

Programlar user mode’da çalışır çünkü bilgisayara tam erişim konusunda onlara güvenilmez. User mode, sistemin büyük kısmına erişimi engeller; ama programların yine de G/Ç yapması, bellek ayırması ve bir şekilde işletim sistemiyle konuşması gerekir. Bunun için user mode’da çalışan yazılımın kernel’dan yardım istemesi gerekir. Kernel da bu sırada kendi güvenlik kontrollerini uygulayabilir.

İşletim sistemiyle etkileşen kod yazdıysan muhtemelen open, read, fork ve exit gibi fonksiyonları görmüşsündür. Bu fonksiyonların altında, işletim sisteminden yardım istemek için syscall‘lar kullanılır. Syscall, bir programın user space’ten kernel space’e geçiş başlatmasına; yani program kodundan işletim sistemi koduna atlamasına izin veren özel prosedürdür.

User space’ten kernel space’e kontrol devri, software interrupt denilen işlemci özelliğiyle yapılır:

  1. Boot sırasında işletim sistemi, RAM’de interrupt vector table (IVT; x86-64 tarafında interrupt descriptor table olarak geçer) adı verilen bir tablo kurar ve CPU’ya kaydeder. IVT, interrupt numaralarını handler kodu işaretçileriyle eşler.
Interrupt Vector Table şeması. Her interrupt numarası bir handler adresine karşılık geliyor.
  1. Ardından user-space programları, INT gibi bir talimat kullanarak CPU’ya şu işi yaptırabilir: IVT’de ilgili interrupt numarasını bul, kernel mode’a geç ve instruction pointer’ı oradaki handler adresine atla.

Bu kernel kodu işi bitirdiğinde, IRET gibi bir talimatla CPU’ya user mode’a geri dönmesini ve instruction pointer’ı interrupt’ın tetiklendiği yere geri koymasını söyler.

(Merak ediyorsan, Linux’ta syscall’lar için kullanılan interrupt kimliği 0x80‘dir. Linux syscall listesini Michael Kerrisk’in çevrimiçi manpage dizininde görebilirsin.)

Wrapper API’ler: Interrupt Ayrıntısını Gizlemek

Şu ana kadar bildiklerimizi toparlayalım:

Bir program syscall tetiklerken kernel’a veri de aktarmalıdır. Kernel’ın hangi syscall’ın çağrıldığını ve örneğin hangi dosya adının açılacağını bilmesi gerekir. Bunun nasıl aktarıldığı mimariye ve işletim sistemine göre değişir; genellikle interrupt tetiklenmeden önce belirli register’lara ya da stack’e veri yazılır.

Mimariler arasında syscall çağırma biçimlerinin değişmesi, programcıların her program için bunu elle uygulamasını pratik olmaktan çıkarır. Aynı zamanda işletim sistemlerinin de “eski biçimi kullanan her program bozulur” korkusuyla bu detayları sabitlemesini zorlaştırır. Üstelik artık ham assembly ile program yazmıyoruz; bir dosya okumak isteyen herkesin assembly yazması beklenemez.

Mimariler arasında syscall uygulamalarının değiştiğini anlatan bir çizim.

Bu yüzden işletim sistemleri, bu interrupt mekanizmasının üstüne bir soyutlama katmanı koyar. Gerekli assembly talimatlarını saran yeniden kullanılabilir üst seviye kütüphane fonksiyonları Unix benzeri sistemlerde libc, Windows’ta ise ntdll.dll tarafından sağlanır. Bu kütüphane fonksiyonlarına yaptığın çağrı doğrudan kernel mode’a geçmez; bunlar normal fonksiyon çağrılarıdır. Kütüphanenin içinde bir yerde assembly devreye girer ve kontrol gerçekten kernel’a aktarılır.

Unix benzeri bir sistemde C içinden exit(1) çağırdığında, bu fonksiyon önce syscall numarasını ve argümanlarını doğru register’lara ya da stack’e yerleştirir, sonra da interrupt ya da mimariye özgü syscall talimatını tetikler. Bilgisayarlar gerçekten çok havalı.

Hız İhtiyacı / Biraz CISC Olalım

x86-64 gibi birçok CISC mimarisi, syscall’ların ne kadar yaygın kullanıldığını görünce bu iş için özel talimatlar ekledi.

Intel ve AMD, x86-64 tarafında bu konuda tam uyum sağlayamadı; sonuçta iki ayrı optimize syscall talimatı ortaya çıktı. SYSCALL ve SYSENTER, INT 0x80 gibi yöntemlere göre optimize edilmiş alternatiflerdir. Bunların dönüş tarafında ise SYSRET ve SYSEXIT bulunur; bunlar user space’e hızlı dönmek için tasarlanmıştır.

(AMD ve Intel işlemciler bu talimatlar konusunda birebir aynı davranmaz. SYSCALL genellikle 64-bit programlar için daha iyi tercih olurken, SYSENTER 32-bit programlarda daha iyi destek görür.)

Buna karşılık RISC mimarileri genelde böyle özel talimatlar eklemeye daha az meyillidir. Apple Silicon’un dayandığı AArch64 mimarisi, hem syscall’lar hem de software interrupt’lar için yalnızca tek bir interrupt talimatı kullanır. Mac kullanıcıları bu konuda fena durumda değil :)


Vay be, bu çok şeydi. Kısa bir özet geçelim:

Şimdi başta sorduğumuz ilk soruya dönelim:

CPU birden fazla process’i takip etmiyorsa ve sadece talimat üstüne talimat 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?

Bunun cevabı, sevgili dostum, aynı zamanda Coldplay’in neden bu kadar popüler olduğunun da cevabı: saatler. Daha doğrusu timer’lar. Evet, bu şakayı yapmak istedim.

2. bölüme devam et: Zamanı Dilimle