Ana içeriğe geç

Bölüm 9:Tahmin, Spekülasyon ve Spectre

8 dakikalık okumaGüncelleme:

Bir önceki bölümde işlemcinin aynı anda onlarca talimatı havada tuttuğunu gördük. Ama o resmin ortasında bilerek bir boşluk bıraktım.

Pipeline’ın dolu kalması için CPU’nun sıradaki talimatı önceden getirmesi gerekir. Peki kodda bir if varsa? Koşulun sonucu daha hesaplanmamışken hangi tarafı getireceksin?

İşlemcinin bu soruya verdiği cevap “beklerim” değil. Cevap şu: tahmin et ve işe koyul. Bu bölümde önce o tahmin makinesini, sonra da onun modern bilgisayarların en ünlü güvenlik açığına nasıl dönüştüğünü göreceğiz.

Bu bölümde neyi çözüyoruz?

  • CPU’nun bir dallanmanın sonucunu nasıl tahmin ettiğini göreceğiz.
  • Yanlış tahminin bedelini ve spekülatif yürütmenin ne olduğunu anlayacağız.
  • Spectre ile Meltdown’ın neden bir yazılım hatası değil, bir tasarım sonucu olduğunu çözeceğiz.
  • Bir soyutlamanın neden en çok hızlandığı yerden sızdığını göreceğiz.

Branch Prediction: Geleceği Tahmin Etmek

Programlar sık sık koşullu dallanmalar kullanır (if, while, for). Pipeline’ın verimli çalışması için CPU bir sonraki talimatı önceden getirmelidir. Ama dalın nereye gideceğini bilmeden ne getireceksin?

Branch prediction (dal tahmini) devreye girer. CPU, geçmişteki dallanma davranışlarına bakarak “bu dal muhtemelen atlayacak” veya “atlamayacak” diye tahminde bulunur.

Modern CPU’ların branch predictor’ları %95+ isabet oranına ulaşabilir. Yanlış tahmin edilirse pipeline boşaltılır (flush) ve doğru yoldan devam edilir; bu ceza onlarca saat çevrimi sürebilir.

Bu cezanın neye benzediğini de aynı şemayla görebiliriz. Bu kez 3. talimat koşullu bir dallanma:

Koşullu dallanmanın pipeline üzerindeki etkisi. 3. talimat dallanma olduğu için 4-7. talimatlar yarıda kesiliyor ve pipeline 15. talimattan yeniden doluyor; aradaki boşluk dal cezası olarak işaretli.

Kaynak: William Stallings, Computer Organization and Architecture, 10. baskı, Şekil 14.11.

4’ten 7’ye kadar olan talimatların yarıda kesildiğine dikkat et — hiçbiri tamamlanmıyor. İşlemci onları çoktan pipeline’a almıştı, çünkü dallanmanın nereye gideceğini 7. çevrime kadar bilemedi. Öğrendiği anda o dört talimatın tamamı çöpe gidiyor ve pipeline 15. talimattan sıfırdan doluyor.

Şemadaki Branch Penalty aralığı işte bu israfın adı. Pipeline ne kadar derinse, yanlış tahmin anında çöpe giden iş de o kadar artar. Branch prediction’ın neden bu kadar önemsendiği tek cümleyle buradadır: doğru tahmin bedavadır, yanlış tahmin bu boşluğu ödetir.

DerinleşmeTahmin nasıl hatırlıyor? 2-bit saturating counter

Dinamik branch prediction’da en yaygın mekanizmalardan biri 2-bit saturating counter’dır. Bu sayaç, bir dalın geçmişteki davranışına göre dört durumdan birinde bulunur:

  • Strongly Not Taken — Kesin atlamaz
  • Weakly Not Taken — Büyük ihtimalle atlamaz
  • Weakly Taken — Büyük ihtimalle atlar
  • Strongly Taken — Kesin atlar

Dal atlarsa sayaç bir artar, atlamazsa bir azalır. Sınır değerlerde “sıkışır” (saturates); bu yüzden tek bir yanlış tahmin hemen yön değiştirmez. Bu, kısa döngülerdeki dal davranışındaki gürültüye karşı dayanıklılık sağlar.

Dört durumun birbirine nasıl bağlandığını tabloda görmek işi tamamen netleştiriyor:

DurumTahminiDal atlarsaDal atlamazsa
Strongly Not Takenatlamaz→ Weakly Not TakenStrongly Not Taken
Weakly Not Takenatlamaz→ Weakly Taken→ Strongly Not Taken
Weakly Takenatlar→ Strongly Taken→ Weakly Not Taken
Strongly TakenatlarStrongly Taken→ Weakly Taken

Uçlardaki iki durumun kendine dönmesi kasıtlıdır: sayaç orada doyuma ulaşır. Bu sayede yüz kez atlamış bir dal, tek bir istisnada tahminini değiştirmez — yalnızca bir kademe zayıflar. Döngülerin son turu her seferinde yanlış tahmin edilir ama döngünün geri kalanı bundan etkilenmez.

Spekülatif Yürütme ve Güvenlik

Devam etmeden önce iki farklı “durum” arasındaki ayrımı yapmam gerekiyor, çünkü bölümün geri kalanı bunun üzerine kuruluyor.

Mimari durum, programın görmesine izin verilen şeydir: register’ların değerleri, belleğin içeriği. Mikro-mimari durum ise programın göremediği, işlemcinin kendi iç defteridir: hangi satır cache’e girdi, tahmin edici ne öğrendi, hangi tampon doldu.

Şimdi bölümün en önemli cümlesi: spekülatif yürütme mimari durumu geri alabilir, mikro-mimari durumu geri alamaz. Spectre’ın tamamı burada saklı.

Tahmin edilen dalın talimatlarını spekülatif olarak yürütme (speculative execution), tahmin doğru çıkana kadar mimari sonuçları geçici tutmak demektir. Tahmin doğruysa sonuçlar görünür hâle gelir, yanlışsa mimari durum geri alınır. Sorun şu: geri alınan spekülatif yürütme bile cache gibi mikro-mimari yapılarda ölçülebilir iz bırakabilir.

2018’de duyurulan Spectre ve Meltdown ailesi açıklar bu farkı görünür yaptı:

Bu açıkların ardından işlemci üreticileri ve işletim sistemleri birkaç ayrı savunma geliştirdi: mikrokod güncellemeleri işlemcinin davranışını sahada değiştirdi, KPTI kernel’ın bellek haritasını kullanıcı programlarının görüş alanından çıkardı, site isolation tarayıcıda her siteyi ayrı bir process’e taşıdı, retpoline ise dolaylı dalları tahmin edilemez hâle getiren bir derleyici hilesi olarak devreye girdi. Bu savunmalardan hangilerinin senin makinende açık olduğunu tahmin etmene gerek yok; bölümün sonunda tek komutla nasıl kontrol edeceğini göstereceğim.

DerinleşmeMeltdown yamalandı, Spectre neden yamalanamıyor?

İki açık aynı anda duyuruldu ve aynı mekanizmadan yararlanıyor gibi göründü. Ama aralarında temel bir fark var ve bu fark, birinin çözülüp diğerinin çözülememesini açıklıyor.

Meltdown bir hataydı. Bazı Intel tasarımlarında izin kontrolü, spekülatif erişimden sonra yapılıyordu. Yani CPU önce kernel belleğini okuyor, sonra “aslında buna hakkın yokmuş” diyerek sonucu iptal ediyordu — ama okunan değer o esnada cache’i etkilemiş oluyordu. Bu, mimarinin gerektirdiği bir davranış değildi; bir sıralama hatasıydı. Nitekim aynı dönemin AMD ve birçok ARM tasarımı bu açıktan etkilenmedi; sonraki nesil Intel çipleri de izin kontrolünü öne alarak sorunu doğrudan donanımda kapattı. Yazılım tarafındaki yama ise KPTI oldu (Kernel Page Table Isolation): kernel’ın bellek haritası, kullanıcı programı çalışırken haritadan tamamen çıkarılır. Pahalı olmasının sebebi de bu — artık her syscall’da sayfa tablosunun baştan kurulması gerekiyordu ve syscall yoğun iş yüklerinde bunun bedeli %5 ile %30 arasında değişebiliyordu.

Spectre bir hata değil. İşlemci tam olarak tasarlandığı gibi davranıyor: dalı tahmin ediyor, tahmin ettiği yolu spekülatif olarak yürütüyor, yanılırsa mimari durumu geri alıyor. Sorun şu ki mimari durumu geri almak, mikro-mimari izleri geri almaz. Cache’e giren satır cache’te kalır ve zamanlama ölçümüyle görülebilir.

Bunu “düzeltmek” için spekülasyonun cache üzerinde hiçbir gözlenebilir iz bırakmaması gerekir. Ama spekülasyonun bütün amacı veriyi önceden getirmektir; izi silmek, faydayı da silmek demektir. Yani Spectre’ın kökü bir kusur değil, performans ile izolasyon arasındaki temel gerilimdir.

Bu yüzden savunmalar açığı kapatmaz, sömürülmesini zorlaştırır: retpoline, hedefi ancak çalışma anında belli olan dolaylı dallara (fonksiyon pointer’ları, sanal metot çağrıları) uygulanır: tahmin ediciyi kasten yanıltıp o dalı tahmin edilemez hâle getirir. lfence talimatı ise konduğu noktada işlemciye “buradan öteye spekülatif geçme” der, tarayıcılar zamanlayıcıların çözünürlüğünü kasten düşürür, bulut sağlayıcıları güvenilmeyen iş yüklerini ayrı çekirdeklere alır. Hepsinin bir performans bedeli vardır ve hiçbiri kesin değildir; nitekim aradan geçen yıllarda aynı aileden yeni varyantlar (Spectre-BHB, Retbleed, Downfall, Inception) düzenli aralıklarla yayımlandı.

Buradan çıkan ders bu kitabın en önemli fikirlerinden biri: bir soyutlamanın sızdırdığı yer, çoğu zaman onu hızlandıran yerdir. Cache, program için görünmez olmalıydı; zamanlama onu görünür kılıyor. Sanal bellek izolasyon vaat ediyordu; paylaşılan donanım o vaadi delik bırakıyor. Performans için kurulan her paylaşım, aynı zamanda bir yan kanaldır.

Bu bölümün en önemli beş adımını tek bir şemada topladım. Özellikle son ikisine dikkat et: geri alınan şey ile geri alınmayan şey.

Spekülatif yürütme ve geri alınmayan iz
koşullu dallanmaif (i < n)koşul henüz hazır değiltahminci"atlayacak"yürütülüyorbellekten okuyan etkiMİMARİ DURUMregister'lar, bellek, bayraklarprogramın gördüğü her şeyMİKRO-MİMARİ DURUMcache satırları, tahmin tamponlarıprogramın göremediği her şeyGERİ ALINDIsanki hiç olmadıGERİ ALINMADIiz orada duruyorzamanlamayı ölçen biri o izi okuyabilir
Koşula gelinir. Kodda bir `if` var ve koşulun sonucu henüz hesaplanmadı. Değer bellekten geliyorsa bu bekleyiş yüzlerce çevrim sürebilir.
  1. Koşula gelinir. Kodda bir `if` var ve koşulun sonucu henüz hesaplanmadı. Değer bellekten geliyorsa bu bekleyiş yüzlerce çevrim sürebilir.
  2. Tahminci karar verir. CPU beklemez. Aynı dalın geçmişte ne yaptığına bakar ve bir yön seçer. Modern işlemcilerde bu tahmin %95'in üzerinde tutar.
  3. Tahmin edilen yol yürütülür. İşlemci tahmin ettiği daldaki talimatları gerçekten çalıştırır. Yol boyunca bellekten okunan her veri, yan etki olarak cache'e girer.
  4. Koşul çıkar: tahmin yanlış. Mimari durum geri alınır. Reorder buffer'daki sonuçlar atılır, pipeline boşaltılır, doğru yoldan yeniden başlanır. Programın gördüğü sonuç kusursuzdur — sanki hiç olmamış gibi.
  5. Ama cache'teki iz kalır. Geri alınmayan tek şey cache'in içeriği. Hangi satırın oraya girdiğini, erişim sürelerini ölçerek anlamak mümkündür. Spectre ve Meltdown'ın tamamı bu tek cümleden doğar.

Bunların İzi Kendi Makinende Duruyor

Bu bölümde anlattıklarım soyut kalmasın: hepsinin izi şu anda önünde duran makinede var.

Linux’ta lscpu komutu işlemcinin modelini, çekirdek sayısını ve çekirdek başına kaç thread çalıştığını (yani SMT açık mı) tek bakışta gösterir. Aynı komutun uzun çıktısında işlemcinin desteklediği özelliklerin listesi de yer alır ve o liste güzel bir tarih dersidir: avx2 SIMD kuşağını, ht hyper-threading’i, nx 15. bölümde göreceğimiz çalıştırılamaz sayfa bitini gösterir. pti gördüğünde ise doğrudan bu bölümün konusuna bakıyorsun demektir — Meltdown’a karşı alınan sayfa tablosu izolasyonu açık.

Yani o listede yalnızca yetenekler değil, geçmişte yaşanmış açıkların izleri de duruyor.

Bir adım daha ileri gitmek istersen ölçülecek tek bir sayı var: IPC (Instructions Per Cycle), yani saat çevrimi başına biten talimat sayısı. IPC birin üzerindeyse işlemci gerçekten superscalar çalışıyor, aynı çevrimde birden fazla talimat bitiriyor demektir. Birin altındaysa pipeline boş bekliyordur: ya cache’ten veri gelmiyordur, ya da bir dal tahmini tutmamıştır. Bu bölümü okuduktan sonra o tek sayıyı gördüğünde arkasında ne olduğunu biliyor olacaksın.

Özet

Peki, ne öğrendik?

  • Branch prediction, dallanmanın yönünü geçmiş davranışa bakarak tahmin eder ve modern işlemcilerde %95’in üzerinde isabet tutturur.
  • Yanlış tahminin bedeli pipeline’ın boşaltılmasıdır; onlarca çevrim çöpe gider.
  • Spekülatif yürütme, tahmin edilen yolu koşul daha hesaplanmadan yürütmektir. Tahmin yanlışsa mimari sonuçlar geri alınır — ama cache’te bıraktığı iz geri alınmaz.
  • Spectre ve Meltdown tam olarak o izin okunabilmesinden doğar. Bir yazılım hatası değil, performans için kurulmuş paylaşımın doğal sonucudurlar.
  • Bu bölümün tek cümlelik dersi: bir soyutlamanın sızdığı yer, çoğu zaman onu hızlandıran yerdir.

Artık CPU’ya bir daha “talimatları sırayla yürüten motor” diye bakamazsın. Baktığın şey, her çevrimde onlarca talimatı havada tutan, geleceği tahmin eden ve bu tahminlerin izini cache’te bırakan bir makine.

Donanım tarafını burada kapatıyoruz. Sıradaki bölümden itibaren yazılım tarafına geçiyor ve tek bir soruyu sonuna kadar takip ediyoruz: bir program çalıştırdığında tam olarak ne oluyor?

10. bölüme devam et: Bir Program Nasıl Çalıştırılır?