Ana içeriğe geç

Bir CPU Hikayesi: Fonksiyon neden return etmek zorunda?

Void bir fonksiyonun sonunda bile neden return var? Çünkü return bir değer değil, CPU'nun kaldığı adresi geri yüklüyor — hesaplama teorisinin donanıma düşen gölgesi. FSM'den transistor satırlarına, stack overflow'un fiziksel tavanına ve derleyicinin sessiz inlining taktiğine kadar tek satırın altında ne varsa inceliyoruz.

Giriş: return gerçekten neyi döndürüyor? #

Hiç durup “Neden return yazıyorum?” diye sordun mu kendine?

Basit görünüyor. “Return değer döndürür, bitti.” Yıllardır yazıyorsun, refleks oldu. Ama hiçbir şey dönmeyen bir fonksiyonun sonunda bile return yazdığın o anı hatırla. İçinden küçük bir ses “Burada neyin değerini döndürüyorum ki?” diye sordu mu hiç? O sesi bastırmak kolay: “Dil istiyor işte.” Ama bu cevap ile atladığımız, bayağı ciddi bir mühendislik detayı var. GeeksforGeeks+1 2 sources 8 Function Call Stack in CGeeksforGeeks 9 JavaScript Call StackJavaScript Tutorial

Soruyu biraz daha net soralım: Neden her fonksiyon bir şekilde return etmek zorunda?

Bazı dillerde keyword’ü yazmasan da derleyici bloğun sonuna hayalet bir return ekliyor. void fonksiyonlarda iş iyice garipleşiyor: tipi resmen “hiçbir şey”, ama fonksiyon yine de dönmek zorunda. Peki fonksiyondan dışarı aslında ne çıkıyor?

Dışarı çıkan şey bir değer değil; CPU’nun kaldığı yer.

Bilgisayarındaki bütün programlar RAM’e yüklenmek zorunda: işletim sistemi, IDE, tarayıcı, script’in, test runner’ın. Hepsi aynı fiziksel adres alanında yüzüyor; sanal belleksanal bellekSanal Bellek — işletim sisteminin fiziksel RAM'i her programa izole adres alanları olarak gösterdiği mekanizma. Bellek izolasyonunu ve verimli kullanımı sağlar. ve izolasyon sadece bu denizi organize etmenin yolları. Bu talimat denizinde CPU’nun elinde tek bir pusula var: program counterprogram counterProgram Counter (PC) — CPU'nun bir sonraki çalıştırılacak komutun adresini saklayan register. . Görevi de çok sade — “Şu an bellekte hangi adresi çalıştırıyorum?” Wikipedia+3 4 sources 1 Call stackWikipedia 3 Program Counter and Instruction RegisterBaeldung 4 Program counterWikipedia 10 Inside the CPU: A Complete Guide to the Instruction Execution Cycledev.to

Bir fonksiyon çağırdığında CPU bu pusulanın gösterdiği adresi hatırlamak zorunda; çünkü iş bitince tam kaldığı yerden devam edecek. return’ün gerçek anlamı tam burada:

Biraz önce bellekte şu adreste kalmıştın — oraya geri dön.

Değer dönsen de dönmesen de, her fonksiyonun donanım seviyesinde gerçekten “döndürdüğü” tek şey, CPU’nun bir sonraki adımı atabilmesi için ihtiyaç duyduğu o küçük adres. Wikipedia+3 4 sources 1 Call stackWikipedia 2 How does a function call work?StackOverflow 4 Program counterWikipedia 11 Understanding how function call workszhu45.org

Bu da bizi “fonksiyon niye var?” sorusundan “hesaplama nasıl modellenir?” sorusuna getiriyor.


Hesaplama gözünden: FSM ve “kaldığın yer” #

Bu iş donanımla başlamıyor. Bilgisayar demeden çok önce de aynı fikri kullanıyorduk: hesaplama dediğin şey, bir durumdan diğerine geçmek.

Kağıt üzerinde toplama yaparken bile aslında şunu diyorsun: “Başta 7’yim, +3 dedim, şimdi 10’um.” Bu bakış açısına FSMFSMFinite State Machine (Sonlu Durum Makinesi) — hesaplamayı durumlar arası geçişler olarak modelleyen teorik yapı. Başlangıç durumu, ara durumlar ve sonlanma durumu içerir. diyoruz. Başlangıç durumu var, ara adımlar var, en sonunda da “bitti” dediğin bir durma durumu. Wikipedia+2 3 sources 1 Call stackWikipedia 27 Finite-state machineWikipedia 28 Finite State MachinesBrilliant

Bir programı da bu gözle okuyabilirsin: her satır, her branch, her fonksiyon çağrısı seni başka bir state’e taşıyor. Fonksiyon ise bu zincirin içinde küçük bir alt makine: kendi başlangıç durumu, kendi ara adımları, kendi bitişi var. İşi bittiğinde seni kaldığın yere geri bırakmak zorunda.

  • Teoride “kaldığın yer” → bir üst FSM’in sonraki state’i.
  • CPU tarafında “kaldığın yer” → caller’ın bir sonraki talimat adresi.

return bu yüzden sadece “değer döndürmek” değil; FSM tarafında “bir üst makinenin kaldığı state’e dön”, donanım tarafında “caller’ın PC’sini geri yükle” demek. Sonradan uydurulmuş bir keyword değil; hesaplama teorisinden CPU’nun register’larına kadar inerken adını değiştirmiş bir zorunluluk. Wikipedia+2 3 sources 1 Call stackWikipedia 27 Finite-state machineWikipedia 28 Finite State MachinesBrilliant


CPU’nun gözünden: PC ve call #

İşlemcinin dünyası bizimkinden çok daha yalın. Elinde tek bir gösterge var: program counterprogram counterProgram Counter (PC) — CPU'nun bir sonraki çalıştırılacak komutun adresini saklayan register. (PC). PC, bellekte tek bir adres tutuyor. Her clock’ta aynı ritüeli çeviriyor: Baeldung+1 2 sources 3 Program Counter and Instruction RegisterBaeldung 4 Program counterWikipedia

  1. PC’nin gösterdiği adresten talimatı al.
  2. Decode et, çalıştır.
  3. “Sonraki” PC’yi ayarla.

Bu üç adım, bugüne kadar üretilmiş her CPU’nun ortak çekirdeği — bugün kullandığın x86-64’ün de, Z80’in de.

Klasik blok diyagrama bakınca isimler ete kemiğe bürünüyor: program counter, stack pointer, register file, aralarındaki bus’lar. Bunlar sadece diyagram kutusu değil, die üstünde yer kaplayan fiziksel bloklar.

Intel 8085 blok diyagramı — program counter, stack pointer ve register file fiziksel kutular olarak, aralarında bus’lar; modern CPU’lar da aynı mimariyi çok daha büyük ölçekte çalıştırıyor
Intel 8085 blok diyagramı — program counter, stack pointer ve register file fiziksel kutular olarak, aralarında bus’lar; modern CPU’lar da aynı mimariyi çok daha büyük ölçekte çalıştırıyor

Sıradan talimatlarda “sonraki” demek, “şu anki adres + talimat boyu” demek. Dallanma ve fonksiyon çağrısı oyunu değiştiriyor: “sonraki” artık yanındaki değil, bambaşka bir yer. Fonksiyon denen şey, bu sıçramayı düzenli ve tekrar kullanılabilir hale getiren bir teknik aslında.

Derleyici func add(a, b) yazdığın kodu alıyor, makine koduna çeviriyor ve bellekte bir adrese yerleştiriyor. CPU add ismini ne biliyor ne umursuyor; onun bildiği tek şey şu: “Bu fonksiyon 0x400580 adresinde başlıyor.” Wikipedia+2 3 sources 1 Call stackWikipedia 2 How does a function call work?StackOverflow 11 Understanding how function call workszhu45.org

call add çalıştığında CPU üç iş yapıyor: Wikipedia+2 3 sources 1 Call stackWikipedia 2 How does a function call work?StackOverflow 12 How is return address specified in Stack?GeeksforGeeks

  • Normalde gideceği adresi — yani call’dan sonraki talimatın adresini — dönüş adresidönüş adresiFonksiyon çağrısından sonraki talimatın bellek adresi — fonksiyon bittiğinde CPU'nun nereden devam edeceğini belirtir. olarak alıyor.
  • Bu adresi bir yere yazıyor (çoğu mimaride stack’in tepesine, bazılarında link register’a).
  • PC’yi add’in başına çekiyor.

Sen kodda sadece add(2, 3) görüyorsun. Alt tarafta olan şu:

Bu adreste bir call var. Bir sonraki talimatın adresini dönüş adresi diye sakladım. PC’yi 0x400580’e çektim. İçeri giriyoruz.

add(2, 3) neye dönüşüyor — argümanlar mov ile register’lara yükleniyor, sonra fonksiyonun adresine call; kaynak tarafındaki ifade yok oluyor, geriye sadece adresler kalıyor
add(2, 3) neye dönüşüyor — argümanlar mov ile register’lara yükleniyor, sonra fonksiyonun adresine call; kaynak tarafındaki ifade yok oluyor, geriye sadece adresler kalıyor


Stack pointer: dönüş adresinin park yeri #

Sahneye stack pointerstack pointerStack Pointer (SP) — fonksiyon çağrıları sırasında dönüş adreslerinin ve yerel değişkenlerin saklandığı stack'in tepesini gösteren özel CPU register'ı. çıkıyor. SP, RAM’de LIFOLIFOLast In First Out — veri yapılarında en son eklenen elemanın ilk olarak çıkarıldığı ilke. Stack'in temel çalışma prensibi. çalışan bir bölgenin tepe adresini tutuyor. call çalıştığında, tipik bir mimaride şu oluyor: Wikipedia+3 4 sources 1 Call stackWikipedia 6 x86/x64 CPU architecture: the stack & stack framesyuriygeorgiev 12 How is return address specified in Stack?GeeksforGeeks 13 Memory Stack Organization in Computer ArchitectureGeeksforGeeks

  • CPU “sonraki talimat” adresini okuyor.
  • SP’yi bir tık aşağı indiriyor (stack çoğu mimaride aşağı doğru büyür).
  • Dönüş adresini stack’in yeni tepesine yazıyor.
  • PC’yi fonksiyonun giriş adresine ayarlıyor.

Her yeni call stack’in üstüne yeni bir dönüş adresi (ve yer yer ekstra frame verisi) bırakıyor; her ret en üstteki dönüş adresini PC’ye geri yüklüyor ve SP’yi bir önceki seviyeye çekiyor. Stack erişimlerinin bu kadar hızlı olmasının sebebi de bu: hep aynı noktada, tepede çalışıyorsun; CPU cache’i bu pattern’i seviyor. yuriygeorgiev+2 3 sources 6 x86/x64 CPU architecture: the stack & stack framesyuriygeorgiev 13 Memory Stack Organization in Computer ArchitectureGeeksforGeeks 14 Is there a point in memory on the stack where performance drops significantly?StackOverflow

Üç karede call ve ret — çağrıdan önce (SP ve PC caller’da), çağrı sırasında (dönüş adresi stack’e push edildi, PC fonksiyonun başına sıçradı), ret’ten sonra (stack’in tepesi PC’ye geri yüklendi, SP eski yerine döndü)
Üç karede call ve ret — çağrıdan önce (SP ve PC caller’da), çağrı sırasında (dönüş adresi stack’e push edildi, PC fonksiyonun başına sıçradı), ret’ten sonra (stack’in tepesi PC’ye geri yüklendi, SP eski yerine döndü)

heapheapHeap — program çalışma süresinde (runtime) dinamik olarak ayrılan, boyutu değişebilen ve uzun ömürlü nesnelerin tutulduğu bellek bölgesi. Stack'in aksine serbest erişimlidir ve parça parça allocation yapılabilir. bambaşka bir dünya. Orada dinamik boyutlu, ömürleri karışık, erişim pattern’i düzensiz nesneler duruyor. Pointer’lar iki tarafı birbirine bağlayabiliyor ama heap’in “dönüş adresi” gibi katı bir sözleşmesi yok. Stack’in sözleşmesi çok sade: her call yukarı bir kayıt koyar, her ret en üsttekini alır. Kuralcı ve işlevesel. Wikipedia+3 4 sources 1 Call stackWikipedia 8 Function Call Stack in CGeeksforGeeks 13 Memory Stack Organization in Computer ArchitectureGeeksforGeeks 15 Will the pointer address be stored in cache or the data that it points to?Reddit

Process belleği — kod en altta, heap yukarı büyüyor, stack aşağı büyüyor; aynı adres uzayını paylaşan iki farklı allocation disiplini
Process belleği — kod en altta, heap yukarı büyüyor, stack aşağı büyüyor; aynı adres uzayını paylaşan iki farklı allocation disiplini

Stack overflow, bu disiplinin fiziksel tavana çarptığı an.
Genelde iki klasik sebebi vardır: base case’i unutulmuş bir recursion, ya da base case’e ulaşamayacak kadar derinleşmiş bir call zinciri.
Her call yeni bir frame ve yeni bir dönüş adresi ekler, SP her seferinde biraz daha aşağı iner.
Bir noktada stack, işletim sisteminin o proses için ayırdığı sınırı zorlar; CPU/OS “buraya kadar” der ve programı düşürür. Wikipedia+4 5 sources 1 Call stackWikipedia 8 Function Call Stack in CGeeksforGeeks 13 Memory Stack Organization in Computer ArchitectureGeeksforGeeks 14 Is there a point in memory on the stack where performance drops significantly?StackOverflow 16 Infinite loops in JavaStackOverflow

Bu resim cebimizde dursun; birazdan “fonksiyonun maliyeti” kısmına geri döneceğiz.


Return etmezsen ne olur? “Hiçlik” nasıl çalışır? #

Buradan bakınca “Return etmezsen ne olur?” sorusu soyut olmaktan çıkıyor.

“Return yoksa sadece hiçlik var” dediğimizde kastettiğimiz şey şu: CPU her clock’ta işini yapmaya, PC’yi ilerletmeye devam ediyor, ama ret hiç çalışmıyor.
Stack’in tepesindeki dönüş adresi PC’ye bir daha geri yüklenmiyor.
Program hâlâ talimat yürütüyor, ama çağrıdan sonraki satıra asla dönemiyor. Wikipedia+3 4 sources 1 Call stackWikipedia 2 How does a function call work?StackOverflow 12 How is return address specified in Stack?GeeksforGeeks 16 Infinite loops in JavaStackOverflow

FSM gözünden bakarsan burada şunu yaptın:
üstteki state makinesini (caller) bırakıp, alttaki makinenin (callee) içinde sonsuza kadar dönmeye karar verdin.
Yani zincirdeki “bir üst state’e geri dön” geçidini koparmış oluyorsun; return tam olarak o geçidi yeniden kuran adım. Wikipedia+2 3 sources 1 Call stackWikipedia 27 Finite-state machineWikipedia 28 Finite State MachinesBrilliant

Klasik iki senaryo:

Dil fark etmiyor: Go, C, Java, Python… Hepsinde gövdesi sonsuz döngü olan bir fonksiyonu çağırırsan, çağrıdan sonrası yürümüyor. CPU aslında bir sürü talimat yürütmeye devam ediyor; sadece PC bir daha call’dan sonraki adrese dönmüyor.


Pusulanın içi: PC ve SP’nin gerçekten var olduğu yer #

Buraya kadar PC ve SP’yi “özel register’lar” diye konuştuk.
Teoride FSM, pratikte call/ret + stack ile resmi tamamladık.
Şimdi bir kat daha inip şunu görelim: bu return zorunluluğu silikonda nerede karşılık buluyor, yani bu state değişimleri fiziksel olarak nerede tutuluyor?

Die tarafına bu yüzden bakıyoruz: return dediğimiz şeyin soyut bir dil kuralı değil, register file’daki belirli satırların ve RAM’deki birkaç byte’ın üst üste oynadığı somut bir donanım davranışı olduğunu görmek için. Bunu akılda tutmanın kazanımı şu: kod yazarken “ya ne olacak ki” demek yerine, abstraction’ların altında yatan fiziksel limitleri ve geliştirme sırasında yaşayabileceğimiz sorunları ön görmek.

Diyagramlarda kutu çizip “register file” diyoruz. Debugger’da RSP = 0x7ffeefbff5c0 görüyoruz. Bunlar, orada duran birer isim gibi geliyor. Gerçekte ise her çekirdeğin içinde yaşayan, her clock tikinde bit pattern’i değişen fiziksel yapılar: flip-flop’lardan örülmüş, die’a kazınmış register satırları. Wikipedia+2 3 sources 5 Stack registerWikipedia 6 x86/x64 CPU architecture: the stack & stack framesyuriygeorgiev 17 CPU Registers x86-64OSDev Wiki

Die seviyesine biraz zoom yapalım. Paket altındaki silikon kalıpsilikon kalıpSilikon Kalıp (Silicon Die) — transistörlerin ve devrelerin fiziksel olarak kazındığı ince silikon dilim. CPU chip'ini oluşturan parça. üzerinde çekirdekler var. Her çekirdeğin içinde cache, ALU, branch unit ve register fileregister fileRegister File — CPU çekirdeğinin içindeki hızlı dahili register'ların bloğu. PC, SP ve diğer CPU register'ları bu bölgede fiziksel olarak bulunur. diye bir blok. Register file, RAX’ın, RBX’in, RSP’nin ve PC’nin yaşadığı mahalle. Wikipedia+1 2 sources 7 Register fileWikipedia 18 The Convoluted Way Intel's 386 Implemented Its RegistersHackaday

Intel Sandy Bridge per-core floor plan — register file, branch unit ve cache’in çekirdek içinde nereye oturduğunu net gösteriyor
Intel Sandy Bridge per-core floor plan — register file, branch unit ve cache’in çekirdek içinde nereye oturduğunu net gösteriyor

Register file tek bir blok değil; satır–sütun dizilmiş bir grid gibi düşünebilirsin. Her hücre 0 veya 1 tutan küçücük bir flip-flop; her satır 64 bitlik bir register. Mimari dokümanlarda ezbere okuduğumuz “RSP”, “RIP”, “RAX” isimleri, bu gridin satır etiketleri aslında. Wikipedia+2 3 sources 7 Register fileWikipedia 19 FlipFlops and RegistersTechTop 20 Flip-flops and registersCPUville

Register file bir grid olarak — her satır bir register, her hücre tek bir biti tutan flip-flop
Register file bir grid olarak — her satır bir register, her hücre tek bir biti tutan flip-flop

RSP = 0x7ffeefbff5c0 dediğinde, o satırdaki 64 flip-flop’un hepsi bu adresin bitlerini tutuyor. call çalıştığında RSP’nin bit pattern’i değişiyor; ret geldiğinde bir önceki pattern’e geri dönüyor. Stack’te bir seviye yukarı çıkmak, die’ın o köşesinde fiziksel bir satırın değer değiştirmesi demek.

Bu detayı şunun için anlatıyorum: “return zorunlu çünkü dil böyle istiyor” yüzeyinde kalmasın diye. Asıl gerçek, “Bu fonksiyon bittiğinde, şu register satırına şu adres pattern’ini geri yazmak zorundayım” cümlesi.


Fonksiyonun gerçek maliyeti ve derleyicinin sessiz cevabı #

Buradan şuraya dönelim: Fonksiyon maliyetsiz bir soyutlama değil.

Her call / ret şunu gerektiriyor:

Kodunu çok küçük fonksiyonlara böldüğünde, fiziksel seviyede şunu söylemiş oluyorsun: “PC ve SP’yi daha sık zıplatacağız, stack daha sık şişecek.” Bunun karşılığında kazanımın okunabilirlik, test edilebilirlik, tekrar kullanılabilirlik.

Peki buna bakıp “O zaman her şeyi tek fonksiyonda yazalım, hiç call olmasın” mı demeliyiz? Hayır. Onun da bedeli var: devasa bir gövde, debug etmesi zor. Bu takası diller senin sorumluluğundan çok uzun zaman önce belli bir noktaya kadar çıkartmayı başardı.

Modern derleyiciler burada devreye giriyor. Küçük, sık kullanılan fonksiyonları çoğu zaman inline ediyorlar: fonksiyon gövdesini gerçekten çağırmak yerine, doğrudan çağrı noktasına yapıştırıyorlar. call / ret ortadan kalkıyor, SP ve PC daha az zıplıyor, binary biraz şişiyor ama sıcak path hızlanıyor. StackOverflow+3 4 sources 2 How does a function call work?StackOverflow 22 Compiler Optimization TechniquesAalto OpenCS 23 Compiler OptimisationUCL 24 x86 Assembly and Call Stack (CS161 textbook)UC Berkeley

Sen mental modelini şöyle kurabilirsin:

  • Anlamlı fonksiyonlara bölmekten korkma.
  • Ama “her iki satıra bir fonksiyon” da yazma; gereksiz abstraction da stack’i şişiriyor.
  • Derleyici “küçük ve sıcak” fonksiyonları zaten inline etmeye çalışıyor; abarttığında ise gerçek call maliyetiyle yüzleşmek zorunda kalıyorsun.

Debug build’lerde inlining genelde kapalıdır. O yüzden debug’da tertemiz call stack görürken, release build’de aynı fonksiyonların “kaybolması” normal — derleyici onları çağrı seviyesinde değil, instruction seviyesinde çözüyor. UC Berkeley+1 2 sources 24 x86 Assembly and Call Stack (CS161 textbook)UC Berkeley 25 Intel 64 and IA-32 Architectures Optimization Reference ManualIntel


Nereye geldik, sırada ne var? #

En dipten başladık: register file’daki satır olarak PC ve SP, stack ile heap’in bellekteki konumu, stack overflow’un neye çarptığı. Aynı fonksiyon çağrısını dört katta gezdik:

  • Teori katı: FSM — state’ten state’e geçen bir hesaplama, fonksiyonun “kaldığın yere geri bırakma” zorunluluğu.
  • CPU katı: call ve ret talimatları, PC ve SP’nin nasıl hareket ettiği.
  • Donanım katı: Register file içindeki fiziksel satırlar, flip-flop’lar.
  • Derleyici / geliştirici katı: Inlining, fonksiyon maliyeti, abstraction miktarı.

Buradan sonra “Fonksiyon dönüyor işte” deyip geçmek içime sinmiyor. Yazdığın her return, die’ın bir köşesinde bir register satırını oynatıyor; RAM’de belirli bir hücreye dokunuyor; branch predictor’ın kafasındaki grafiği güncelliyor.

Bu yazıyı okuduğunda şunu düşünmeni istiyorum:

Madem her çekirdeğin tek bir PC’si ve her thread’in kendi stack’i var, paralelizm nasıl yürüyebiliyor?

Bir sonraki yazı bu olacak. Thread başına ayrı stack, core başına register set, context switch’te PC ve SP’nin nasıl save/restore edildiği, branch prediction ve Return Address Stack’in bu işin neresinde durduğu… Kısacası:

Aynı anda bir sürü fonksiyon çağrısını nasıl yürütüyoruz?

Onu da CPU’nun masasından anlatmaya çalışacağız. Wikipedia+3 4 sources 1 Call stackWikipedia 3 Program Counter and Instruction RegisterBaeldung 6 x86/x64 CPU architecture: the stack & stack framesyuriygeorgiev 26 Branch Target Buffer, Return Address StackUCSD CS141


Teşekkür #

UC Berkeley’in açık ders içerikleri, yazdıklarımın ana kaynağı oldu. Computer architecture, CPU internals, stack/register/bellek hiyerarşisi… Bunları sonradan, iş hayatının ortasında öğrenmeye çalışan biri için bu kadar kaliteli içeriğe ücretsiz erişebilmek büyük lüks.

🎥 Bu yazıyı şekillendiren seri:
UC Berkeley — Computer Architecture (YouTube playlist)

Runaho
Runaho
Software. Philosophy. The space between.

I’ve been writing software since 2015 — mostly in startups, mostly under pressure. I care about performance, security, observability, and maintainability. But what I spend the most time on is thinking about why any of it matters.