Ders 09 / 16
Komutlar ve Program Sayacı
Komutların bit örüntüsü olarak kodlanması, getir–çöz–yürüt döngüsü ve program akışının oluşumu.
İçindekiler
Önceki ders, verinin bellekte adreslenmiş baytlar olarak durduğunu kurdu. Komutlar da aynı bellekte, aynı biçimde durur: bir komut, belirli bir kurala göre kodlanmış bit örüntüsünden ibarettir. Program ile veri arasındaki ayrım donanımda değil, o baytlara uygulanan yorumdadır.
Bu dersin sorusu şudur: işlemci, bellekteki bir bit örüntüsünü nasıl bir eyleme çevirir ve bir sonraki eylemi nasıl belirler?
Komut Kümesi
Her işlemci ailesinin tanımlı bir komut kümesi (instruction set) vardır: hangi işlemlerin var olduğunu, her birinin hangi bit örüntüsüyle kodlandığını ve hangi işlenenleri aldığını belirten sözleşme. Komut kümesi, donanım ile yazılım arasındaki arayüzdür; aynı komut kümesini uygulayan iki farklı işlemci, aynı programı çalıştırabilir.
Tipik komut sınıfları azdır:
- Veri taşıma — bellekten yazmaca yükleme, yazmaçtan belleğe saklama.
- Aritmetik ve mantık — toplama, çıkarma, karşılaştırma, bit işlemleri.
- Akış denetimi — dallanma, alt yordam çağrısı ve dönüş.
- Sistem — kesme, ayrıcalık düzeyi değiştirme, işletim sistemi çağrısı.
Bir programlama dilindeki her yapı, sonunda bu sınıflardan komutlara indirgenir.
Bir Komut Nasıl Kodlanır
Komut kodlaması, bit alanlarına bölünmüş bir örüntüdür. Alanlardan biri işlem kodu (opcode), hangi işlemin yapılacağını söyler; diğerleri işlenenleri belirtir.
Yapıyı somutlaştırmak için, yalnızca öğretim amacıyla tanımlanmış 32 bitlik bir kodlama düşünün. Bu kodlama gerçek bir işlemciye ait değildir; alan yerleşimi her mimaride farklıdır.
| Bit aralığı | Alan | Genişlik |
|---|---|---|
| 31–24 | işlem kodu | 8 bit |
| 23–16 | hedef yazmaç | 8 bit |
| 15–8 | birinci kaynak | 8 bit |
| 7–0 | ikinci kaynak | 8 bit |
Kursun ortak örneği bu kurala göre çözülürse:
0x41 işlem kodunun “topla” anlamına geldiği varsayılırsa, komut şunu söyler: 67 ve 68
numaralı yazmaçların içeriğini topla, sonucu 66 numaralı yazmaca yaz.
Aynı otuz iki bit, bu konuya kadar dört kez yorumlandı: tam sayı, kayan noktalı sayı, metin ve şimdi komut. Örüntüyü hangi yorumun karşılayacağına, işlemcinin o baytları nereden okuduğu karar verir — komut olarak mı, veri olarak mı.
Sabit uzunluklu kodlamalar (her komut aynı genişlikte) çözme aşamasını basitleştirir. Değişken uzunluklu kodlamalar ise sık kullanılan komutları daha az bayta sığdırarak program boyutunu küçültür. İki yaklaşım da yaygındır ve seçim, mimarinin tasarım önceliklerini yansıtır.
Makine Dili ve Çevirici Dili
Bit örüntülerini elle yazmak hataya açıktır. Çevirici dili (assembly), her komuta okunabilir bir ad veren birebir gösterimdir:
yukle r1, [1000] ; 1000 adresindeki değeri r1'e yükle
yukle r2, [1004]
topla r3, r1, r2 ; r3 = r1 + r2
sakla [1008], r3
Çevirici dili ile makine dili arasındaki ilişki neredeyse birebirdir: her satır bir komuta çevrilir. Yüksek seviyeli dillerdeki bir satır ise onlarca komuta karşılık gelebilir; bu çeviriyi derleyici yapar ve konunun son dersinde ele alınır.
Getir–Çöz–Yürüt Döngüsü
İşlemci, açıldığı andan kapanana kadar tek bir döngüyü tekrarlar:
- Getir (fetch). Program sayacının gösterdiği adresten komut okunur.
- Çöz (decode). Bit alanları ayrıştırılır; işlem kodu ve işlenenler belirlenir.
- Yürüt (execute). İşlem, aritmetik ve mantık birimi veya bellek birimi tarafından gerçekleştirilir.
- Sonucu yaz. Üretilen değer hedef yazmaca veya belleğe yazılır.
- Program sayacı bir sonraki komuta ilerletilir ve döngü baştan başlar.
Program sayacı (program counter), bir sonraki komutun adresini tutan özel bir yazmaçtır. Sıradan komutlarda her adımda komut genişliği kadar artar; program bu yüzden “yukarıdan aşağı” akar.
Bu döngü, işlemcinin yaptığı her şeyin tanımıdır. Bir işletim sistemi, bir tarayıcı ya da bir oyun arasındaki fark, döngüde değil, döngünün okuduğu komut dizisindedir.
Adımların Örtüşmesi
Döngünün adımları farklı donanım birimlerini kullanır: getirme bellek arayüzünü, çözme denetim birimini, yürütme aritmetik birimini meşgul eder. Her adımı sırayla beklemek, bu birimlerin çoğunu her an boşta bırakır.
Boru hattı (pipeline) bu boşluğu kapatır: bir komut yürütülürken bir sonraki çözülür, üçüncüsü getirilir. Tek bir komutun tamamlanma süresi değişmez; birim zamanda tamamlanan komut sayısı artar.
Boru hattının bedeli dallanmadır. Koşullu dallanmada bir sonraki komutun adresi, koşul hesaplanana kadar bilinmez; boru hattına hangi komutların alınacağı belirsizdir. İşlemci bir tahminde bulunur ve tahmin yanlış çıkarsa hattaki komutları atar. Tahmin edilmesi güç dallanmalar bu yüzden ölçülebilir bir maliyet taşır.
Bu, ilk konudaki bir gözlemi tekrarlar: komut sayısı, sürenin tek belirleyicisi değildir.
Akışın Değiştirilmesi
Program sayacına doğrudan yazılabilmesi, tüm denetim yapılarının kaynağıdır.
Koşulsuz dallanma, program sayacına yeni bir adres yazar; akış oradan devam eder. Koşullu dallanma ise önce bir karşılaştırma sonucuna bakar. Karşılaştırma komutları, sonucu bayrak bitlerine yazar — sonuç sıfır mı, negatif mi, taşma oldu mu. Koşullu dallanma bu bayrakları okur ve program sayacını yalnızca koşul sağlanıyorsa değiştirir.
Yüksek seviyeli yapıların karşılığı böylece kurulur:
| Yapı | Komut karşılığı |
|---|---|
if |
Karşılaştır, koşul sağlanmıyorsa bloğun sonuna dallan |
while |
Koşulu sınayan dallanma + gövde sonunda başa dönen dallanma |
| Fonksiyon çağrısı | Dönüş adresini sakla, hedefe dallan; dönüşte saklanan adrese dallan |
Fonksiyon çağrısında dönüş adresinin nereye saklandığı, çağrı yığıtının işlevidir; yığıt düzeni konunun son dersinde ele alınacaktır.
Aşağıdaki program, getir–çöz–yürüt döngüsünü öğretim amaçlı kodlama üzerinde canlandırır:
ISLEM_ADLARI = {0x41: "topla", 0x42: "cikar", 0xFF: "dur"} def coz(komut: int) -> tuple[str, int, int, int]: """32 bitlik komutu alanlarına ayırır.""" islem = (komut >> 24) & 0xFF hedef = (komut >> 16) & 0xFF kaynak1 = (komut >> 8) & 0xFF kaynak2 = komut & 0xFF return ISLEM_ADLARI.get(islem, "bilinmeyen"), hedef, kaynak1, kaynak2 def calistir(bellek: list[int], yazmac: dict[int, int]) -> None: ps = 0 # program sayacı while ps < len(bellek): islem, hedef, k1, k2 = coz(bellek[ps]) if islem == "dur": break if islem == "topla": yazmac[hedef] = yazmac.get(k1, 0) + yazmac.get(k2, 0) elif islem == "cikar": yazmac[hedef] = yazmac.get(k1, 0) - yazmac.get(k2, 0) ps += 1 # bir sonraki komuta ilerle print(coz(0x41424344)) # ('topla', 66, 67, 68) bellek = [0x41424344, 0x42024344, 0xFF000000] yazmac = {67: 10, 68: 4} calistir(bellek, yazmac) print(yazmac[66], yazmac[2]) # 14 6
ps += 1 satırı, program sayacının sıradan ilerlemesidir. Bir dallanma komutu
eklenseydi, yaptığı tek şey bu değişkene farklı bir değer yazmak olurdu.
Özet
- Komutlar da veriler gibi bellekte durur; ayrım, baytlara uygulanan yorumdadır.
- Komut kümesi, donanım ile yazılım arasındaki sözleşmedir ve az sayıda komut sınıfı içerir.
- Bir komut, işlem kodu ve işlenen alanlarına bölünmüş bir bit örüntüsüdür; alan yerleşimi mimariye özgüdür.
- İşlemci getir–çöz–yürüt döngüsünü sürekli tekrarlar; programlar arasındaki fark döngüde değil, okunan komut dizisindedir.
- Program sayacı bir sonraki komutun adresini tutar; koşullu ve koşulsuz dallanma bu yazmacı değiştirerek tüm denetim yapılarını üretir.
Sonraki Adım
Döngünün her turu belleğe en az bir erişim gerektirir ve önceki derste görüldüğü gibi ana bellek, işlemciye göre yüzlerce kat yavaştır. Sonraki ders, bu farkı kapatan önbellek katmanını ve önbelleğin verimli çalışması için programın uyması gereken erişim düzenini ele alacak.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.