İçeriğe geç
academia.sh

Ders 14 / 15

Sınır Denetimi

kayit_olustur'un sinir parametresi uzunluğu kaynaktan değil, çağırandan alır; ders bu parametrenin bir arabellek taşmasını nasıl önlediğini, snprintf'in kesme bildirimini ve uzunluğun kimin sorumluluğunda olduğunu koşturarak gösterir.

Yayın Güncelleme

Bu derste

  • kayit_olustur'un sinir parametresini çağıranın verdiği bir sözleşme olarak tanır
  • Uzunluğu bilinmeyen bir kaynağı sabit boyutlu bir arabelleğe snprintf ile kesme bildirimiyle kopyalar
  • Sınırın kaynakta değil, çağıranda yazılı olduğunu iki farklı sınır değeriyle ölçer
İçindekiler

Önceki ders kayit_olustur’un hata yolunda temizliğini ölçtü; o hata yolu bir tek koşuldan doğuyordu: verilen adın uzunluğu sinir parametresini aşıyordu. Bu ders o parametrenin kendisini merkeze alıyor. sinir, dilin ya da sahip.c biriminin bir sabiti değildir — çağıranın her çağrıda verdiği bir değerdir ve bir arabellek taşmasını önleyen şey tam olarak budur.

Önceki iki ders “sahipsiz” ve “çağrılan” sınıflarını ölçtü; bu ders üçüncü sınıfa geçiyor: çağıran. Aradaki fark şu soruda somutlaşıyor — bir arabellek taşmasını kim önler? Önceki derste yanıt “çağrılan işlevin kendisi”ydi: kayit_olustur kendi ayırdığı bloğu kendi sınırlıyordu. Bu derste ölçülen iki işlev de (kayit_olustur’un sinir’i ve sinirli_kopyala’nın hedef_boyut’u) sınırı kendileri belirlemiyor — sınırı çağıran belirliyor ve işlev yalnız o sınıra uyuyor. Sınırın doğru olup olmadığı, işlevin değil, çağıranın sorumluluğudur.

Uzunluğu Çağıran Verir

kayit_olustur(const char *ad, size_t sinir) imzasına bakıldığında sinir’in nereden geldiği görünmez; imza yalnız bir size_t alır. Ama sözleşme başlık dosyasındaki yorumda yazılıdır: işlev, ad’in uzunluğunu sinir’e göre denetler ve sığmazsa NULL döner. Bu denetim dilin bir parçası değildir — strlen sınır tanımaz, memcpy sınır tanımaz; sınırı tanıyan tek şey kayit_olustur’un kendi gövdesindeki karşılaştırmadır.

Bu noktada dikkat edilmesi gereken şey, sinir’in nereden geldiğidir. İmzada size_t sinir yazması, değerin sabit ya da güvenli olduğu anlamına gelmez — çağıran onu bir sabitten, bir kullanıcı girdisinden ya da başka bir hesaplamadan türetebilir. kayit_olustur kendisine verilen sinir’in doğru olup olmadığını denetlemez, yalnız ad’in o sinir’e sığıp sığmadığını denetler. Sınırın kendisi yanlışsa — örneğin gerçek hedef kapasitesinden büyükse — işlev hâlâ doğru çalışır ama denetimin koruduğu şey artık gerçek hedef değil, çağıranın yanlış bildirdiği sayıdır. Bu ayrım dersin sonundaki “Sınırlayıcı Ölçüm” bölümünde yeniden ele alınıyor.

// sayac.h — her ayirmayi ve her birakmayi sayan sarmalayici
#ifndef SAYAC_H
#define SAYAC_H

#include <stddef.h>

void *say_ayir(size_t boyut);
void *say_buyut(void *eski, size_t boyut);
void say_birak(void *blok);
int ayrilan_sayisi(void);
int birakilan_sayisi(void);

#endif
// sayac.c — sayaclar yalniz bu birimde tutulur
#include <stdlib.h>
#include "sayac.h"

static int ayrilan = 0;
static int birakilan = 0;

void *say_ayir(size_t boyut) {
    void *blok = malloc(boyut);
    if (blok != NULL) { ayrilan++; }
    return blok;
}

void *say_buyut(void *eski, size_t boyut) {
    int yeni_blok = (eski == NULL);
    void *yeni = realloc(eski, boyut);
    if (yeni != NULL && yeni_blok) { ayrilan++; }
    return yeni;
}

void say_birak(void *blok) {
    if (blok != NULL) { birakilan++; }
    free(blok);
}

int ayrilan_sayisi(void) { return ayrilan; }
int birakilan_sayisi(void) { return birakilan; }
// sahip.h — kaydin ici gizli, sahiplik sozlesmesi yorumlarda yazili
#ifndef SAHIP_H
#define SAHIP_H

#include <stddef.h>

typedef struct Kayit Kayit;

/* Cagiran sahiplenir ve kayit_yok_et ile birakir. Ad sinira sigmazsa NULL
   doner; o ana kadar ayrilan bellegi islev kendisi birakir. */
Kayit *kayit_olustur(const char *ad, size_t sinir);

/* Kaydi ve kaydin icindeki ad tamponunu birlikte birakir. */
void kayit_yok_et(Kayit *k);

/* Kayit sahiplenir: donen isaretci kayit yok edilene dek gecerlidir,
   cagiran onu birakmaz. */
const char *kayit_ad(const Kayit *k);

/* Cagiran sahiplenir: donen kopya say_birak ile birakilir. */
char *kayit_ad_kopyasi(const Kayit *k);

#endif
// sahip.c — sozlesmenin uygulandigi birim
#include <string.h>
#include "sahip.h"
#include "sayac.h"

struct Kayit {
    char *ad;
    size_t uzunluk;
};

Kayit *kayit_olustur(const char *ad, size_t sinir) {
    Kayit *k = say_ayir(sizeof *k);
    if (k == NULL) { return NULL; }

    k->uzunluk = strlen(ad);
    if (k->uzunluk >= sinir) {
        say_birak(k);
        return NULL;
    }

    k->ad = say_ayir(k->uzunluk + 1);
    if (k->ad == NULL) {
        say_birak(k);
        return NULL;
    }
    memcpy(k->ad, ad, k->uzunluk + 1);
    return k;
}

void kayit_yok_et(Kayit *k) {
    if (k == NULL) { return; }
    say_birak(k->ad);
    say_birak(k);
}

const char *kayit_ad(const Kayit *k) { return k->ad; }

char *kayit_ad_kopyasi(const Kayit *k) {
    char *kopya = say_ayir(k->uzunluk + 1);
    if (kopya != NULL) { memcpy(kopya, k->ad, k->uzunluk + 1); }
    return kopya;
}

SL7. kayit_olustur aynı ad ile iki farklı sinir değeriyle çağrıldığında iki farklı sonuç verebilir — sonucu belirleyen ad değil, çağıranın verdiği sinir’dir. Aşağıdaki ölçüm bunu tam olarak bu şekilde gösteriyor: aynı ad, biri dar biri geniş iki sınırla.

Bu gözlem, önceki dersteki kayit_olustur ölçümünden kasıtlı olarak farklı bir açıdan bakıyor. Önceki ders sinir’i sabit tuttu ve ad’in uzunluğunu değiştirerek hata yolunu tetikledi; bu ders tam tersini yapıyor — ad’i sabit tutuyor ve sinir’i değiştiriyor. İkisi de aynı karşılaştırmayı (k->uzunluk >= sinir) tetikliyor, ama hangi tarafın değiştiğini göstermek, sözleşmenin hangi parametreye ait olduğunu netleştiriyor: karşılaştırma her iki değişkene de bakıyor, ama bu derste sorulan soru özellikle “sınırı kim veriyor” olduğu için, sabit tutulması gereken taraf ad, değiştirilmesi gereken taraf sinir’dir.

Sabit Boyutlu Arabelleğe Kesmeli Kopyalama

kayit_olustur kaynağı ayırıp kopyalıyor; ama bazen hedef sabit boyutlu bir yerel arabellektir ve kaynağın uzunluğu önceden bilinmez. Bu durumda strcpy kullanmak arabellek taşmasına açık kapı bırakır: strcpy hedefin boyutunu hiç bilmez, kaynağı olduğu gibi kopyalar. snprintf ise hedefin boyutunu bir parametre olarak alır ve asla o boyutu aşmaz; kestiğinde de bunu dönüş değeriyle bildirir — dönüş değeri, kesme olmasaydı yazılacak olan toplam uzunluktur.

Bu iki kalıp arasındaki fark, önceki bölümdeki kayit_olustur’la karşılaştırıldığında daha net görülüyor. kayit_olustur sınırı aştığında hiçbir şey yazmadan NULL döner — kısmi bir yazma söz konusu değildir. sinirli_kopyala ise farklı davranır: hedefe sığdığı kadarını yazar ve gerçek uzunluğu ayrıca bildirir. İkisi de arabellek taşmasını önlüyor, ama biri “ya tam sığar ya da hiç yazmam” derken öbürü “sığdığım kadarını yazarım, ama sana kaç karakterin sığmadığını da söylerim” diyor. Hangi kalıbın uygun olduğu, hedefin ne için kullanılacağına bağlıdır: bir kayıt adı gibi bütünlüğü bozulmaması gereken bir veri için ilki, bir günlük satırı gibi kesilmesi kabul edilebilir bir metin için ikincisi tercih edilir.

// kesme.h — sinirli hedefe kesmeli kopyalamanin bildirimi
#ifndef KESME_H
#define KESME_H

#include <stddef.h>

/* Hedefin boyutu hedef_boyut'tur. Donen deger, kesme olmasaydi yazilacak
   toplam uzunluktur; bu deger hedef_boyut'tan buyukse kesme olmustur. */
size_t sinirli_kopyala(char *hedef, size_t hedef_boyut, const char *kaynak);

#endif
// kesme.c — snprintf ile sinir asilmadan kopyalama
#include <stdio.h>
#include "kesme.h"

size_t sinirli_kopyala(char *hedef, size_t hedef_boyut, const char *kaynak) {
    int yazilacak = snprintf(hedef, hedef_boyut, "%s", kaynak);
    if (yazilacak < 0) {
        return 0;
    }
    return (size_t)yazilacak;
}

SL8. sinirli_kopyala’nın döndürdüğü değer ile hedef_boyut - 1 karşılaştırıldığında kesilip kesilmediği anlaşılır: dönüş değeri hedefe sığmayan uzunluktur, hedefe fiilen yazılan uzunluk değildir. Bu iki sayı aynı olabilir (kesme yoksa) ya da farklı olabilir (kesme varsa) — ders aşağıdaki ölçümde ikisini de gösteriyor. Karşılaştırmanın hedef_boyut - 1 ile yapılması, snprintf’in hedefe her zaman bir sonlandırıcı sıfır bayt eklediğinin sonucudur: hedef_boyut baytlık bir alanda en fazla hedef_boyut - 1 karakter ve bir sonlandırıcı sığar.

İki Sınırla Tek Ölçüm

Aşağıdaki tek main, aynı iki kaynağı iki farklı sınırla ölçüyor: kayit_olustur için dar ve geniş bir sinir, sinirli_kopyala için küçük ve yeterli bir hedef_boyut.

// olcum.c — cagiranin verdigi sinirin iki farkli degerle olculmesi
#include <stdio.h>
#include "sahip.h"
#include "sayac.h"
#include "kesme.h"

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

int main(void) {
    char tampon[64];

    printf("%-28s%s\n", "gozlem", "sonuc");

    /* --- kayit_olustur: ayni ad, iki farkli sinir --- */
    Kayit *dar = kayit_olustur("saglamlik", 6);
    snprintf(tampon, sizeof tampon, "kayit %s", dar == NULL ? "reddedildi" : "kabul edildi");
    gozle("dar sinir (6)", tampon);

    Kayit *genis = kayit_olustur("saglamlik", 32);
    snprintf(tampon, sizeof tampon, "kayit %s", genis == NULL ? "reddedildi" : "kabul edildi");
    gozle("genis sinir (32)", tampon);
    kayit_yok_et(genis);

    /* --- sinirli_kopyala: ayni kaynak, iki farkli hedef boyutu --- */
    char kucuk[8];
    size_t gerekli_kucuk = sinirli_kopyala(kucuk, sizeof kucuk, "saglamlik dersi");
    snprintf(tampon, sizeof tampon, "gerekli %zu, sigan %zu, %s",
             gerekli_kucuk, sizeof kucuk - 1,
             gerekli_kucuk >= sizeof kucuk ? "kesildi" : "kesilmedi");
    gozle("kucuk hedef (8)", tampon);

    char yeterli[32];
    size_t gerekli_yeterli = sinirli_kopyala(yeterli, sizeof yeterli, "saglamlik dersi");
    snprintf(tampon, sizeof tampon, "gerekli %zu, sigan %zu, %s",
             gerekli_yeterli, sizeof yeterli - 1,
             gerekli_yeterli >= sizeof yeterli ? "kesildi" : "kesilmedi");
    gozle("yeterli hedef (32)", tampon);

    return 0;
}
gozlem                      sonuc
dar sinir (6)               kayit reddedildi
genis sinir (32)            kayit kabul edildi
kucuk hedef (8)             gerekli 15, sigan 7, kesildi
yeterli hedef (32)          gerekli 15, sigan 31, kesilmedi

SL9. "saglamlik" dokuz karakterdir; dar sınır (6) onu reddediyor, geniş sınır (32) aynı adı kabul ediyor — aradaki fark kaynakta değil, çağıranın verdiği sayıdadır. sinirli_kopyala’da da aynı kaynak ("saglamlik dersi", on beş karakter) iki farklı hedef boyutuyla ölçülüyor: sekiz baytlık hedef yedi karaktere sığdığı için kesiliyor, otuz iki baytlık hedef on beş karaktere rahatça sığdığı için kesilmiyor — gerekli değeri her iki çağrıda da aynı kalıyor, değişen yalnız hedefin kapasitesi.

Dört gözlemin ikisi kayit_olustur üzerinden, ikisi sinirli_kopyala üzerinden geliyor, ama dördü de aynı yapıyı paylaşıyor: aynı kaynak, iki farklı sınır. Bu tekrar kasıtlı — dersin tek bir kanıtı yok, iki bağımsız işlevde aynı ilkeyi gösteren iki kanıtı var. kayit_olustur sınırı aşınca tamamen reddediyor; sinirli_kopyala sınırı aşınca kısmen kabul edip durumu bildiriyor. İki farklı tepki, aynı kaynağın (çağıranın verdiği sınır) iki farklı sonucu nasıl belirleyebileceğini gösteriyor. main içindeki once/fark düzenine burada ihtiyaç yok, çünkü hiçbir gözlem bir öncekinin sayaç durumuna bağlı değil — her ikisi de kendi girdisiyle, kendi sınırıyla bağımsız ölçülüyor; dar ve genis işaretçilerinden yalnız genis bırakılıyor çünkü dar zaten NULL, kayit_yok_et(NULL) de güvenle hiçbir şey yapmıyor.

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

sinir ve hedef_boyut parametrelerinin doğru değerle çağrılması derleyicinin denetlediği bir şey değildir. kayit_olustur(ad, 1000000) gibi gerçek boyuttan kopuk, aşırı büyük bir sınır vermek de derlenir; bu durumda işlev kendi içindeki denetimi hâlâ doğru uygular, ama denetim artık anlamsız hale gelir çünkü çağıran gerçek hedefin boyutunu yanlış bildirmiştir. Sınır denetimi, sınırın kendisi yanlışsa hiçbir şeyi kurtarmaz.

Aynı sınır, sinirli_kopyala’nın hedef_boyut parametresi için de geçerlidir. sizeof kucuk ifadesi burada doğru çalışıyor çünkü kucuk gerçek bir dizi değişkenidir ve derleyici onun boyutunu bilir; ama bir işaretçi parametresine sizeof uygulansaydı — örneğin kucuk bir işlev parametresi olarak char *kucuk şeklinde alınsaydı — sizeof kucuk dizinin boyutunu değil, işaretçinin kendi boyutunu verirdi ve bu da sessizce yanlış bir hedef_boyut üretirdi. Bu derste sizeof kucuk ve sizeof yeterli doğrudan main içindeki dizi tanımlarının üzerinde çağrıldığı için bu tuzağa girilmiyor, ama kalıbı başka bir işleve taşırken bu ayrım gözden kaçırılabilir; hedef_boyut’u her zaman dizinin tanımlandığı kapsamda hesaplamak, işaretçiye dönüştükten sonra hesaplamamak gerekir.

Sınırın doğruluğu bir kez saptandıktan sonra bile, sınırın kalıcılığı ayrı bir sorudur. kayit_olustur’a verilen sinir yalnız o tek çağrı için geçerlidir; aynı Kayit daha sonra başka bir işleve geçirilirken orijinal sinir değeri taşınmaz — yalnız kayit_ad’in döndürdüğü işaretçi ve kayit_ad_kopyasi’nin ürettiği kopya taşınır. Bu, çağıranın sorumluluğunun tek bir çağrıyla bitmediğini gösteriyor: sınırı ilk veren çağıran, o sınırın hangi varsayımla (örneğin “bu ad en fazla otuz bir karakter olacak”) verildiğini de belgelemek zorundadır, çünkü sonraki kod o varsayımı kaynaktan okuyarak değil, yalnız belgeden okuyarak öğrenebilir.

Özet

  • kayit_olustur’un sinir parametresi kaynaktan değil çağırandan gelir; aynı ad, iki farklı sınırla iki farklı sonuç verdi.
  • snprintf, hedefin boyutunu parametre olarak alıp asla aşmayan ve kesildiğinde bunu dönüş değeriyle bildiren bir kopyalama sağladı; strcpy bu bildirimi hiç yapmaz.
  • İki ayrı hedef boyutuyla aynı kaynak ölçüldüğünde gerekli uzunluk sabit kaldı, yalnız kesilip kesilmediği değişti — sınırın doğruluğu hedefin gerçek kapasitesine bağlıdır.
  • Bu dersin ölçtüğü bellek çağıran sınıfındadır: hedefin boyutunu bilen ve bildiren taraf koddaki sinir ya da hedef_boyut parametresini veren çağrının kendisidir.
  • Bu üç ders, kursun “kim sahipleniyor” ekseninde üç ayrı sınıfı sırayla ölçtü: önceki iki ders sahipsiz ve çağrılan sınıflarını sözleşmenin kırıldığı ve işlevin başarısız olduğu anlarla, bu ders çağıran sınıfını sınırın kaynakta değil çağıranda yazılı olduğunu göstererek ölçtü. Üç dersin ortak paydası, hiçbirinde koruyucu tarafın dil olmamasıdır — koruma her seferinde ayrı bir tarafın kendi eklediği bir satırdan geliyor.

Sonraki Adım

Bu üç ders sözleşmenin kırıldığı, işlevin başarısız olduğu ve çağıranın sınır verdiği anları ölçtü — üçü de aynı platformda, aynı derleyicide koşturuldu. Bu ortak varsayım şimdiye kadar hiç sorgulanmadı: her ölçüm, aynı cc, aynı bayraklar, aynı işletim sistemi altında aynı sonucu üretti. Ama tanımlı davranışın kendisi bile her yerde birebir aynı değildir — bir kısmı standarda göre tanımlıyken, hangi değeri alacağı uygulamaya bağlı kalabilir; bu derste ölçülen sinirli_kopyala’nın döndürdüğü uzunluk türü olan size_t’in kendi genişliği bile platformdan platforma değişebilir. Sonraki ders bu sözleşmenin platformlar arasında ne kadar değiştiğini ele alıyor: hangi davranış tanımlı kalır, hangisi uygulamaya bağlıdır ve kod bunu nasıl taşınabilir tutar. Bu dersteki üç örnek de bu yüzden yalnız bir platformun kanıtıdır — sonucun kendisi değişmeden kalsa bile, hangi varsayımların o sonucu taşıdığı henüz açık yazılmadı. O yazım işi, ders kodunun bir satırını bile değiştirmeden, yalnız derleme ortamını değiştirerek başlıyor.

İ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