İçeriğe geç
academia.sh

Ders 12 / 15

Tanımsız Davranış

Sarkan işaretçi okumak, bırakılmış belleği kullanmak, bir bloğu iki kez bırakmak ve dizinin sınırı dışına erişmek dördü de tanımsız davranıştır; hiçbiri koşturulmaz, ders her birini düzyazıda tarif eder ve yanına tanımlı karşılığını koşturarak gösterir.

Yayın Güncelleme

Bu derste

  • Tanımsız davranışın dört kaynağını (sarkan işaretçi, bırakılmış bellek, çift bırakma, sınır dışı erişim) sözleşme ihlali olarak tanır
  • Tanımsız davranışın neden var olduğunu derleyicinin varsayım yapma hakkıyla açıklar
  • Her tanımsız kaynağın yerine tanımlı bir karşılığı yazar ve koşturarak doğrular
İçindekiler

Önceki ders dört bölgeyi tek eksende topladı: yığıt, statik alan, dizgi değişmezi ve öbek — hepsinin ortak noktası, ömrünü kimin belirlediğinin kaynakta yazılı olmasıydı. Bu ders o sözleşmenin kırıldığı anı ölçüyor. Sözleşme “bu bloğu kim bırakacak” sorusuna bir yanıt veriyordu; sözleşme kırılınca ortaya çıkan şey bir hata mesajı değil, tanımsız davranıştır (undefined behavior) — ve tanımsız davranışın en önemli özelliği, ölçülemeyecek olmasıdır.

Bu son cümle bir uyarı değil, dersin sınırını çizen bir tanımdır. Aşağıdaki dört bölüm tanımsız davranışın dört ayrı kaynağını anlatıyor — sarkan işaretçi, bırakılmış bellek, çift bırakma, sınır dışı erişim — ama hiçbiri kaynağın kendisini koşturmuyor. Her bölümün yanında koşan kod, o kaynağın yerine geçen tanımlı karşılıktır: sözleşme kırılmadan önce araya giren bir denetim satırı. Bu ayrım kurs boyunca hep aynıdır — ölçülen şey her zaman tanımlı davranıştır — ama bu derste özellikle önemlidir, çünkü ölçülmeyen tarafın adı da düzyazıda açıkça geçiyor.

Tanımsız Davranış Neden Var

C standardı her ifadenin anlamını tanımlamaz. Bir kısım ifade tanımlıdır: int toplam = a + b; her zaman aynı kuralla çalışır. Bir kısmı uygulamaya bağlıdır (implementation-defined): derleyici bir seçim yapar ve o seçimi belgeler. Ama bir kısım ifade tanımsızdır: standart onun ne yapacağını hiç söylemez — derleyici istediği gibi davranabilir, hatta hiç davranmayabilir.

Bu boşluk bir eksiklik değil, bir tasarım kararıdır. Derleyici, bir programın tanımsız davranışa hiç girmediğini varsayarak optimizasyon yapar. Örneğin bir işaretçinin her zaman geçerli bir bloğu gösterdiğini varsayabilir, çünkü geçersiz bir bloğu göstermesi zaten tanımsızdır ve standart bu durumda hiçbir garanti vermez. Bu varsayım sayesinde derleyici gereksiz denetimleri silebilir, sırayı değiştirebilir, bir değeri yeniden hesaplamak yerine önbelleğe alabilir. Varsayım yanlış çıkarsa — yani program gerçekten tanımsız davranışa girerse — üretilen kod hâlâ derlenir ve hâlâ bir şey yapar, ama o şeyin ne olduğu standardın konusu değildir.

Bu varsayımın etkisi derleyici sürümüyle, optimizasyon düzeyiyle ve hatta çevredeki kodun şekliyle değişir; aynı tanımsız ifade bir derlemede fark edilmeden geçebilir, bir başkasında programın tamamen farklı bir yol izlemesine yol açabilir. Standart bu iki sonucu da eşit derecede geçerli sayar, çünkü ikisi de “tanımsız” sözünün kapsamındadır. Bu yüzden tanımsız davranışı bulmanın yolu programı çalıştırıp “hata verdi mi” diye bakmak değildir — hata vermemesi hiçbir şey kanıtlamaz, yalnız o belirli koşumda, o belirli derleyicide bir şey gözlenmediğini gösterir.

Ölçüm çekirdeğinin dört sınıfı arasında bu dersin konusu sahipsiz bellektir: sözleşme kırıldığında ne dil, ne çağıran, ne de çağrılan artık o bloğa tanımlı bir yoldan erişemiyordur — blok hâlâ orada durabilir, ama onu güvenle kullanacak hiçbir taraf kalmaz. Bu yüzden aşağıdaki dört kaynağın hiçbiri “kim sahipleniyor” sorusuna doğrudan bir yanıt vermez; hepsi, sahiplik sözleşmesi zaten çiğnendikten sonraki anı gösterir.

Bu yüzden “çalışıyor görünmesi” hiçbir kanıt taşımaz. Bir sonraki bölüm, bu depoda ölçüm çekirdeğinin dört bilerek koşturmadığı kaynağı tek tek ele alıyor; her birinde önce sözleşme neden kırıldığı düzyazıyla anlatılıyor, sonra yanına tanımlı bir karşılığı koşturuluyor.

Sarkan İşaretçiyi Okumak

Bir blok bırakıldıktan sonra ona işaret eden işaretçi sarkar (dangling): işaretçinin kendisi hâlâ eski adresi tutar, ama o adresteki bellek artık ayırıcıya geri verilmiştir. Sarkan bir işaretçiyi dolaylamak (*p) — okumak amacıyla bile olsa — tanımsız davranıştır. Bellek fiilen hâlâ aynı içeriği taşıyor gibi görünebilir, çünkü ayırıcı o bloğu henüz başka bir isteğe vermemiş olabilir; bu rastlantıdır, garanti değildir. Aynı program bir sonraki koşumda, ya da aynı koşumda bir sonraki ayırma isteğinden sonra, o adresi tamamen farklı bir bloğa verebilir.

Ders bu okumayı hiç koşturmuyor. Yerine, sarkan işaretçinin oluşmasını engelleyen tanımlı kalıbı koşturuyor: bir blok bırakıldığı anda işaretçiye NULL atamak ve her kullanımdan önce bunu denetlemek.

// koru.c — sarkan isaretcinin olusmasini engelleyen tanimli karsilik
#include <stdlib.h>

const char *sarkan_karsiligi(void) {
    int *blok = malloc(sizeof *blok);
    if (blok != NULL) {
        *blok = 9;
    }
    free(blok);
    blok = NULL;
    return (blok == NULL) ? "isaretci bos, dolaylama yapilmadi" : "erisildi";
}

Bu karşılığın ölçümü, dersin tek main’ini içeren son kod bloğunda, öbür üç karşılıkla birlikte gösteriliyor.

Dikkat edilmesi gereken nokta şu: blok’u NULL’a çekmek, işaretçinin kendisini değiştirir, işaret ettiği belleği değil — o bellek free çağrısıyla zaten ayırıcıya geri döndü. Denetim yalnız aynı işaretçi değişkeninin bir daha kullanılmasını engelliyor; aynı adresi tutan başka bir işaretçi varsa (örneğin aynı bloğa işaret eden ikinci bir değişken), o ikinci işaretçi hâlâ sarkar ve bu kalıp onu korumaz. Kalıbın kapsamı, tam olarak denetlediği işaretçi değişkeniyle sınırlıdır.

Bırakılmış Belleği Kullanmak

Bırakılmış belleği kullanmak (use-after-free), sarkan işaretçiyi okumaktan biraz daha geniştir: bloğa yazmayı da kapsar. free çağrısından sonra aynı bloğa *p = değer; yazmak, ayırıcının o bellek bölgesini yeniden kullanıma açmış olabileceği anlamına gelir — yazma, artık ayırıcının kendi defterine ait olan baytların üzerine gidebilir ve ayırıcının iç tutarlılığını bozabilir. Bu da tanımsızdır ve bir hata iletisi vermeyebilir; program bir süre daha “normal” çalışabilir.

Tanımlı karşılığı, blokla ilişkilendirilmiş bir geçerlilik bayrağı taşımaktır: blok bırakıldığında bayrak düşürülür ve her yazma bu bayrağa bakar.

// gecerlilik.c — birakma sonrasi yazmayi bayrakla engelleyen kalip
#include <stdlib.h>

const char *kullanim_karsiligi(void) {
    int *ikinci = malloc(sizeof *ikinci);
    int gecerli = (ikinci != NULL);
    if (gecerli) {
        *ikinci = 4;
    }
    free(ikinci);
    ikinci = NULL;
    gecerli = 0;
    if (gecerli) {
        *ikinci = 7;
    }
    return gecerli ? "yazildi" : "yazilmadi, bayrak dustu";
}

Bu karşılığın ölçümü de aynı son kod bloğunda gösteriliyor. Bayrağın yanında işaretçinin kendisi de free’den hemen sonra NULL’a çekiliyor. Bu satır süs değildir: o olmadan, uyarıları açık bir derleme ikinci’nin free sonrasında hâlâ metinde geçtiğini görüp uyarı basar — dal hiç çalışmasa bile, çünkü derleyici dalın çalışmayacağını her zaman kanıtlayamaz. Derleyicinin kendisi bile bırakılmış bir işaretçiyi gözünde tutmaya devam eder; bayrak dalı kapatır, NULL’a çekme adın bırakılmış bir adres taşımasını önler ve ikisi birlikte bloğu uyarısız derletir.

Bayrağın yeri de önemlidir: bayrak, koruduğu işaretçiyle aynı ömre sahip olmalıdır. Bayrak yerel bir değişkense ve işaretçi bir işlevden diğerine geçiyorsa, bayrak işaretçiyle birlikte taşınmadığı sürece çağrılan taraf işaretçinin hâlâ geçerli olup olmadığını bilemez — bu da geçerlilik denetiminin kendisinin, denetlediği bloktan ayrı bir sahiplik taşıdığı anlamına gelir; bayrağı kim güncelleyecek, kimin sorumluluğundadır.

Çift Bırakma

Aynı bloğu iki kez free’ye vermek de tanımsızdır. Ayırıcı, bir bloğu bıraktıktan sonra o adresi kendi defterine (serbest liste) ekler; ikinci free çağrısı aynı adresi bu deftere ikinci kez eklemeye çalışır ve deftere ait iç yapıyı bozabilir. Sonuç, ayırıcının bir sonraki malloc çağrısında görünür ya da hiç görünmez — ikisi de standardın söylediği bir şey değil.

Tanımlı karşılığı, bir bloğun bırakıldığını da bir bayrakla işaretlemek ve ikinci bırakma isteğini bu bayrakla atlamaktır.

// tekbirak.c — cift birakmayi bayrakla engelleyen kalip
#include <stdlib.h>

const char *tek_birakma(void) {
    int *ucuncu = malloc(sizeof *ucuncu);
    int birakildi = 0;

    if (!birakildi) {
        free(ucuncu);
        birakildi = 1;
    }
    if (!birakildi) {
        free(ucuncu);
        birakildi = 1;
    }
    return birakildi ? "bir kez birakildi" : "birakilmadi";
}

İkinci birakildi denetimi burada hiç çalışmayacak — kod satırı olarak var, ama birakildi bayrağı o noktada zaten doğru olduğu için koşulu hiç sağlamayacak. Bu, dersin başında kurulan kuralın somut hâlidir: tanımsız yolu gösteren satır kaynakta durabilir, yeter ki hiç yürütülmesin. Ölçümü yine son kod bloğunda.

Çift bırakma, sahiplik açısından bakıldığında ilginç bir durumdur: blok bırakıldığı anda sahipliği zaten hiç kimseye devretmiştir — ne dil, ne çağıran, ne çağrılan onu artık sahiplenmez. İkinci free çağrısı bu boşluğa yapılan bir çağrıdır; ayırıcı o adresi tanımaz ya da yanlış tanır, çünkü elinde o adresle ilgili artık geçerli bir kayıt yoktur. Tek-bırakma bayrağı bu boşluğu doldurmuyor, yalnız oraya bir daha girilmesini engelliyor.

Sınır Dışı Erişim

Bir dizinin [0, uzunluk) aralığının dışına erişmek — dizi[uzunluk] ya da daha ötesi — tanımsızdır. Bellek fiziksel olarak orada bir şey içerebilir, ama o bayt dizinin bir parçası değildir; derleyici, bir dizi erişiminin her zaman sınırlar içinde kaldığını varsayarak sınır denetimlerini silebilir. Sınır dışı bir okuma rastgele bir değer, sınır dışı bir yazma başka bir değişkenin verisini bozabilir.

Tanımlı karşılığı, erişimden önce istenen indeksi bilinen uzunlukla karşılaştırmaktır.

// sinirli.c — indeksi uzunlukla karsilastirip erisimi tanimli tutan kalip
const char *sinirli_erisim(int istenen) {
    int dizi[4] = {10, 20, 30, 40};
    int uzunluk = 4;
    if (istenen >= 0 && istenen < uzunluk) {
        (void)dizi[istenen];
        return "erisildi";
    }
    return "reddedildi, sinir disi";
}

istenen < uzunluk denetimi C’nin kendisinde yoktur — dizi erişimi (dizi[i]) dilde her zaman sessizce izin verir, ne çalışma zamanında ne derleme zamanında bir denetim yapılır. Denetim tamamen programcının eklediği bir satırdır; eklenmezse dizinin ötesine geçen bir indeks derlenir ve tanımsız davranışa girer.

Bu, öbür üç kaynaktan farklı bir noktayı gösteriyor: sarkan işaretçi ve bırakılmış bellek bir zamanlama sorunudur — blok bir noktada geçerliyken sonra geçersiz olur. Sınır dışı erişim ise zamanlamadan bağımsızdır; dizi hiç bırakılmamış, hâlâ tamamen geçerli olsa bile yanlış indeks tanımsız davranışa girer. Ortak nokta, ikisinde de derleyicinin hiçbir çalışma zamanı denetimi eklememesidir — ne zamanlamayı, ne indeksi C’nin kendisi izler.

Dört Karşılığın Birlikte Koşturulması

Dördü de tanımsız değil, tanımsızlığı önleyen kalıplardır; hepsi tek bir main’de, sırayla koşturuluyor.

// olcum.c — dordunun de tanimli karsiligi tek main'de olculuyor
#include <stdio.h>

const char *sarkan_karsiligi(void);
const char *kullanim_karsiligi(void);
const char *tek_birakma(void);
const char *sinirli_erisim(int istenen);

static void gozle(const char *ad, const char *sonuc) {
    printf("%-28s%s\n", ad, sonuc);
}

int main(void) {
    printf("%-28s%s\n", "gozlem", "sonuc");
    gozle("sarkan isaretci", sarkan_karsiligi());
    gozle("birakilmis bellek", kullanim_karsiligi());
    gozle("cift birakma denemesi", tek_birakma());
    gozle("gecerli indeks", sinirli_erisim(2));
    gozle("sinir disi indeks", sinirli_erisim(5));
    return 0;
}
gozlem                      sonuc
sarkan isaretci             isaretci bos, dolaylama yapilmadi
birakilmis bellek           yazilmadi, bayrak dustu
cift birakma denemesi       bir kez birakildi
gecerli indeks              erisildi
sinir disi indeks           reddedildi, sinir disi

SL1. blok == NULL denetimi burada sarkan işaretçiyi önlüyor, sarkan işaretçinin kendisini ölçmüyor. Tanımsız davranışın ölçülebilir bir “sonucu” yoktur; ölçülebilen tek şey, ona hiç girilmediğinin kanıtıdır — bu yüzden çıktı satırı “dolaylama yapılmadı” diyor, “değer şu çıktı” demiyor.

SL2. Bayrağın kendisi sıradan bir int‘tir; C’nin ayrı bir “geçerlilik” tipi yoktur. Güvence, tipten değil, her yazma noktasının bayrağı denetlemesi disiplininden gelir — derleyici bu disiplini zorlamaz, denetlemeyi unutan bir satır sessizce derlenir.

SL3. tek_birakma içindeki ikinci birakildi denetimi koşumda hiç çalışmadı — çıktı tek bir “bir kez birakildi” satırı verdi, ikinci bir bırakmanın izi yok.

SL4. Son iki satır aynı işlevin aynı diziye iki farklı istenen değeriyle çağrılışıdır: geçerli indeks dizinin içinde kalıp erişiliyor, sınır dışı indeks erişilmeden reddediliyor — ikisi de tanımlı, ikisi de dizinin ötesine hiç dokunmuyor.

Dört satırın da ortak noktası, hiçbirinin bir dilin ya da bir çalışma zamanının garantisine dayanmamasıdır. sarkan_karsiligi işaretçiyi kendi elleriyle NULL’a çeker, kullanim_karsiligi bayrağı kendi elleriyle düşürür, tek_birakma ikinci bırakmayı kendi elleriyle atlar, sinirli_erisim sınırı kendi elleriyle karşılaştırır — dördü de aynı bloğu sahiplenen bir taraf olmadığı için, kalan tek güvence kaynakta yazılı denetimdir. Bu, önceki dersin dört sınıfından (dil, çağıran, çağrılan, sahipsiz) hiçbirinin burada devrede olmadığı anlamına gelmez; tam tersine, sahipsiz sınıfının ne anlama geldiğini tam da bu dört örnek somutlaştırıyor: blok bir noktada geçerliliğini kaybettiğinde, dil de çağıran da çağrılan da onu artık kendiliğinden korumaz. Koruma varsa, onu koyan kod satırıdır — derleyici değil, çalışma zamanı kütüphanesi değil, tip sistemi hiç değil. Bu yüzden sahipsiz sınıfının bu dersteki karşılığı, bir önceki dersteki dil/çağıran/çağrılan sınıflarının yerini almaz; onların hepsinin ortak bir zayıf noktasını, sözleşme kırıldığı andaki durumu, ayrıca ele alır.

Sınırlayıcı Ölçüm

Tanımsız davranış bir hata mesajı değildir. Yukarıdaki dört karşılık her koşumda birebir aynı çıktıyı veriyor — ama bu tutarlılık, karşılıklarının tanımlı olmasından geliyor, ilgili tanımsız işlemlerin “aslında zararsız olduğunu” göstermiyor. Bu dersin hiçbir kod bloğu bir sarkan işaretçiyi dolaylamıyor, bırakılmış bir bloğa yazmıyor, bir bloğu iki kez bırakmıyor ya da bir dizinin ötesine erişmiyor; hepsi bunları önleyen kalıbı koşturuyor. Bir programın “çalışıyor görünmesi”, içinde tanımsız davranış olmadığının kanıtı değildir — derleyici sürümü, optimizasyon düzeyi ya da bir sonraki ayırma isteği değiştiğinde aynı kaynak farklı davranabilir.

Bu, dersin ölçüm sınırını da belirliyor: burada koşturulan hiçbir program, tanımsız davranışın “ne yaptığını” göstermiyor, çünkü öyle bir şey göstermek zaten kavramın tanımıyla çelişir. Ders yalnız iki şeyi ölçebiliyor — sözleşmenin nerede kırıldığını düzyazıyla anlatmayı ve kırılmanın yerine geçen tanımlı yolu koşturmayı. Aradaki fark, bir laboratuvar deneyiyle bir tarif arasındaki farka benzer: deney sonucu gözlemlenebilir, tarif yalnız izlenirse sonucu garanti eder — izlenmezse ne olacağı tarifin konusu değildir.

Özet

  • Tanımsız davranış, derleyicinin hiç girilmediğini varsayarak optimizasyon yaptığı bir boşluktur; “çalışıyor görünmek” onun tanımlı olduğunu göstermez.
  • Sarkan işaretçiyi okumak, bırakılmış belleği kullanmak, bir bloğu iki kez bırakmak ve dizinin sınırı dışına erişmek — dördü de tanımsızdır ve bu derste hiçbiri koşturulmadı.
  • Her kaynağın tanımlı bir karşılığı vardır: geçerlilik bayrağı, tek-bırakma bayrağı ve indeks-uzunluk denetimi — hepsi programcının eklediği, dilin zorlamadığı satırlardır.
  • Bu dersin ölçtüğü bellek sahipsiz sınıfındadır: sözleşme kırıldığında blok bırakılmakla yükümlü hiçbir tarafça artık tanımlı yoldan erişilemez.

Sonraki Adım

Sözleşmenin kırılması yalnız bellek erişiminde değil, bir işlevin hata döndürdüğü yolda da görülür — orada da bir şeyin temizlenmesi gerekir, ama bu kez tanımsız davranışa girmeden. Sonraki ders bu temizliği çağrılan sınıfının işlevi üzerinden ölçüyor: dönüş kodu, hata numarası ve tek çıkış noktalı temizlik kalıpları.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat