C'de Her Şey Tanımsız Davranıştır
(blog.habets.se)- Tanımsız davranış (UB), derleyicinin kötü niyetli optimizasyonları değil; kodun geçerli olduğu varsayımıyla imkânsız yürütme yollarını ele almak zorunda olmamasını belirleyen bir kuraldır
- Sıradan olmayan C/C++ kodlarında yalnızca double-free veya sınır dışı erişim değil; hizalama, cast etme, başlatma ve tür uyuşmazlığı gibi daha ince UB türleri de yaygın biçimde gizlidir
- Hizalanmamış
int*veyastd::atomic<int>*erişimleri, platforma göre SIGBUS, çekirdeğin düzeltmesi ya da normal çalışıyormuş gibi görünen sonuçlar üretebilir; ancak standarda göre bunlar zaten UB'dir isxdigit()fonksiyonuna signedcharvermek,floatdeğeriniinte çevirmek veyaNULLile değişken argümanları yanlış kullanmak gibi yaygın kodlar da kolayca standardın dışına çıkar- Mevcut kod tabanlarını çöpe atmak mümkün değil; ancak bunu büyük ölçekte düzeltmek için LLM tabanlı UB tespiti ile uzman doğrulamasını birleştirmek gerekir ve konu junior geliştiricilere bırakılmayacak kadar incedir
C/C++'ta tanımsız davranış bir optimizasyon sorunu değildir
- Tanımsız davranış (UB), derleyicinin geliştirici hatalarını “istismar ettiği” anlamına gelmez; programın standart açısından geçerli olduğunu varsayabileceği anlamına gelir
- İnsan açısından niyet açık görünse bile, bu niyeti derleyici aşamalarında ya da modüller arasında ifade etmek zor olabilir
- Derleyici, “gerçekleşmesi mümkün olmayan” özel durumları kod üretiminde ele almak zorunda değildir ve donanımı da kapsayan yürütme yolunda niyetten farklı sonuçlar ortaya çıkabilir
- Optimizasyonu kapatmak UB'yi güvenli hâle getirmez; ayrıca aynı davranışın bugünkü veya gelecekteki derleyici ve mimarilerde korunacağına dair de garanti yoktur
UB yalnızca anormal kodlarda bulunmaz
- double-free, use-after-free, nesne sınırlarının dışına erişim ve başlatılmamış belleğe erişim iyi bilinen UB örnekleridir; ancak bunlar sektörde hâlâ tekrar tekrar görülür
- Daha ince ve sezgisel olmayan pek çok UB türü de vardır; bu yüzden sıradan görünen C/C++ kodu kolayca standardın dışına çıkabilir
- C23 standardında “undefined” sözcüğü 283 kez geçer; açıkça yazılmayıp yine de tanımsız kalan durumlar da düşünülürse kapsam daha da geniştir
- Sıradan olmayan C/C++ kodlarında UB her yere dağılmış durumdadır ve bunu yalnızca tek tek programcıların dikkatsizliğine bağlamak zordur
Hizalanmamış nesne erişimi
- Aşağıdaki gibi
int*işaretçisini dereference eden bir fonksiyon, işaretçi doğru hizalanmamışsa UB olurint foo(const int* p) { return *p; } - Hizalama (alignment), çoğu zaman
sizeof(int)katı adres anlamına gelebilir; ancak gerçek gereksinimler platforma ve uygulamaya göre değişebilir - Linux Alpha'da bazı durumlarda çekirdek trap'i yakalayıp amaçlanan erişimi yazılımla taklit edebilirdi; ancak başka durumlarda program
SIGBUSile sonlanabilirdi - SPARC'ta
SIGBUSoluşur; x86/amd64'te ise çoğu zaman sorun çıkmadan çalışabilir veya atomik okuma gibi görünebilir - ARM, RISC-V ve gelecekteki mimarilerde sonuçlar genellenemez; gelecekte bir mimari
int*için alt bitleri kullanmayan özel yazmaçlar da kullanabilir - Derleyici farklı bir load komutu kullanırsa, daha önce çekirdeğin düzelttiği erişimler artık düzeltilemeyebilir
- Derleyicinin hizalanmamış işaretçilerde de çalışacak assembly üretme zorunluluğu yoktur; çünkü o erişimin kendisi zaten UB'dir
Atomik türlerde de yanlış hizalama zaten UB'dir
- Aşağıdaki gibi
std::atomic<int>*üzerindestore()veyaload()çağrısı yapsanız bile, nesne doğru hizalanmamışsa davranış UB'dirvoid set_it(std::atomic<int>* p) { p->store(123); } int get_it(std::atomic<int>* p) { return p->load(); } - “Hizalanmamış nesnelerde bu işlem atomik mi?” sorusu, standart açısından anlamlı bir soru değildir
- Gerçek donanımda atomiklik konusu önemli olabilir; ancak standart açısından bundan önce zaten UB söz konusudur
- Atomik olarak okunduğu sanılan nesne bir sayfa sınırını aşıyorsa durum daha da karmaşıklaşır; ama sonuç “sorun yok” değil, yine UB'dir
Sadece işaretçi oluşturmak bile sorun olabilir
- Hizalanmamış bir işaretçiyi dereference etmeden önce bile belirli bir türe cast etmek başlı başına sorun olabilir
bool parse_packet(const uint8_t* bytes) { const int* magic_intp = (const int*)bytes; // UB! int magic_raw = foo(magic_intp); // Probably crashes on SPARC. int magic = ntohl(magic_raw); // this is fine, at least. […] } - Buradaki sorun
foo()çağrısı değil,(const int*)bytescast işlemidir - Standart açısından derleyicinin
int*işaretçisinin alt bitlerine çöp toplama veya güvenlik etiket biti gibi anlamlar yüklemesi de mümkündür
isxdigit() fonksiyonuna char vermek
- Aşağıdaki kod basit görünür; ancak
chartürünün signed olduğu mimarilerde giriş değeri 0–127 aralığının dışındaysa UB olabilirbool bar(char ch) { return isxdigit(ch); } isxdigit(), bir karakterin onaltılık karakter olup olmadığını kontrol eden fonksiyondur ve argüman olarakEOFda alabilir- C23 7.4p1'e göre
EOF,inttüründedir veunsigned charile temsil edilemeyen bir değer olduğu çıkarımı yapılabilir isxdigit()chardeğilintalır;chardaninte dönüşüm mümkün olsa da signedcharın negatif değerleri sorun yaratır- C23 6.2.5 paragraf 20'ye göre
charın signed olup olmadığı implementation-defined'dır - Aşağıdaki gibi yazılmış bir
isxdigit()uygulaması, negatif indeksle bilinmeyen bellek okuyabilirint isxdigit(int c) { if (c == EOF) { return false; } return some_array[c]; } - Bu bellek I/O eşlemeli bir bölgeyse, rastgele değer ya da çökme üretmenin ötesinde doğrudan donanım davranışını da tetikleyebilir
- Bu, masaüstü işletim sistemlerindeki uygulamalardan çok gömülü sistemlerde daha olasıdır; ancak kullanıcı alanı ağ sürücüleri gibi yalnızca kullanıcı alanında olmasına rağmen korumanın yetersiz kaldığı durumlar da vardır
floattan inte cast etme sorunu
- Aşağıdaki gibi saniye cinsinden
floatdeğeri milisaniye cinsindeninte dönüştüren kod yaygındır; ancak UB içerirint milliseconds(float seconds) { int tmp = (int)(seconds * 1000.0); /* WRONG */ return tmp + 1; /* WRONG separately (signed overflow is UB) */ } - C23 6.3.1.4, sonlu bir gerçek kayan nokta değerini tamsayı türüne dönüştürürken tamsayı kısmı hedef tamsayı türüyle temsil edilemiyorsa davranışın tanımsız olduğunu belirtir
- Sonlu olmayan değerler için de açık bir tanım yoktur; bu yüzden onlar da UB olur
floatdeğeriINT_MAXile karşılaştırmak da sanıldığı kadar basit değildirfloatıinte cast etmek, kaçınmaya çalıştığınız UB'yi doğurabilirINT_MAXdeğerinifloata cast ettiğinizde tam olarak temsil edilip edilmediği bilinmezINT_MAX,floata yuvarlandığındaintile temsil edilemeyen bir değere dönüşürse karşılaştırma temsil gücünü kaybedebilir
- Güvenli hâle getirmek için
isfinite()kontrolü,INT_MIN + 1000veINT_MAX - 1000gibi pay bırakılmış sınır karşılaştırmaları ve dönüşümden sonra toplama öncesi ek kontroller gerekirint milliseconds(float seconds) { const float ftmp = seconds * 1000.0f; if (!isfinite(ftmp)) { return 0; } if ((float)(INT_MIN + 1000) > ftmp) { return 0; } if ((float)(INT_MAX - 1000) < ftmp) { return 0; } const int tmp = (int)ftmp; if (INT_MAX == tmp) { return 0; } return tmp + 1; } - İnsan sadece
floatıinte çevirmek istiyor gibi görünse de, güvenli kod çok daha uzun olur
0 adresindeki nesne ve null pointer
- İşletim sistemi çekirdeklerinde veya gömülü kodda, 0 adresine nesne yerleştirme ihtiyacı doğabilir
- C standardına uygun biçimde gerçekten 0 adresinde nesne bulundurmanın pratik bir yolu olmadığı söylenebilir
- C 6.3.2.3'e göre işaretçiye dönüştürülebilen tamsayı sabiti 0 ve
nullptr, “null pointer constant”tır; burada bunaNULLdenebilir - C, gerçek
NULLpointer'ın makine adresi 0'ı gösterdiğini belirtmez - C standardı donanımı değil, C soyut makinesini ele alır;
NULLile 0 karşılaştırıldığında yalnızca eşit oldukları garanti edilir - Bu eşitlik, tamsayı 0'ın ilgili platformun yerel
NULLdeğerine dönüştürülmesinden kaynaklanıyor olabilir ve bu değer0xffffbile olabilir - null pointer'ı dereference etmek, değeri ne olursa olsun UB'dir ve C 3.4.3'teki tipik örneklerden biridir
- Bu nedenle
memset(&ptr, 0, sizeof(ptr));ifadesininNULLpointer ürettiği varsayılamaz - Bir struct'ı sıfırlayıp içindeki işaretçi üyelerin
NULLolduğunu varsaymak, çoğu programcı açısından da gerçek bir sorun kaynağıdır - Tarihsel olarak 0 olmayan NULL pointer kullanan makineler da vardı
0 adresinde fonksiyon olduğunu varsayma sorunu
- Modern makinelerde
NULL0 adresini gösterse ve gerçekten o adreste bir nesne veya fonksiyon bulunsa bile, C 6.3.2.3NULL'ın hiçbir nesneye veya fonksiyona eşit olmadığını söyler - Dolayısıyla aşağıdaki kod UB'dir
void (*func_ptr)() = NULL; func_ptr(); - C açısından bunun anlamı “orada fonksiyon yok” demektir ve derleyicinin içinde bu niyeti ifade etmenin bir yolu olmayabilir
- Sadece tüm bitleri 0 olan bir adrese
callkomutu üretileceğini varsayamazsınız - 16 bit x86'da “tümü 0” değerinin
0000:0000mı yoksaCS:0000mı olduğu bile net değildir
Değişken argümanlar ve tür uyuşmazlığı
execl()fonksiyonunun son argümanı bir işaretçi olmalıdır; bu yüzdenNULLmakrosunu veya tamsayı 0'ı doğrudan vermek UB olabilirexecl("/bin/sh", "sh", "-c", "date", NULL); /* WRONG */ execl("/bin/sh", "sh", "-c", "date", 0); /* WRONG */- Doğru biçim, açıkça işaretçi türüne cast etmektir
execl("/bin/sh", "sh", "-c", "date", (char*)NULL); NULLmakrosu tamsayı 0 olarak yorumlanabilir ve değişken argümanlarda gereken tür bilgisi aktarılmazprintf()içinde de format belirteci ile gerçek argüman türü uyuşmuyorsa UB oluşuruint64_t blah = 123; printf("%ld\n", blah); /* WRONG */uint64_tyazdırmak içinPRIu64kullanılmalıdıruint64_t blah = 123; printf("%"PRIu64"\n", blah);uid_tyazdırmak içinuintmax_ttürüne cast edipPRIuMAXkullanmak bir seçenek olabilir; ancakuid_ttürünün unsigned olduğundan bile emin olunamayabilir- En kötü durumda
-1yerine anlamsız bir değer yazdırılabilir
0'a bölme ve güvenlik sorunları
- 0'a bölmenin UB olduğu yaygın olarak bilinir; ancak payda güvenilmeyen girdiden geliyorsa bu bir güvenlik sorunu hâline gelir
- Buradaki kritik nokta, bunun yalnızca basit bir runtime hatası değil; girdi doğrulama sınırında ortaya çıkan bir UB olmasıdır
UB olmasa da integer promotion da risklidir
- Integer promotion kuralları kodu hızlıca gözden geçirirken uygulanması zor kurallardır ve sezgiye aykırı sonuçlar üretebilir
- Aşağıdaki kodda
overfloweddeğeri 1 değil 0 olurunsigned char a = 0xff; unsigned char b = 1; unsigned char zero = 0; bool overflowed = (a + b) == zero; // overflowed is set to zero, not one. - Aşağıdaki kodda tüm değişkenler unsigned gibi görünse de sonuç
2147483648 (0x80000000)değil,18446744071562067968 (ffffffff80000000)olurunsigned char a = 0x80; uint64_t b = a << 24; // Bonus UB(?) - UB olmasa bile C/C++'ın tamsayı kuralları sezgisel değildir ve hata üretmeye çok elverişlidir
LLM ile UB tespiti
- Güncel LLM'lere rastgele bir C kodunda UB bulmaları söylendiğinde, neredeyse her zaman bir sorun buluyorlar ve çoğunlukla doğru sonuç veriyorlar
- Kişisel kodda UB bulunduktan sonra, aynı yaklaşım olgun ve sıkı yazılmış OpenBSD koduna da uygulandı
- İlk akla gelen araç olan
findüzerinde çeşitli sorunlar bulundu - OpenBSD'ye sınır dışı yazma için bir patch ve UB olmayan mantık hatası için başka bir patch gönderildi
- Kalan birçok UB için patch gönderilmedi
- OpenBSD projesinin geçmişte hata raporlarına pek açık olmadığı yönünde bir deneyim vardı
- Bazı durumlarda pratikte sorun çıkmayabileceği düşünüldü
- OpenBSD'nin kod tabanından UB'yi temizlemesi için, LLM ile proje arasında tek tek patch taşımaktan daha büyük ölçekli bir girişim gerekir
C/C++ kod tabanları için gerçekçi yönelim
- Mevcut C/C++ kod tabanlarını atmak mümkün değil; ama onları özünde bozuk hâlde bırakmak da bir seçenek değil
- Yapay zekânın ürettiği düşük kaliteli değişiklikleri commit etmeden ve insan gözden geçirenleri ezmeden, UB büyük ölçekte düzeltilmeli
- 2026'da LLM tabanlı UB denetimi olmadan C veya C++ yazmak, SOX ihlali gibi görülebilir ve sorumsuzluk sayılabilir
- OpenBSD geliştiricileri 30 yılı aşkın sürede tüm bu sorunları bulamadıysa, diğer projelerin şansı daha da düşüktür
- Kişisel projelerde, LLM'den UB bulması, gerekirse açıklaması ve düzeltmesi istenebilir; ardından bir insan sonucu doğrulayabilir
- Ancak sonucu doğrulamak için uzman gerekir ve uzmanlar genelde başka işlerle meşguldür
- Bu iş bir temizlik işi gibi görünse de, geleneksel olarak böyle işlere verilen junior programcılara bırakılmayacak kadar incedir
1 yorum
Hacker News yorumları
C'de şaşırtıcı ve tuhaf pek çok tanımsız davranış var, ama bu yazı bunu pek iyi göstermiyor; sadece yüzeyini hafifçe kazıyor
Daha da tuhaf bir örnek olarak
volatile int x = 5; printf("%d in hex is 0x%x.\n", x, x);verilebilir.xnormal birintise sorun yok, amavolatileise bu tanımsız davranış oluyor. C standardındavolatileerişimi sadece okumak bile yan etkidir; aynı skaler nesne üzerindeki sırasız yan etkiler tanımsız davranıştır ve fonksiyon argümanlarının değerlendirilme sırası da birbirine göre belirli değildirGenelde veri yarışı, farklı thread'lerin aynı nesneye aynı anda erişmesi ve bunlardan en az birinin yazma yapması anlamına gelir, ama C'de tek bir thread içinde de yazma olmadan veri yarışına benzer bir durum oluşabilir
undefinedkelimesinin geçtiği 283 yeri ya da eksiklikler nedeniyle tanımsız olan tüm durumları tek tek saymak değildiAsıl mesele bunun kaçınılmaz olması. En azından 1972'de C ortaya çıktığından beri insanlar bunu tamamen önleyebilmiş değil
54 yıldır başarılamadıysa, çözüm “daha çok çabala” ya da “hata yapma” değildir. Mythos'un OpenBSD'de bulduğu istismar edilebilir bir açık OpenBSD geliştiricileri açısından oldukça iyi bir puan sayılırdı, ama en basit kodda araçları çalıştırınca bile her yerde tanımsız davranış çıktı
Örneğin
findiçindewaitpid(&status)sonrasında,waitpid()hatasını kontrol etmeden önce başlatılmamış otomatik değişkenstatus'un okunması da tanımsız davranıştır; gerçi bunun istismar edilebilir olacağı bir mimari ya da derleyici hayal etmek zorYazıda da belirttiğim gibi amaç dünyadaki tüm tanımsız davranışları listelemek değil, önemsiz olmayan tüm C/C++ kodlarında tanımsız davranış vardır noktasını vurgulamak
volatile, tip sistemi hilesidir. Daha ilkesel bir çözüm yapılmalıydı ve modern diller de “C böyle yaptıysa iyi bir fikirdir” diye bunu taklit etmemeliİlk C derleyicileri değerleri her zaman belleğe yazıyordu; bu yüzden işaretçiyi memory-mapped I/O donanımına uygun yere koyarsanız,
xher değiştiğinde CPU komutu gerçekten belleğe yazıyor ve sürücü kodu çalışıyorduAma optimizasyon gelince derleyici bunun sadece
x'i değiştirmek olduğunu düşünüp değeri register'da tutmaya başladı ve sürücü bozuldu. C'dekivolatile, derleyiciye “o optimizasyonu yapma” diyen bir hack'tir; oysa doğru çözüm olan kütüphane düzeyinde memory-mapped I/O içsel fonksiyonları sağlamak çok daha büyük bir iş olurduİçsel fonksiyonların gerekli olmasının nedeni, mümkün olan ve olmayan davranışları tam olarak ifade edebilmesidir. Bazı hedeflerde 1 bayt, 2 bayt, 4 bayt yazmaların her biri farklı davranış üretir ve donanım bunu ayırt eder. Bazı cihazlar 4 baytlık RGBA yazımı bekler; bunun yerine 1 baytlık dört yazma gönderirseniz cihaz şaşırabilir ya da çalışmayabilir. Bazı hedefler bit düzeyinde yazmayı da destekler. Yalnızca
volatileile ne olup bittiğini ve bunun ne anlama geldiğini bilmenin yolu yokturBir C programını derledikten sonra tersine çevrilip assembly olarak incelerseniz, ortada tanımsız davranışı olmayan bir assembly programı kalır. Çünkü assembly'de tanımsız davranış diye bir kavram yoktur
Tanımsız davranış kaynak programın özelliğidir, çalıştırılabilir dosyanın değil. Yani programın yazıldığı dilin tanımı o programa bir anlam vermez. Buna karşılık derleme sonucunda oluşan çalıştırılabilir dosyaya anlamı makine tanımı verir
Yarış ise programın çalışmasının bir özelliğidir. Bu yüzden bir C programında tanımsız davranış olduğunu söyleyebilirsiniz, ama çalıştırılabilir dosyada gerçekten yarış olduğunu söyleyemezsiniz. Elbette derleyici tanımsız davranış içeren bir programı istediği gibi derleyebilir ve yarış da ortaya çıkarabilir; ama yeni thread'ler oluşturmadan derlenirse yarış da olmaz
volatileın anlamı zaten değerin başka bir şey tarafından değiştirilebilir olmasıdır. Global bir değişkense bu başka şey yalnızca başka bir thread değil, interrupt ya da signal handler da olabilir. Belirli bir adresi okuyan bir işaretçiyse, değeri değişen bir donanım aygıtı register'ı da olabilirvolatiledeğişken kavramının kendisi sorun değil. Interrupt rutinlerini ve memory-mapped I/O'yu desteklemek isteyen bir dilin, aynı donanım register'ını iki kez okumanın aynı bellek konumunu iki kez okumakla aynı şey olmadığını derleyiciye anlatacak bir yola ihtiyacı vardırAsıl sorun, dil özellikleri ile kısıtlar arasındaki etkileşimin yeterince netleştirilmemiş olması. “Bu değer her an değişebilir” diye özellikle belirtip sonra tam da bu nedenle bazı kullanımları tanımsız davranış saymak saçma.
volatiledeğişkenler için “sırasız yan etkiler” tanımında bir istisna olmalıydıBirçok kişi C ve C++'ın “istediğin her şeyi yapmana izin verdiği için çok esnek” olduğunu sanıyor. Gerçekte güçlü ve havalı görünen neredeyse her teknik bir tanımsız davranış mayın tarlası
Hizalanmamış işaretçi tanımsız davranışı daha da kötü. Hizalanmamış işaretçi, erişim anında değil, işaretçinin kendisiyle bile tanımsız davranıştır
Bu yüzden
void* v'yiint* i'ye örtük dönüştürmek, örneğin C'dei=vya daint*alanf(v)çağrısı da ortaya çıkan işaretçiinthizalama koşulunu karşılamıyorsa tanımsız davranıştırBunun C düzeyinde bir sorun olması önemli. Bir C programında tanımsız davranış varsa, o C programı biçimsel olarak geçerli değildir ve hatalı bir programdır. Bu bir donanım sorunu değildir; çökme ya da arıza ile de doğrudan ilgili değildir
void*'denint*'ye dönüşümün donanım kodunda genelde hiçbir karşılığı yoktur ve tipler yalnızca C'de bulunduğu için donanım bu dönüşüm yüzünden çökmez de. Register içindeki tamsayı değeri ise sorun yok sanabilirsiniz, ama mesele donanımda işaretçinin gerçekten tamsayı olup olmaması değil; hizalanmamış işaretçiye dönüştürdüğünüz anda C programının tanım gereği bozulmuş olmasıdırTanımsız davranışın donanımda olmadığını ve çökme ya da arızayla aynı şey olmadığını anlatmaya çalışıyordum. Aynı zamanda “ama bakınca düzgün çalışıyor” diyenlere örnek göstermek istedim; aslında öyle değil
#pragma pack(push, 1)ile struct oluşturursam, tesadüfen hizalanmış bir üye değilse üye işaretçilerini kullanamayacağım anlamına mı geliyor?Bu tür tanımsız davranışlar makuldü ve donanım farkları yüzünden hata çıkmasını büyük bir sorun gören pek kimse yoktu
Ama zamanla saldırgan yorumlar C'yi örtük sözleşmeyle tasarım diline benzetti ve kısıtlar görünmez hale geldi. Bu da RAII'de örtük yıkıcı çağrılarının görünmez olmasına benzer bir sorun yaratıyor
C'de bir işaretçiyi dereference ettiğinizde derleyici fonksiyon imzasına örtük bir null olamaz kısıtı ekliyor. Null olabilecek işaretçiyi fonksiyona verip kontrol ya da assertion yazmama hatası yerine, derleyici bu null olamaz kısıtını işaretçi üzerinde sessizce yayıyor. Sonra bu kısıtın yanlış olduğunu kanıtlarsa fonksiyonu erişilemez sayıyor ve erişilemez fonksiyon çağrısı da çağıran fonksiyonu erişilemez hale getiriyor
C'de tanımsız davranışı öğrenmenin 5 aşaması
İnkar: “İşaretli taşmanın benim makinemde nasıl davrandığını biliyorum”
Öfke: “Bu derleyici çöp! Neden dediğimi yapmıyor?”
Pazarlık: “C'yi düzeltmek için wg14'e şu öneriyi göndereceğim...”
Depresyon: “C kodunda güvenilebilecek bir şey var mı?”
Kabul: “Sadece tanımsız davranış kullanma”
Hizalanmamış erişim için packed struct kullanırsın. Derleyici sihirli biçimde doğru kodu üretir. Aslında derleyici bunu hep yapabiliyordu, sadece yapmıyordu
Strict aliasing için union tabanlı tür dönüştürmesi kullanırsın. Önemli derleyiciler, standart söylemese bile bunun çalıştığını belgelemektedir. Ya da
-fno-strict-aliasingile tamamen kapatırsın. Belleği istediğin gibi yeniden yorumlarsın; keskin köşeler olabilir ama en azından derleyiciden kaynaklanmazTaşmayı
-fwrapvile tanımlarsın.+,-,*yerine__builtin_*_overflowkullanırsan açık hata kontrolünü de bedavaya alırsın. Fonksiyonel arayüzü de güzeldir, verimli kod da üretirAsıl kabul, “normal insanlar C standardını umursamaz” noktasına daha yakın. Standart berbattır; önemli olan derleyicidir. Derleyicilerde bu sorunların çoğunu aşmaya yarayan son derece kullanışlı özellikler var. İnsanların bunları kullanmamasının nedeni “taşınabilir”, “standart” C yazmak istemeleri; o düşünce tarzından çıkmak asıl kabuldür
Bu mantıkla freestanding C ortamında bir Lisp yorumlayıcısı yaptım ve UBSan'den de geçti. Başta patlayacağını sanıyordum ama olmadı; ben yapabildiysem herkes yapabilir
Kod yazan insanlar var oldukça bu son durak olamaz. Hiçbir insan C/C++'ta tanımsız davranıştan tamamen kaçınamaz
Örnekler, gerçekten tanımsız davranıştan çok, girdiye ya da koşullara bağlı olarak tanımsız davranışa dönüşebilecek durumlara benziyor
Bu kadar geniş bakarsan her fonksiyon çağrısı da stack alanını aşabileceği için tanımsız davranış olur. Aslında benzer bir anlamda bunu neredeyse her dil için söyleyebilirsin
C'de dikkat çekmeye yetecek kadar gerçek pürüz zaten var; bu tür sansasyonellik özellikle yeni başlayanların dikkatini dağıtıp daha da zararlı olabilir
STORAGE_ERRORistisnası tanımlıdırhttp://archive.adaic.com/standards/83lrm/html/lrm-11-01.html
Buna göre “alt program çağrısının yürütülmesi sırasında yeterli depolama alanı yoksa” bu istisna yükseltilir
Öncelikle stack alanı bittiğinde ne olacağını tanımlamak mümkündür. Ayrıca her program keyfi büyüklükte stack gerektirmez; bazı programlar önceden hesaplanabilir sabit büyüklükte stack ile yetinir. Hatta bazı dil uygulamaları hiç stack kullanmaz
Bir dil, kalan stack alanını kontrol etmeye yarayan araçlar sunup buna göre garanti verebilir. Ya da stack alanı tükendiğinde çalışacak bir handler kurulmasına izin verebilir
Doğru bakış açısı, tanımsız davranış oluştuğu anda artık dil standardının koruması altında olmadığını kabul etmektir. Bir süre, hatta belki sonsuza kadar düzgün çalışabilir. Ama fiiliyatta farkında olmadan araç zincirinin, derleyici değişiminin ya da yükseltmesinin, mimarinin, runtime'ın, libc sürümü farklarının keyfine bağlı hale gelirsin
Sonunda kum üstüne temel atmış olursun; tanımsız davranışın tehlikesi de budur
Tanımsız davranışın sorunu, bazı mimarilerde çökebilmesi değil
Asıl sorun, derleyicinin böyle kodun asla oluşmayacağını varsayması. Yine de tanımsız davranış içeren kod yazarsan, derleyici, özellikle optimize edici, bunu normal yürütme yoluna uygun gördüğü herhangi bir biçimde çevirebilir. Bu “herhangi bir biçim” bazen büyük kod bloklarının silinmesi gibi çok beklenmedik şeyler olabilir
Üstüne tail recursion da gelirse, debug derlemede sonsuz döngüye hiç ulaşmayan hata ancak optimizasyon seviyesini artırınca ortaya çıkabilir
Daha kötü durumlarda program sessizce çöp verilerle çalışmaya devam edebilir, hard diski biçimlendirebilir ya da saldırgana krallığın anahtarlarını teslim edebilir
“Bunu tanımlı ya da unspecified yapalım” diyenler, derleyicinin programın büyük bölümlerini atabilmesinin asıl mesele olduğunu kaçırıyor
Belirli girdilerde tanımsız davranış olan kod yazıyorsan, o girdiler için programın hiçbir davranışı olmamasını kastetmiş olursun. Derleyicinin o yolu optimizasyonla kaldırmasını ya da tanımlı diğer durumların davranışına yardımcı olacak başka işlemler yapmasını beklersin
Yalnızca tanımsız davranış üzerinden erişilebilen bir log dizesi koyup, bu dizenin binary içinde kalmadığını görmek oldukça tatmin edici
Bir zamanlar dönüşüm hattının en sonunda çalışacağı varsayımıyla bir analiz geçişi yazmıştım ve doğruluk için bu varsayım gerekliydi. Artık optimizasyon olmayacağı için güvenli sanıyordum, ama artık o kadar emin değilim
20 yıldır C kullanıyorum ama son 6 ayda Hacker News'te gördüğüm kadar çok tanımsız davranış konuşulduğunu daha önce hiç görmedim
Gerçek hayattaki sohbetlerde neredeyse hiç geçmiyordu. Kodu yazarsın; çalışmazsa debug eder, düzeltir ya da etrafından dolaşırsın. C'deki tanımsız davranış konusu neden bu kadar düzenli biçimde ana sayfaya çıkıyor, anlamıyorum
Yeni programlama dilleri geliştirmeyi ya da kullanmayı dünyadaki en ilginç şey sayan küçük ama sürekli bir bilgisayar bilimi kitlesi var; bunların bir kısmı bunu yıllarca sürdürüyor
Böyle insanların dil tasarımına ilgi duyması doğal ve C'deki tanımsız davranış da bu alanın içinde. Yine de bunun önemli bir bölümü başlangıçta eski CPU mimarilerini performans kaybı olmadan kapsama çabasından doğmuştu; dolayısıyla buna, tekerleğin yuvarlak olması kadar “tasarım tercihi” demek de biraz tuhaf kaçıyor
GCC 3.2 civarında derleyiciler optimizasyonda tanımsız davranışı çok daha saldırgan biçimde kullanmaya başlayınca epey ünlü “skandallar” yaşandı ve bu yüzden birçok kişi uzun süre GCC 2.95'te kaldı. GCC 3.2, 2002'de çıktı
Tüm şirketler güvenlik ve görünürlük, yani haberlere çıkma meselesini sürekli öne çıkardığı için “güvensizliğe karşı” anlatı aşırı büyüdü
Yeni dünya, vahşi doğayı hiç görmemiş şehir insanlarının çim biçme makinesini görünce korkmasına benziyor. Bıçaklar mı dönüyor? Olacak şey değil!
Gerçek hedef uzak bir iletişim kulesinin üstündeki küçük bir embedded sistemse, “benim makinemde çalışıyor” hiçbir işe yaramaz. Elbette çoğu kişi böyle iş yapmıyor ve buradaki geliştiricilerin büyük kısmı muhtemelen web geliştiricisi, ama bunu yaşamamış olsan bile ilginç bir tartışma. Hatta belki de tam bu yüzden daha ilginç
Derleyicide spesifikasyona göre çalışması gerekirken çalışmayan hatalar olabilir; standartta karşılığı olmayan çok sayıda genişletme vardır; ayrıca standartta tanımsız olsa bile belirli uygulamalarda anlamlı sonuç verilmiş davranışlar da bulunur
Giriş kısmına genel olarak katılıyorum ama örnekler iyi değil ve yazının tamamı LLM ile kodlamayı pazarlamak için paketlenmiş gibi duruyor
Sanki istediği gibi her türlü kodu yazıp bunun tüm ortamlarda aynı davranmasını bekleyen biri gibi görünüyor. Dili böyle tasarlarsan, gerektiğinde platforma özgü yazabilme avantajını kaybedersin
Yazıdaki C++ kodunun bazı bölümleri 10 yıldan uzun süredir deyimsel değil; bugün ise kod kokusu sayılabilir
Dil, ilk tasarlandığı zamandan oldukça farklı bir şeye evrildi. Her yerde ham işaretçiler ve doğrudan işaretçi erişimi görünce, yazının bazı kısımlarını filtreleyerek okumak gerektiği açık oldu
Bir diğer bariz sorun da C ile C++'ı neredeyse aynı dilmiş gibi bir arada ele alması. Günümüzde bu iki dil gerçekte epey uzaklaştı
atomic_intdeğilstd::atomicyazdığını gördümC'deki tanımsız davranışı şöyle anlamak doğru mu?
Program P için, tanımsız davranış üretmeyen bir A girdi kümesi ve bunu üreten tamamlayıcı bir B kümesi vardır
Doğru bir derleyici, P'yi yürütülebilir P' dosyasına derler. A içindeki tüm girdiler için P', P ile aynı davranmalıdır
Ama B içindeki herhangi bir girdi için P' davranışı üzerinde hiçbir gereklilik yoktur
Hizalanmamış işaretçi nedeniyle oluşan tanımsız davranışa dair somut örnek: https://pzemtsov.github.io/2016/11/06/bug-story-alignment-on...