Evet, ISO C ++ bu seçimi yapmak için uygulamalara izin verir (ancak zorunlu değildir).
Ancak, programın UB ile karşılaşması durumunda, örneğin hataları bulmanıza yardımcı olacak bir yol olarak, ISO C ++ 'ın bir derleyicinin bilerek çökmesini (örneğin geçersiz bir talimatla) yayınlamasına izin verdiğini unutmayın. (Ya da bir DeathStation 9000 olduğu için. C ++ uygulamasının herhangi bir gerçek amaç için yararlı olması için kesinlikle uygun olmak yeterli değildir). Böylece ISO C ++, bir derleyicinin başlatılmamış bir kodu okuyan benzer kodda bile çökmesini (tamamen farklı nedenlerle) yapmasına izin verecektir uint32_t
. Her ne kadar bu, tuzak temsili olmayan sabit düzenli bir tip olmalıdır.
Gerçek uygulamaların nasıl çalıştığı hakkında ilginç bir soru, ancak cevap farklı olsa bile, kodunuzun hala güvenli olmayacağını unutmayın, çünkü modern C ++, montaj dilinin taşınabilir bir sürümü değildir.
X86-64 System V ABI için derlersiniz ; bu bool
, bir kayıttaki bir işlev argümanı olarakfalse=0
true=1
kayıt 1'in bit desenleri ve düşük 8 bitiyle temsil edildiğini belirtir . Bellekte, bool
yine 0 veya 1 tamsayı değerine sahip olması gereken 1 baytlık bir türdür.
(Bir ABI, aynı platform için derleyicilerin üzerinde anlaştığı bir dizi uygulama seçeneğidir, böylece tür boyutları, yapı düzeni kuralları ve çağrı kuralları dahil olmak üzere birbirlerinin işlevlerini çağıran kodlar oluşturabilirler.)
ISO C ++ bunu belirtmez, ancak bu ABI kararı yaygındır, çünkü bool-> int dönüşümünü ucuz yapar (sadece sıfır uzatma) . bool
Herhangi bir mimari (sadece x86 için değil) için derleyicinin 0 veya 1 varsaymasına izin vermeyen ABI'lerin farkında değilim . Bu optimizasyonlar gibi tanır !mybool
ile xor eax,1
: düşük bit çevirmek için tek işlemci talimatında 0 ile 1 arasında bir bit / tamsayı / bool çevirebilirsiniz Herhangi olası bir kod . Veya türler a&&b
için bit yönünde VE derleme bool
. Bazı derleyiciler gerçekte Boolean değerlerini derleyicilerde 8 bit olarak kullanırlar. Üzerlerindeki işlemler yetersiz mi? .
Genel olarak, as-if kuralı, derleyicinin derlenmekte olduğu hedef platformda doğru olan şeylerden yararlanmasına izin verir , çünkü sonuç, C ++ kaynağıyla aynı dıştan görünür davranışı uygulayan yürütülebilir kod olacaktır. (Tanımsız Davranış'ın aslında "harici olarak görünür" olana getirdiği tüm kısıtlamalarla: bir hata ayıklayıcıyla değil, iyi biçimlendirilmiş / yasal bir C ++ programındaki başka bir iş parçacığından.)
Derleyici kesinlikle onun kod-gen bir ABI teminat tam olarak yararlanabilmek ve optimize hangi bulundu gibi bir kod yapmak için izin strlen(whichString)
için
5U - boolValue
. (BTW, bu optimizasyon biraz zekidir, ancak memcpy
anlık verilerin depolanması olarak dallanma ve satır içi ile karşılaştırıldığında dar görüşlü olabilir 2 )
Veya derleyici bir işaretçi tablosu oluşturabilir ve bool
yine 0 veya 1 olduğunu varsayarak tamsayı değeri ile dizine ekleyebilirdi . ( Bu olasılık @ Barmar'ın cevabının önerdiği şeydir .)
Kişisel __attribute((noinline))
optimizasyonu ile yapıcı olarak kullanılmak üzere yığınından bir bayt yüklenirken sadece tınlamak yol açtı etkin uninitializedBool
. Bu nesne için yer yapılmış main
olan push rax
(olabildiğince verimli olarak ilgili küçük ve çeşitli nedenle hangi sub rsp, 8
olursa olsun bu yüzden çöp girişte AL oldu) main
onun için kullanılan değerdir uninitializedBool
. Bu yüzden aslında sadece olmayan değerlere sahipsiniz 0
.
5U - random garbage
büyük bir imzasız değere kolayca sarılabilir, memcpy'nin eşlenmemiş belleğe gitmesine neden olur. Hedef yığını değil, statik depolama alanındadır, bu nedenle bir dönüş adresinin veya başka bir şeyin üzerine yazmazsınız.
Diğer uygulamalar farklı seçimler yapabilir, örn. false=0
Ve true=any non-zero value
. Sonra clang muhtemelen bu özel UB örneği için kilitlenen kod yapmaz . (Ama yine de isteseydi izin verilecekti.) X86-64'ün yaptıklarından başka bir şey seçen herhangi bir uygulama bilmiyorum bool
, ancak C ++ standardı kimsenin yapmadığı ve hatta yapmak istemeyeceği birçok şeye izin veriyor mevcut CPU'lar gibi bir donanım.
ISO C ++, a nesnesinin temsilini incelediğinizde veya değiştirdiğinizde ne bulacağınızı belirtmeden bırakırbool
. (örn memcpy
. bool
içine girerek unsigned char
izin verebilirsiniz, çünkü char*
herhangi bir şeyi takma adlandırabilirsiniz. Ve unsigned char
hiçbir dolgu bitine sahip olmadığı garanti edilir, bu nedenle C ++ standardı resmi olarak herhangi bir UB olmadan nesne temsillerini hexdump yapmanızı sağlar. Nesneyi kopyalamak için işaretçi döküm temsil, char foo = my_bool
elbette, atamaktan farklıdır , bu nedenle 0 veya 1'e booleanization olmaz ve ham nesne temsilini alırsınız.)
Sen ettik kısmen ile derleyici Bu yürütme yolunda UB "gizli"noinline
. Bununla birlikte, satır içi olmasa bile, süreçler arası optimizasyonlar hala başka bir fonksiyonun tanımına bağlı olan fonksiyonun bir versiyonunu yapabilir. (Birincisi, clang, sembol-interpozisyonun olabileceği Unix paylaşılan kütüphanesini değil, yürütülebilir bir dosya yapıyor. İkincisi, tanımın içindeki class{}
tanım, böylece tüm çeviri birimlerinin aynı tanıma sahip olması gerekir. inline
Anahtar kelimede olduğu gibi.)
Böylece bir derleyici , tanım olarak sadece bir ret
veya ud2
(yasadışı talimat) yayabilir main
, çünkü main
kaçınılmaz olarak üstünden başlayan yürütme yolu, Tanımlanamayan Davranış ile karşılaşır. (Derleyici, satır içi olmayan kurucu aracılığıyla yolu izlemeye karar verirse derleme zamanında görebileceği.)
UB ile karşılaşan herhangi bir program tüm varlığı için tanımsızdır. Ama if()
aslında hiç çalışmayan bir fonksiyonun veya dalın içindeki UB , programın geri kalanını bozmaz. Uygulamada bu, derleyicilerin yasadışı bir talimat vermeye veya bir ret
şey yaymaya veya herhangi bir şey yaymaya ve bir sonraki bloğa / fonksiyona girmeye karar verebileceği anlamına gelir; derleme zamanında UB içerdiği veya yol açtığı kanıtlanabilen tüm temel blok için.
GCC ve pratikte Clang do aslında bazen yayarlar ud2
yerine bile hiçbir anlam yürütme yolları için kod oluşturmak için çalışmak yerine, UB üzerinde. Veya bir void
işlev dışı durumun sonundan düşme gibi durumlarda , gcc bazen bir ret
talimatı atlar . Eğer "işlevim RAX'taki çöp ne olursa olsun geri dönecek" diye düşünüyorsan, yanılıyorsun. Modern C ++ derleyicileri artık portatif bir montaj dili gibi davranmıyor. Programınızın, işlevinizin bağımsız, satır içi olmayan bir sürümünün nasıl görünebileceği konusunda varsayımlar yapmadan, gerçekten geçerli C ++ olması gerekir.
Başka bir eğlenceli örnek, mmap'ed belleğe hizalanmamış erişim neden AMD64'te bazen segfault oluyor? . x86, hizalanmamış tamsayılarda hata değil, değil mi? Öyleyse yanlış hizalanmış uint16_t*
bir sorun neden sorun olsun ki? Çünkü alignof(uint16_t) == 2
ve bu varsayımı ihlal etmek, SSE2 ile otomatik vektörleştirilirken bir segfault yol açmıştır.
Ayrıca bkz. Her C Programcısının bir clang geliştiricisi tarafından yazılan Tanımsız Davranış # 1/3 Hakkında Bilmesi Gerekenler .
Anahtar nokta: derleyici derleme zamanında UB fark ettiyseniz, bu olabilir "mola" nedenler bile herhangi bit deseni için geçerli bir nesne temsilidir ABI hedefleyen eğer UB senin kodu ile yol (şaşırtıcı asm yayarlar) bool
.
Programcı tarafından, özellikle modern derleyicilerin uyardığı birçok hataya karşı tam bir düşmanlık bekliyoruz. Bu yüzden -Wall
uyarıları kullanmalı ve düzeltmelisiniz. C ++ kullanıcı dostu bir dil değildir ve C ++ 'da bir şey, derlediğiniz hedefe güvenli bir şekilde güvenli olsa bile güvenli olmayabilir. (örneğin, imzalı taşma C ++ 'da UB'dir ve siz kullanmadıkça, 2'nin tamamlayıcısı x86 için derleme yaparken bile derleyiciler gerçekleşmeyeceğini varsayar clang/gcc -fwrapv
.)
Derleme zamanı görünür UB her zaman tehlikelidir ve UB'yi derleyiciden gerçekten gizlediğinizden ve böylece ne tür bir asm üreteceğinden emin olabileceğinizden (bağlantı zamanı optimizasyonu ile) gerçekten zor.
Aşırı dramatik olmamak; genellikle derleyiciler bazı şeylerden kurtulmanıza izin verir ve bir şey UB olsa bile beklediğiniz gibi kod yayarlar. Ancak, derleyici geliştiricilerin değer aralıkları hakkında daha fazla bilgi toplayan bir optimizasyon uygulaması gelecekte de bir sorun olacaktır (örneğin, bir değişkenin negatif olmadığı, belki de işaret uzantısını x86- 64). Örneğin, mevcut gcc ve clang'da yapmak her zaman yanlış olarak tmp = a+INT_MIN
optimize edilmez a<0
, sadece bu tmp
her zaman negatiftir. (Çünkü INT_MIN
+ a=INT_MAX
bu 2'nin tamamlayıcı hedefinde negatiftir ve a
bundan daha yüksek olamaz.)
Dolayısıyla, gcc / clang şu anda bir hesaplamanın girdileri için aralık bilgisi türetmek için geriye doğru ilerlemiyor , sadece imzalı taşma varsayımına dayanan sonuçlara dayanıyor: Godbolt örneği . Bu optimizasyon kasıtlı olarak kullanıcı dostu ya da ne adına "özledim" olup olmadığını bilmiyorum.
Ayrıca, uygulamaların (derleyiciler olarak da bilinir) ISO C ++ 'nın tanımsız bıraktığı davranışı tanımlamasına izin verildiğini unutmayın . Örneğin, tüm derleyiciler (gibi destek Intel'in intrinsics o _mm_add_ps(__m128, __m128)
manuel SIMD vektörleştirme için) bile eğer C ++ UB olduğunu yanlış hizalanmış işaretçileri, şekillendirme izin vermelidir yok onlara KQUEUE. a veya değil, __m128i _mm_loadu_si128(const __m128i *)
yanlış hizalanmış bir __m128i*
argüman alarak hizalanmamış yükler yapar . Donanım vektörü işaretçisi ve karşılık gelen tip arasındaki reinterpret_cast tanımsız bir davranış mı?void*
char*
GNU C / C ++ -fwrapv
, normal işaretli taşma UB kurallarından ayrı olarak negatif işaretli bir sayıyı (olmadan da ) sola kaydırma davranışını da tanımlar . ( Bu, ISO C ++ 'da UB'dir, işaretli sayıların sağ kaymaları uygulama tanımlıdır (mantıksal ve aritmetik); kaliteli uygulamalar HW'de aritmetik sağ kayması olan aritmetiği seçer, ancak ISO C ++ belirtmez). Bu, C standartlarının bir şekilde tanımlanması için uygulamaların gerektirdiği uygulama tanımlı davranışı tanımlamanın yanı sıra GCC kılavuzunun Tamsayı bölümünde belgelenmiştir .
Derleyici geliştiricilerin önem verdiği kesinlikle uygulama kalitesi sorunları vardır; genellikle kasıtlı olarak düşmanca olan derleyiciler yapmaya çalışmazlar , ancak daha iyi optimize etmek için C ++ 'daki tüm UB çukurlarından (tanımlamayı seçtikleri hariç) faydalanmak zaman zaman neredeyse ayırt edilemez olabilir.
Dipnot 1 : Üst 56 bit, her zamanki gibi bir kayıttan daha dar tipler için callee'nin göz ardı etmesi gereken çöp olabilir.
( Diğer ABI yapmak burada farklı seçimler yapmak . Geçirilen veya MIPS64 ve PowerPC64 gibi fonksiyonlar, döndüğünde bazı sıfır veya oturum genişletilmiş bir kayıt doldurmak için olmak dar tamsayı tiplerini gerektirir. Son bölümüne bakın bu X86-64 cevap önceki ISA'larla karşılaştırılan )
Örneğin, bir arayan a & 0x01010101
, arama yapmadan önce RDI'da hesaplamış ve başka bir şey için kullanmış olabilir bool_func(a&1)
. Arayan bunu optimize edebilir, &1
çünkü bunu zaten bir parçası olarak düşük bayta yaptı ve arayanın and edi, 0x01010101
yüksek baytları görmezden gelmesi gerektiğini biliyor.
Veya 3. bağımsız değişken olarak bir bool iletilirse, kod boyutu için optimize eden bir arayan, mov dl, [mem]
bunun yerine yükler movzx edx, [mem]
, RDX'in eski değerine yanlış bir bağımlılık pahasına 1 bayt kaydeder (veya diğer kısmi kayıt efekti, CPU modelinde). Ya da ilk argüman mov dil, byte [r10]
yerine movzx edi, byte [r10]
, her ikisi de yine de bir REX öneki gerektirir.
Bu yüzden çınlama yayar olduğu movzx eax, dil
içinde Serialize
yerine, sub eax, edi
. (Tamsayı args için, çınlama yerine sıfır veya gcc ve clang belgesiz davranışa bağlı olarak, bu ABI kural ihlal eder. 32 bit, dar tamsayılar oturum uzanan bir işareti mi ya da bir işaretçiye ofset 32bit eklerken sıfır uzantısı gerekli x86-64 ABI?
bu yüzden aynı şeyi yapmaz görmeye ilgilenen edildi bool
.)
Dipnot 2: Dallanma sonrasında, sadece 4 bayt- mov
orta veya 4 bayt + 1 baytlık bir mağazanız olur. Uzunluk, mağaza genişlikleri + ofsetlerde örtüktür.
OTOH, glibc memcpy, uzunluğa bağlı bir çakışma ile iki adet 4 baytlık yük / depo yapacak, bu gerçekten her şeyi booleandaki koşullu dallardan arındırır. Glibc'nin memcpy / memmove içindeki L(between_4_7):
bloğa bakın . Ya da en azından, memcpy'nin dallamasındaki boole için bir yığın boyutu seçmek için aynı şekilde gidin.
Satır içi ise, 2x mov
-immediate + cmov
ve koşullu bir ofset kullanabilirsiniz veya dize verilerini bellekte bırakabilirsiniz.
Veya Intel Ice Lake için ayarlama yapıyorsanız ( Hızlı Kısa REP MOV özelliği ile ), gerçek bir rep movsb
optimal olabilir. glibc , bu özelliğe sahip CPU'lardaki küçük boyutlar için memcpy
kullanmaya başlayarak rep movsb
çok fazla dallanma tasarrufu sağlayabilir.
UB tespiti ve başlatılmamış değerlerin kullanımı için araçlar
Gcc ve clang'da, -fsanitize=undefined
çalışma zamanında gerçekleşen UB'yi uyaracak veya hata verecek çalışma zamanı enstrümanları eklemek için derleyebilirsiniz . Yine de bu birimselleştirilmiş değişkenleri yakalamaz. (Çünkü "başlatılmamış" bir bit için yer açmak için tip boyutlarını arttırmaz).
Bkz. Https://developers.redhat.com/blog/2014/10/16/gcc-undefined-behavior-sanitizer-ubsan/
Başlatılmamış verilerin kullanımını bulmak için clang / LLVM'de Adres Temizleyici ve Bellek Temizleyici bulunur. https://github.com/google/sanitizers/wiki/MemorySanitizer , clang -fsanitize=memory -fPIE -pie
başlatılmamış bellek okumalarını algılama örneklerini gösterir . Optimizasyon olmadan derlerseniz en iyi sonucu verebilir , bu nedenle değişkenlerin tüm okumaları asm'deki bellekten yüklenir. -O2
Yükün optimize edilmeyeceği bir durumda kullanıldığını gösterirler . Ben kendim denemedim. (Bazı durumlarda, örneğin bir diziyi toplamadan önce bir akümülatör başlatma değil, clang -O3, hiç başlatılmadığı bir vektör kaydına toplanan bir kod yayar. . Fakat-fsanitize=memory
oluşturulan grubu değiştirir ve bunun için bir kontrolle sonuçlanabilir.)
Başlatılmamış belleğin kopyalanmasını ve bununla birlikte basit mantık ve aritmetik işlemleri tolere edecektir. Genel olarak, MemorySanitizer başlatılmamış verilerin bellekteki yayılmasını sessizce izler ve başlatılmamış bir değere bağlı olarak bir kod dalı alındığında (veya alınmadığında) bir uyarı bildirir.
MemorySanitizer, Valgrind'de (Memcheck aracı) bulunan bir işlev alt kümesi uygular.
Çağrı glibc için çünkü bu durum için çalışması gerektiğini memcpy
bir ile length
başlatılmamış bellekten hesaplanan bir dalda sonucu (kütüphane içinde) dayalı olacaktır length
. Az önce kullanılan cmov
, dizine alma ve iki mağaza kullanan tamamen dalsız bir sürümü satır içine alsaydı, işe yaramamış olabilir.
Valgrind'smemcheck
de bu tür bir sorunu arayacak, yine programın başlatılmamış verilerin etrafına kopyalanıp kopyalanmadığından şikayet etmeyecek. Ancak, başlatılmamış verilere bağlı olarak dışarıdan görünen herhangi bir davranışı yakalamaya çalışmak için "Koşullu atlama veya hareketin başlatılmamış değerlere bağlı olduğunu" algılayacağını söylüyor.
Belki de sadece bir yükü işaretlememenin arkasındaki fikir, yapıların dolgusu olabileceğidir ve tüm yapıyı (dolgu dahil) geniş bir vektör yükü / deposu ile kopyalamak, tek tek üyeler bir seferde sadece bir tane yazılsa bile bir hata değildir. Asm seviyesinde, neyin dolgu olduğu ve değerin gerçekte neyin bir parçası olduğu hakkındaki bilgiler kaybolmuştur.