C/C++ kodundaki tanımsız davranışları gösteren karmaşık bir sahne, yazılım geliştirmenin zorluklarını ve AI'ın bu alandaki potansiyelini vurguluyor.

C/C++’ta Tanımsız Davranış: Kodu Doğru Yazmak Neden İmkansız?

C/C++’ta Tanımsız Davranış: Kodu Doğru Yazmak Neden İmkansız?

Deneyimli C/C++ programcıları bile, Cardinal Richelieu’nun dediği gibi, yazdıkları altı satır kodda bile tanımsız davranış (Undefined Behavior – UB) bulabileceğimizi itiraf ediyor. Yaklaşık 30 yıldır C ve C++ ile iç içe olan bir geliştirici, bu dillerde hatasız kod yazmanın neredeyse imkansız olduğunu belirtiyor. 1972 (C) ve 1985 (C++) ortamları ile günümüz arasında büyük farklar olsa da, bu dillerdeki tanımsız davranışlar sanılandan çok daha yaygın ve incelikli.

Çift serbest bırakma, serbest bırakma sonrası kullanım, nesne sınırları dışına erişim ve başlatılmamış belleğe erişim gibi bilinen UB vakalarının yanı sıra, çok daha belirsiz ve mantık dışı durumlar da mevcuttur. Sektör olarak bu hataları tekrar tekrar yapmaktan kaçınamıyoruz.

Tanımsız Davranış Optimizasyonlarla İlgili Değil

Bazı programcılar, derleyicinin optimizasyonlar açık olmadığında tanımsız davranışın kendilerine zarar veremeyeceğini düşünüyor. Ancak bu hatalı bir inanıştır. Tanımsız davranış, derleyicinin kodunuzun geçerli olduğunu varsayması anlamına gelir. İnsan tarafından okunduğunda çok açık olan bir niyetin, derleyici aşamaları veya modüller arasında ifade edilememesi durumudur. Derleyici, bu tür durumların ‘asla gerçekleşmeyeceğini’ varsayarak özel durumları uygulamak zorunda kalmaz. Temelde, derleyici ve donanım, UB içeren niyetlerinizle bir ‘telefon oyunu’ oynar; istediğiniz sonucu verebilir ama garantisi yoktur.

Tanımsız Davranış Her Yerde

Tüm ciddi C/C++ kodlarında tanımsız davranışın bulunduğunu iddia etmek abartı değildir. İşte birkaç örnek:

1. Doğru Hizalanmamış Bir Nesneye Erişim

Örneğin, bir ‘int’ işaretçisi (pointer) ile hizalanmamış bir bellek adresine erişmek tanımsız davranıştır (C23 6.3.2.3). Bu durum, farklı mimarilerde farklı sonuçlar doğurabilir: Linux Alpha’da çekirdek tarafından emüle edilebilir, SPARC’ta SIGBUS hatasıyla program çökebilir veya x86/amd64’te sorunsuz çalışabilir. Derleyicinin hizalanmamış işaretçiler üzerinde çalışacak talimatlar üretme yükümlülüğü yoktur. Benzer şekilde, hizalanmamış bir ‘std::atomic‘ nesnesine atomik olarak erişmek de UB’dir ve pratikte atomisite sorunlarına yol açabilir.

2. İşaretçinin Oluşturulması Bile UB Olabilir

Yukarıdaki örnekte sorun sadece işaretçiyi referans almak değildi; işaretçiyi oluşturma eylemi bile UB olabilir. Örneğin, ‘uint8_t’ dizisinden ‘int*’ türüne yapılan bir dönüşüm (cast) tanımsız davranıştır. Derleyicinin, bir ‘int*’ işaretçisinin alt bitlerine çöp toplama veya güvenlik etiketleme bitleri gibi özel anlamlar ataması tamamen geçerlidir.

3. ‘isxdigit()’ Fonksiyonunun ‘char’ Girişiyle Kullanımı

Eğer ‘char’ türü mimarinizde ‘signed’ ise ve ‘isxdigit()’ fonksiyonuna 0-127 aralığı dışında bir değer geçirilirse, bu değer negatif olabilir. ‘isxdigit()’ ise bir ‘int’ değeri bekler ve ‘EOF’ (genellikle negatif) haricindeki negatif değerler için davranışı tanımlanmamıştır. Bu durum, bellek dışı okumalara ve hatta gömülü sistemlerde istenmeyen donanım tetiklemelerine yol açabilir.

4. ‘float’tan ‘int’e Dönüşüm

Bir ‘float’ değeri ‘int’e dönüştürülürken, eğer ‘float’ın tam sayı kısmı ‘int’ türü tarafından temsil edilemiyorsa veya ‘float’ sonlu bir değer değilse, bu tanımsız davranıştır (C23 6.3.1.4). ‘INT_MAX’ gibi değerleri ‘float’a dönüştürüp karşılaştırmak bile yuvarlama sorunları nedeniyle yanıltıcı olabilir. Güvenli bir dönüşüm yapmak, beklediğimizden çok daha karmaşık bir dizi kontrol gerektirir.

5. Sıfır Adresindeki Nesne

C standardı, ‘NULL’ işaretçisinin makine adres sıfıra işaret ettiğini garanti etmez, sadece sıfır ile karşılaştırıldığında eşit olacağını garanti eder. ‘NULL’ işaretçisini referans almak her zaman tanımsız davranıştır (C23 3.4.3). Ayrıca, ‘memset(&ptr, 0, sizeof(ptr));’ kullanarak bir işaretçiyi ‘NULL’ yapmak güvenli değildir, çünkü ‘NULL’ın bit deseni sıfır olmayabilir. Geçmişte bazı makineler sıfır olmayan ‘NULL’ işaretçileri kullanmıştır.

6. Değişken Argümanlar ve Tür Uyuşmazlıkları

‘execl’ gibi fonksiyonlarda ‘NULL’ yerine doğrudan ‘0’ kullanmak veya ‘printf’ ile ‘uint64_t’ için yanlış biçim belirleyici (‘%ld’ yerine ‘%lld’) kullanmak tanımsız davranıştır. Argüman türlerinin dikkatli bir şekilde eşleştirilmesi hayati önem taşır.

7. Sıfıra Bölme

Sıfıra bölme işlemi tanımsız davranıştır ve özellikle denklemin paydası güvenilmeyen bir girdiden geldiğinde ciddi güvenlik zafiyetlerine yol açabilir.

C23 standardı sadece ‘tanımsız’ kelimesini 283 kez kullanmakla kalmaz, aynı zamanda ihmal yoluyla tanımsız kalan birçok durum da vardır.

LLM’ler Bu Konuda Bizden Daha İyi

İnsan programcıların aksine, günümüzdeki Büyük Dil Modelleri (LLM’ler) herhangi bir C kodundaki tanımsız davranışları yüksek doğrulukla tespit edebiliyor. Hatta OpenBSD gibi pedantik olarak yazılmış projelerde bile UB bulabiliyorlar. Bu durum, LLM’lerin UB tespiti konusunda uzman insanlardan bile daha yetenekli olduğunu gösteriyor. Artık C/C++ kodu yazarken bir LLM’in denetimi olmadan çalışmak, 2026’da Sarbanes-Oxley (SOX) ihlali kadar sorumsuz görülebilir.

Mevcut C/C++ kod tabanlarını bir anda terk edemeyiz, ancak onları düzeltmeden bırakmak da bir seçenek değil. LLM’ler, büyük ölçekli kod tabanlarındaki UB’yi bulma ve düzeltme konusunda kritik bir rol oynayabilir, insan incelemecilerini aşırı yüklemeden bu ‘hademelik’ işini üstlenebilirler. Ancak LLM’lerin bulgularını doğrulamak için hala uzman insanlara ihtiyaç vardır.

Comments

No comments yet. Why don’t you start the discussion?

    Bir yanıt yazın

    E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir