Bölüm 3:Kernel, User Mode ve Syscall
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
Bir önceki bölümü üç soruyla kapattık. Şimdi ikincisini ve üçüncüsünü cevaplayacağız: programlar doğrudan CPU üzerinde çalışıyorsa ve CPU doğrudan RAM’e erişebiliyorsa, neden birbirlerinin belleğini okuyamıyorlar? Ve syscall denen şey tam olarak nedir?
Cevabın tamamı tek bir donanım özelliğine dayanıyor — ve o özellik, taşıdığı yükün yanında şaşırtıcı derecede basit.
Bu bölümde neyi çözüyoruz?
- CPU’nun ayrıcalık seviyelerini, yani kernel mode ile user mode ayrımını göreceğiz.
- Interrupt’ın ne olduğunu ve neden syscall için biçilmiş kaftan olduğunu anlayacağız.
- Bir syscall’ın register düzeyinde nasıl göründüğünü somut olarak izleyeceğiz.
exit(1)gibi tek satırlık bir çağrının altında kaç katman olduğunu çözeceğiz.
Hepsine Hükmedecek İki Halka
Şimdi CPU’nun en az konuşulan ama en çok işe yarayan özelliğine geldik. CPU her an bir mode içindedir (ayrıcalık seviyesi ya da ring olarak da geçer) ve o an neleri yapmasına izin verildiğini bu belirler. Aynı çip, aynı talimatı, hangi mode’da olduğuna göre ya çalıştırır ya da reddeder. Modern mimarilerde iki temel mode vardır: kernel/supervisor mode ve user mode. İkiden fazlası 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, I/O 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.

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. Daha net söyleyeyim: CPL, cs register’ının en düşük değerlikli 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 I/O 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.
Bu geçişin nasıl yapıldığını anlatabilmek için önce bir kavramı yerine oturtmam gerekiyor: interrupt.
Interrupt, CPU’ya “yürüttüğün işi bırak, şu adrese git” diyen mekanizmadır. Fetch-execute cycle normalde bir sonraki talimata ilerler; interrupt bu akışı keser, CPU’nun nerede kaldığını kaydeder ve onu önceden belirlenmiş başka bir adrese götürür. Oradaki iş bitince CPU kaldığı yere döner ve hiçbir şey olmamış gibi devam eder.
İki türü var ve farkları kimin tetiklediğidir:
- Hardware interrupt donanımdan gelir. Klavyeye bastığında, ağ kartına paket düştüğünde ya da bir timer’ın süresi dolduğunda üretilir. Program bunu istemez, başına gelir. 7. bölümde çoklu görevin tamamen bunun üstüne kurulduğunu göreceğiz.
- Software interrupt’ı ise programın kendisi bilerek tetikler. “Şu anda kesilmek istiyorum” der.
Syscall için biçilmiş kaftan olmasının sebebi şu: bu, atlanacak yeri programın değil kernel’ın önceden belirlediği tek atlama biçimidir. Sıradan bir jmp talimatıyla istediğin adrese gidebilirsin; interrupt ile ancak kernel’ın senin için hazırladığı yere gidebilirsin.
İşte user space’ten kernel space’e kontrol devrinin temel fikri budur: CPU, sadece kernel’ın önceden hazırladığı güvenli giriş noktalarına geçişe izin verir. Tarihsel olarak bu iş software interrupt ile anlatılır; modern mimarilerde ise aynı rolü çoğu zaman özel syscall giriş talimatları üstlenir.
Bu cümlenin içindeki “önceden hazırladığı” ifadesi, bütün güvenlik modelinin kilit taşı. Neden bu kadar önemli olduğunu görmek için bir an tersini hayal et: eğer bir program kernel’ın içinde istediği adrese atlayabilseydi, dosya açma kodunun izin kontrolünden sonraki satırına atlar ve kontrolü tamamen atlardı. Kapıyı kilitlemenin bir anlamı kalmazdı.
Bu yüzden CPU şu kuralı donanım seviyesinde uygular: user mode’daki bir program kernel’a nereden gireceğini seçemez. Yalnızca “girmek istiyorum” diyebilir; nereye düşeceğine kernel’ın önceden yazdığı liste karar verir.
Acil durum numaralarını düşün. Yangın çıktığında itfaiyecinin cep telefonunu aramazsın, 112’yi ararsın. O numaranın karşılığında kimin oturduğunu sen değil, sistemi kuran kişi belirlemiştir. Sana düşen tek şey bir numara söylemektir.
Kernel de açılışta tam olarak böyle numaralı bir liste hazırlar: “0 numaralı olay olursa şu adrese git, 14 numaralı olursa bu adrese git.” Sonra CPU’ya bu listenin nerede durduğunu bir kez bildirir. Listenin x86-64’teki adı IDT’dir (Interrupt Descriptor Table). Adı şimdilik önemli değil; önemli olan, listeyi yalnızca kernel mode’daki kodun değiştirebilmesi. User mode’daki bir program ne listeye yeni bir numara ekleyebilir ne de mevcut bir girdiyi baypas edebilir.
Şimdi mekanizmanın kendisine bakalım.
- Boot sırasında işletim sistemi bu tabloyu RAM’de kurar ve adresini CPU’ya kaydeder. Tablo, olay numaralarını handler adresleriyle eşler. (Eski 16-bit dünyada aynı yapının adı interrupt vector table — IVT idi; x86-64’te interrupt descriptor table olarak geçer.)

- Eski 32-bit x86 Linux modelinde user-space programları, INT gibi bir talimat kullanarak CPU’ya şu işi yaptırabilirdi: IVT/IDT’de ilgili interrupt numarasını bul, kernel mode’a geç ve instruction pointer’ı oradaki handler adresine atla.
Modern 64-bit Linux’ta x86-64 için yerel syscall yolu syscall talimatıdır. int 0x80 ve sysenter hâlâ eski uyumluluk bağlamlarında karşına çıkabilir, ama bugünün 64-bit programları için ana yol değildir.
Dönüş de gidiş kadar kontrollüdür. Kernel işini bitirince CPU’ya iki şeyi birden yaptırır: ayrıcalık seviyesini user mode’a düşürür ve instruction pointer’ı programın kaldığı yere geri koyar. Bu iki işi tek hamlede yapan özel bir dönüş talimatı vardır; adı mimariye göre değişir (x86-64’te iret ya da sysret, AArch64’te exception return). İsimleri ezberlemene gerek yok; önemli olan dönüşün de rastgele bir atlama olmadığı.
(Merak ediyorsan, 32-bit x86 Linux’ta klasik syscall interrupt numarası 0x80’dir. Linux syscall listesini Michael Kerrisk’in çevrimiçi manpage dizininde görebilirsin.)
Bütün yolculuğu tek bir şemada adım adım izleyelim:
- Program çalışıyor. Kod user mode'da (ring 3) yürüyor. Bu modda I/O yok, kernel belleğine erişim yok.
- Argümanlar register'lara yazılır. Syscall numarası `rax`'e, argümanlar `rdi`, `rsi`, `rdx`'e konur. Bu düzenin adı calling convention.
- `syscall` talimatı tetiklenir. CPU tek hamlede iki iş yapar: ayrıcalık seviyesini kernel mode'a çıkarır ve kernel'ın önceden belirlediği giriş noktasına atlar. Program nereye gideceğini seçemez.
- Kernel işi yapar. Önce argümanları doğrular ve izinleri kontrol eder, sonra asıl işi yürütür. Sonuç `rax`'e yazılır.
- Geri dönülür. `sysret` yine tek hamlede iki iş yapar: ayrıcalık user mode'a düşer ve instruction pointer programın kaldığı yere geri konur.
Wrapper API’ler: Interrupt Ayrıntısını Gizlemek
Şu ana kadar bildiklerimizi toparlayalım:
- User-mode programları ayrıcalıklı I/O’ya, fiziksel belleğe ya da kernel belleğine doğrudan erişemez. Dış dünyayla etkileşime geçmek için işletim sisteminden yardım istemeleri gerekir.
- Programlar,
syscall/sysret,svc/exception return veya legacyint/iretgibi mimariye özgü yollarla kontrolü işletim sistemine devredebilir. - Programlar ayrıcalık seviyesini doğrudan değiştiremez. Bu kontrollü geçişler güvenlidir çünkü CPU, işletim sistemi kodunda nereye atlanacağını önceden kernel tarafından yapılandırılmış giriş noktalarından alır. Bu tablolar ve register’lar yalnızca kernel mode’da değiştirilebilir.
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 yapıldığını somut görmek her şeyi yerine oturtuyor. 64-bit Linux’ta kural şu: syscall numarası rax register’ına, ilk üç argüman da sırasıyla rdi, rsi ve rdx register’larına yazılır. Sonra syscall talimatı tetiklenir. Programı sonlandıran exit(1) çağrısının aslı, iki register’a değer koyup bir talimat çalıştırmaktan ibarettir:
rax = 60— 60, Linux’taexitsyscall’ının numarası.rdi = 1— çıkış kodu.syscall
Kernel devraldığında yaptığı ilk iş bu register’ları okumaktır. Yani register’lar burada iki dünya arasındaki ortak dil işlevi görüyor: program yazıyor, kernel okuyor. Bu düzenin adı calling convention’dır ve mimariden mimariye, işletim sisteminden işletim sistemine değişir.
Mimariler arasında syscall çağırma biçimlerinin değişmesi, programcıların her program için bunu elle yazmasını pratik olmaktan çıkarır. Dahası, işletim sistemini kendi ayrıntılarına mahkûm eder: her program bu ayrıntıları doğrudan kullanıyorsa, en küçük değişiklik çalışan her programı bozar. Üstelik artık ham assembly ile program yazmıyoruz; bir dosya okumak isteyen herkesin assembly yazması beklenemez.

Bu yüzden işletim sistemleri, bu mimariye özgü kernel giriş yolları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 yerleştirir, sonra da mimariye özgü syscall talimatını tetikler. Yani senin tek satırlık exit(1) çağrın, altında register düzenleyen bir assembly bloğuna, oradan da CPU’nun ayrıcalık seviyesini değiştiren tek bir talimata iniyor. Bu kadar katmanın üst üste dizilip tek satır gibi görünmesi, bilgisayarların en iyi yaptığı iş.
Bir Syscall Ne Kadar Pahalı?
Bütün bu mekanizmayı gördükten sonra doğal bir soru geliyor: madem syscall sıradan bir fonksiyon çağrısı değil, ne kadar pahalı?
Karşılaştırma şöyle. Sıradan bir fonksiyon çağrısı birkaç saat çevrimi sürer. Bir syscall ise mertebe olarak yüzlerce çevrim. Aradaki farkı üç şey doğuruyor:
- Mod geçişinin kendisi. CPU ayrıcalık seviyesini değiştirir, register’ları kaydeder, kernel yığınına geçer. Dönüşte hepsini geri alır.
- Kernel’ın doğrulama işi. Kullanıcıdan gelen her işaretçi ve her sayı şüphelidir; kernel bunları kontrol etmek ve kendi tarafına kopyalamak zorundadır.
- Spekülatif yürütme yamalarının bedeli. Meltdown ve Spectre sonrası eklenen korumalar, her geçişte sayfa tablosu değiştirmek ya da tahmin tamponlarını temizlemek gibi ek işler getirdi. Bu bedel, 2018 sonrası sistemlerde syscall maliyetini gözle görülür biçimde artırdı.
Bu sayı, kitabın ilerisindeki birçok tasarım kararının arkasındaki asıl sebep. Bir programın neden veriyi tek tek değil de tampon dolusu yazdığı, epoll ve io_uring gibi arayüzlerin neden var olduğu — hepsi aynı hedefe hizmet eder: aynı işi daha az syscall ile yapmak.
İşin ilginç tarafı, bazı syscall’ların bu bedeli hiç ödememesi. Saati sormak gibi çok sık yapılan ve hiçbir gizli bilgi gerektirmeyen birkaç iş için kernel, gerekli kodu doğrudan programın adres alanına eşler; çağrı kernel’a hiç sıçramadan user space’te biter. Bu numaranın adı vDSO ve son bölümde ayrıntısına döneceğiz.
Toparlayalım:
- Hangi talimatların çalıştırılabileceğini, işlemcinin o anki mode’u belirler. İşletim sistemi kodu kernel mode’da çalışır; bir programı çalıştırmadan önce user mode’a geçer.
- Programların ayrıcalıkları kısıtlı olduğu için dış dünyayla etkileşim kurmak istediklerinde kernel’dan yardım istemek zorundadırlar. Syscall, bu geçişin standart yoludur.
- Geçişin güvenli olmasının sebebi, atlanacak yeri programın değil kernel’ın belirlemesidir. User mode’daki kod kernel’a nereden gireceğini seçemez; yalnızca girmek istediğini söyleyebilir.
- Programlar syscall’ları genelde libc gibi kütüphane fonksiyonları üzerinden kullanır. Mimariye özgü ayrıntıyı saran katman odur.
Son maddedeki “mimariye özgü” ifadesi, şimdiye kadar birkaç kez kullandığım ama hiç açmadığım bir konuya işaret ediyor. x86-64, AArch64, RISC — bu isimler tam olarak neyi anlatıyor ve neden bir program bir makinede çalışıp diğerinde çalışmıyor? Sıradaki bölümün konusu bu.
4. bölüme devam et: Mimariler: x86, ARM ve Diğerleri