1 puan yazan GN⁺ 2 시간 전 | 1 yorum | WhatsApp'ta paylaş
  • float değerinin kesirli kısmı atıldıktan sonraki hali hedef tamsayı türünün aralığı dışındaysa tanımsız davranış (UB) oluşur; örtük dönüşüm, işlevsel cast ve static_cast bunun hepsinden etkilenir
  • -Wall ve -Wextra bunun için uyarı vermez; -Wconversion da yalnızca örtük dönüşümleri algıladığı için kolayca gözden kaçabilir
  • Microsoft GSL'in güvenli daraltıcı dönüşüm işlevi gsl::narrow da bazı kayan nokta→tamsayı girdilerinde UB üretir; bu yüzden temsil edilemeyen değerlerde istisna fırlattığını söyleyen belgelenmiş davranışını koruyamaz
  • x86'daki CVTTSS2SI temsil edilemeyen değerleri INT_MIN olarak işlerken, AArch64'teki FCVTZS doygun dönüşüm yapar ve NaN'i 0'a çevirir; bu nedenle sonuçlar donanıma göre değişebilir
  • Güvenli dönüşüm için cast işleminden önce aralık kontrolü yapılmalıdır; Clang ve GCC'nin UBSan seçeneği -fsanitize=float-cast-overflow ile sorun tespit edilebilir

Dönüşüm kuralları ve tespitin sınırları

  • C++ kayan nokta-tamsayı dönüşüm kurallarına göre, kesirli kısım atıldıktan sonra değer hedef tamsayı türüne sığmıyorsa tanımsız davranış oluşur
    • Hedef unsigned olsa bile modüler aritmetik uygulanmaz
    • int i0 = f, int(f), static_cast<int>(f) üçü de bazı girdilerde UB üretir
  • Yalnızca genel derleyici uyarılarıyla tüm sorunları bulmak zordur
    • -Wall ve -Wextra bu üç dönüşümün hiçbiri için uyarı vermez
    • -Wconversion yalnızca örtük dönüşümler için uyarı verir
  • Program mevcut işlemci ve derleyicide çalışmaya devam etse bile sonuç platforma göre değişebilir
    • x86'daki CVTTSS2SI, temsil edilemeyen girdileri INT_MIN değerine eşler
    • AArch64'teki FCVTZS doygun işleme yapar ve NaN'i 0'a eşler
    • Gerçekleşmiş bir UB, derleyici farklı dönüşümler uyguladığında kodun aniden hatalı çalışmasına neden olabilir

GSL örneği ve güvenli yaklaşım

  • Microsoft Guidelines Support Library'deki gsl::narrow, hedef türde temsil edilemeyen değerlerde istisna fırlatan güvenli bir daraltıcı dönüşüm olarak sunulur
    • Ancak gerçek kayan nokta→tamsayı dönüşümü bazı girdilerde önce UB'yi çalıştırdığı için belgeyle uyuşmaz
    • GSL tarafı, hedef platformda donanımsal trap gösterimini tetiklemediği sürece iç UB'nin zararsız olduğunu değerlendirdi; bu mantık koda yansıtıldı ve sorun düzeltilmedi
  • Doğru çözüm, cast işleminden önce aralığı kontrol etmektir
  • Clang ve GCC'nin Undefined Behavior Sanitizer'ında -fsanitize=float-cast-overflow kullanılarak bu UB tespit edilebilir
    • Tüm C++ kodunu UBSan ile test etme yaklaşımı öneriliyor

1 yorum

 
GN⁺ 2 시간 전
Lobste.rs yorumları
  • C ve C++'taki pek çok incelikli tanımsız davranışı biliyor olsam da bu örnek şaşırtıcıydı.
    Rust da LLVM IR'nin aynı kayan nokta→tamsayı dönüşüm kurallarını devraldığı için bir süre tanımsız davranışa sahipti; ancak 2020'de daha karmaşık IR üretecek şekilde düzeltildi. Performans farkı büyük olduğundan, değerin sonlu olduğunu ve hedef türün aralığına girdiğini bildiğiniz yüksek performanslı döngüler için denetimsiz kayan nokta→tamsayı dönüşümü de sunuyor.
    C++ Core Guidelines Library'nin bunu önemsiz görmesi saçma. LLVM, dönüşümden elde ettiği bilgiyi sınır denetimlerini kaldırmak için kullandı ve bunun sonucunda sınır denetimi olmasına rağmen dizinin sınırları dışına çıkılan bir vaka var. Bellek güvenliğini ve tanımsız davranıştan kaçınmayı ciddiye alıyorsanız bunu görmezden gelemezsiniz; Herb Sutter'ın “iyi huylu tanımsız davranış”tan söz etmesi de hayal kırıklığı.

  • C++'ta çok sayıda tanımsız davranış olduğunu biliyordum ama bu özellikle şaşırtıcı. Dönüşümü olabildiğince hızlı yapmak istemiş olmaları ve mimariden mimariye bu uç değerleri farklı işleyen komutlar bulunması nedeniyle bunu tanımsız davranış olarak mı bıraktılar, merak ediyorum. İşaretli tamsayı taşmasına benzer şekilde, bu tür kuralların ortaya çıkmasının başlıca nedeni bu gibi görünüyor.

    • Sebep buysa tanımsız davranış değil, uygulama tanımlı davranış olmalıydı. Sıfıra bölmenin bazı mimarilerde trap'e düşmesi nedeniyle tanımsız davranış olmasını anlayabiliyorum.
      Tanımsız davranış; serbest bırakıldıktan sonra kullanma gibi C soyut makinesinin dışındaki etkiler nedeniyle aynı platformda bile tutarlı sonuç garanti edilemeyen ya da bazı hedeflerde trap'e düşebilecek durumlarla sınırlanmalı.
      Standarda uygun bir uygulama tanımsız davranışı kendisi tanımlayabilir; GCC de bazı maddelerde bunu yapıyor. Kararlı semantik performans maliyeti olmadan garanti edilebiliyorsa, hedefe özgü kayan nokta→tamsayı komutunun davranışı aynen tanımlanabilir; ancak diğer uygulamalarda yine de tanımsız davranış olur.
    • Muhtemelen öyledir. PowerPC'nin fctiw ailesi saturating conversion yapar, NaN'i INT_MINe çevirir ve FPSCR bayraklarını da ayarlar. 64 bit Power ISA'daki fctid de daha büyük tamsayılar için aynı işlemi yapar; ancak ikisi de AArch64 veya x86'nın davranışıyla uyuşmaz.
  • Böyle durumlarda bunu uygulama tanımlı davranış ya da belirtilmemiş bir değer olarak ele almak daha doğru. Sonsuzu inte dönüştürdü diye tüm programın C++ standardının kontrolü dışına çıkması mantıklı değil.
    C++26 bazı mantıksız tanımsız davranışları kaldırdı ve bu da açıkça kaldırılmaya aday. Deneylere göre GCC ve Clang bu tanımsız davranışı optimizasyonda kullanmıyor gibi, bu yüzden pratik etkisi sınırlı görünüyor.

  • Spesifikasyonun bu işlemi tanımsız davranış olarak tanımlaması için hiçbir neden olmayan bir başka can sıkıcı örnek.

  • IEEE 754'ten hoşlanmamak için bir neden daha çıktı. Başka kütüphaneler veya performans nedeniyle mutlaka gerekmedikçe kayan nokta yerine mümkün olduğunca saf tamsayı, büyük tamsayı pay ve payda kullanan rasyonel sayılar ya da sabit noktalı ondalık kullanıyorum.

    • Bu durumun IEEE 754 ile ilgisi yok; tamamen C++'ın hatası.
  • C veya C++ kullandıysanız float32 ile int32, hatta int64 arasındaki fark bile şaşırtıcı olmamalı. Büyük üs kullanan bir float32, int64ten çok daha büyük tamsayı değerleri temsil edebilir.
    Birbirinin üst kümesi olmayan ve ifade gücü farklı biçimler arasında, dil sözdiziminden bağımsız olarak kayan nokta→tamsayı dönüşümünün güvenli olduğunu varsaymak için neden yok.

    • Asıl noktayı kaçırıyorsunuz. Kayan noktanın tüm değerlerinin korunamaması ile tanımsız davranış farklı şeylerdir.
      uint32_t de uint64_tnin tüm değerlerini temsil edemez ama dönüşüm semantiği kırpma olarak tanımlanmıştır. Burada esasen farklı olan sorun, belirli girdilerde derleyicinin her şeyi yapabilmesidir.
    • Temsil aralıklarının farklı olması güvenli bir dönüşüm tanımlanamayacağı anlamına gelmez. Rust, kayan nokta→tamsayı dönüşümünü açıkça tanımlar; Java 26 dil spesifikasyonu ise 5.1.3 bölümünde dönüşüm prosedürünü ayrıntılı olarak belirtir.
      Düşük seviyeli bir dilde bunu CVTTSS2SI veya FCVTZS gibi assembly komutlarına karşılık gelecek şekilde yapmak da mümkün. Düşük seviyeli diller assembly'ye yakın olduğundan ve diğer “iyi huylu” tanımsız davranışlar da çok tartışmalı olduğundan, bu dönüşümün tanımsız davranışa izin vermesi daha da şaşırtıcı.
    • Donanıma bağlı olarak uygulamaya özgü çöp değerler üretmesi ya da donanım/denetim araçlarının trap'e düşürüp programı durdurması çok şaşırtıcı değil.
      Ama ilgisiz kodu tuhaf biçimde yanlış derlemeye, sabit diski biçimlendirmeye ya da “burnunuzdan iblisler çıkmasına” bile izin verilmesi şaşırtıcı.
      Çift free veya dizi sınırı dışına yazma için her şeyin olabileceği tanımsız davranış kavramı makul; ancak C ve C++, uygulama tanımlı çöp değerler ya da programın durdurulması gibi daha sıkı kurallar konabilecek yerlerde bile bunu aşırı kullanıyor. Rust, bazı tamsayı taşmalarını debug modunda trap'e düşürür ve release modunda belirtilmemiş bir değer döndürür; ama alakasız kodu bozmasına izin vermez.
      C++'ın Java veya Rust olması gerekmiyor, ancak gereksiz tanımsız davranışları azaltmak onu kesinlikle daha iyi bir dil yapar.
    • Benim için yeni bir bilgiydi. Temsil edilemeyen bir kayan nokta değeri tamsayıya dönüştürülürse sonuç tamsayısına saçma bir değer gireceğini sanıyordum; tüm programın sonsuza dek kirlenip C++ standardının kontrolü dışına çıkacağını bilmiyordum.