Ya, ISO C ++ memungkinkan (tetapi tidak mengharuskan) implementasi untuk membuat pilihan ini.
Tetapi juga perhatikan bahwa ISO C ++ memungkinkan kompiler untuk memancarkan kode yang sengaja macet (misalnya dengan instruksi ilegal) jika program menemui UB, misalnya sebagai cara untuk membantu Anda menemukan kesalahan. (Atau karena itu adalah DeathStation 9000. Menjadi benar-benar menyesuaikan saja tidak cukup untuk implementasi C ++ berguna untuk tujuan nyata apa pun). Jadi ISO C ++ akan memungkinkan kompiler untuk membuat asm yang crash (untuk alasan yang sama sekali berbeda) bahkan pada kode serupa yang membaca yang tidak diinisialisasi uint32_t
. Meskipun itu diperlukan tipe tata letak tetap tanpa representasi trap.
Ini adalah pertanyaan menarik tentang bagaimana implementasi nyata bekerja, tetapi ingat bahwa meskipun jawabannya berbeda, kode Anda tetap tidak aman karena C ++ modern bukan versi portabel bahasa rakitan.
Anda sedang mengkompilasi untuk System V ABI x86-64 , yang menentukan bahwa bool
sebagai fungsi arg dalam register diwakili oleh pola-bit false=0
dantrue=1
dalam bit 8 yang rendah dari register 1 . Dalam memori, bool
adalah tipe 1-byte yang lagi-lagi harus memiliki nilai integer 0 atau 1.
(ABI adalah sekumpulan pilihan implementasi yang disetujui oleh penyusun untuk platform yang sama sehingga mereka dapat membuat kode yang memanggil fungsi masing-masing, termasuk ukuran tipe, aturan tata letak struct, dan konvensi pemanggilan.)
ISO C ++ tidak menentukannya, tetapi keputusan ABI ini tersebar luas karena membuat konversi bool-> int menjadi murah (hanya ekstensi-nol) . Saya tidak mengetahui adanya ABI yang tidak membiarkan kompiler menganggap 0 atau 1 untuk bool
, untuk arsitektur apa pun (bukan hanya x86). Ini memungkinkan optimasi seperti !mybool
dengan xor eax,1
membalik bit rendah: Setiap kode yang mungkin dapat membalik sedikit / integer / bool antara 0 dan 1 dalam instruksi CPU tunggal . Atau mengkompilasi a&&b
ke bitwise DAN untuk bool
jenis. Beberapa kompiler benar-benar memanfaatkan nilai Boolean sebagai 8 bit dalam kompiler. Apakah operasi pada mereka tidak efisien? .
Secara umum, aturan as-if memungkinkan memungkinkan kompiler untuk mengambil keuntungan dari hal-hal yang benar pada platform target yang dikompilasi , karena hasil akhirnya akan menjadi kode yang dapat dieksekusi yang mengimplementasikan perilaku yang terlihat secara eksternal sama seperti sumber C ++. (Dengan semua pembatasan yang dilakukan Perilaku Tidak Terdefinisi pada apa yang sebenarnya "terlihat secara eksternal": tidak dengan debugger, tetapi dari utas lain dalam program C ++ yang legal / baik.)
Compiler pasti diizinkan untuk mengambil keuntungan penuh dari jaminan ABI di nya kode-gen, dan membuat kode seperti Anda menemukan yang mengoptimalkan strlen(whichString)
untuk
5U - boolValue
. (BTW, optimasi ini agak pintar, tapi mungkin picik vs bercabang dan inlining memcpy
sebagai penyimpan data langsung 2. )
Atau kompiler bisa saja membuat tabel pointer dan mengindeksnya dengan nilai integer bool
, sekali lagi dengan anggapan itu adalah 0 atau 1. ( Kemungkinan inilah yang disarankan oleh jawaban Barmar .)
__attribute((noinline))
Konstruktor Anda dengan optimisasi diaktifkan menyebabkan hanya memuat byte dari tumpukan untuk digunakan sebagai uninitializedBool
. Itu membuat ruang untuk objek main
dengan push rax
(yang lebih kecil dan karena berbagai alasan tentang seefisien sub rsp, 8
), jadi apa pun sampah di AL pada entri main
adalah nilai yang digunakan untuk itu uninitializedBool
. Inilah sebabnya mengapa Anda benar-benar mendapatkan nilai yang tidak adil 0
.
5U - random garbage
dapat dengan mudah membungkus ke nilai yang tidak ditandatangani besar, memimpin memcpy untuk masuk ke memori yang belum dipetakan. Tujuannya adalah penyimpanan statis, bukan tumpukan, jadi Anda tidak menimpa alamat pengirim atau sesuatu.
Implementasi lain dapat membuat pilihan yang berbeda, misalnya false=0
dan true=any non-zero value
. Kemudian dentang mungkin tidak akan membuat kode yang crash untuk ini contoh spesifik dari UB. (Tapi itu masih akan diizinkan jika ingin.) Saya tidak tahu implementasi apa pun yang memilih untuk apa x86-64 dilakukan bool
, tetapi standar C ++ memungkinkan banyak hal yang tidak dilakukan oleh siapa pun atau bahkan ingin dilakukan pada perangkat keras yang mirip dengan CPU saat ini.
ISO C ++ membiarkannya tidak ditentukan apa yang akan Anda temukan ketika Anda memeriksa atau memodifikasi representasi objek dari abool
. (misalnya dengan memcpy
memasukkan bool
ke dalam unsigned char
, yang diizinkan untuk Anda lakukan karena char*
bisa alias apa saja. Dan unsigned char
dijamin tidak memiliki bit padding, sehingga standar C ++ memungkinkan Anda secara hexdump representasi objek tanpa UB. Pointer-casting untuk menyalin objek representasi berbeda dari penetapan char foo = my_bool
, tentu saja, jadi booleanisasi ke 0 atau 1 tidak akan terjadi dan Anda akan mendapatkan representasi objek mentah.)
Anda telah sebagian "menyembunyikan" UB di jalur eksekusi ini dari kompiler dengannoinline
. Meskipun tidak sejajar, optimasi interprocedural masih bisa membuat versi fungsi yang tergantung pada definisi fungsi lain. (Pertama, dentang membuat yang dapat dieksekusi, bukan perpustakaan bersama Unix di mana simbol-interposisi dapat terjadi. Kedua, definisi di dalam class{}
definisi sehingga semua unit terjemahan harus memiliki definisi yang sama. Seperti dengan inline
kata kunci.)
Jadi penyusun dapat memancarkan hanya ret
atau ud2
(instruksi ilegal) sebagai definisi untuk main
, karena jalur eksekusi dimulai dari atas yang main
tak terhindarkan menghadapi Perilaku Tidak Terdefinisi. (Yang dapat dilihat kompilator pada waktu kompilasi jika memutuskan untuk mengikuti jalur melalui konstruktor non-inline.)
Program apa pun yang bertemu UB benar-benar tidak ditentukan untuk seluruh keberadaannya. Tetapi UB di dalam fungsi atau if()
cabang yang tidak pernah benar-benar berjalan tidak merusak sisa program. Dalam prakteknya itu berarti bahwa penyusun dapat memutuskan untuk mengeluarkan instruksi ilegal, atau ret
, atau tidak memancarkan apa pun dan jatuh ke blok / fungsi berikutnya, untuk seluruh blok dasar yang dapat dibuktikan pada waktu kompilasi untuk mengandung atau mengarah ke UB.
GCC dan Dentang dalam praktek kadang-kadang benar - benar memancarkan ud2
di UB, bukannya mencoba menghasilkan kode untuk jalur eksekusi yang tidak masuk akal. Atau untuk kasus-kasus seperti jatuh dari ujung non- void
fungsi, gcc terkadang akan menghilangkan ret
instruksi. Jika Anda berpikir bahwa "fungsi saya hanya akan kembali dengan sampah apa pun di RAX", Anda salah besar. Kompiler C ++ modern tidak lagi memperlakukan bahasa seperti bahasa rakitan portabel. Program Anda benar-benar harus valid C ++, tanpa membuat asumsi tentang bagaimana versi yang berdiri sendiri dari fungsi Anda mungkin terlihat dalam asm.
Contoh lain yang menyenangkan adalah Mengapa akses yang tidak selaras ke memori mmap'ed kadang-kadang terpisah pada AMD64? . x86 tidak kesalahan pada bilangan bulat yang tidak selaras, kan? Jadi mengapa orang yang tidak selaras uint16_t*
menjadi masalah? Karena alignof(uint16_t) == 2
, dan melanggar asumsi itu menyebabkan segfault ketika auto-vectorizing dengan SSE2.
Lihat juga Apa yang Harus Diketahui Setiap Pemrogram C Tentang Perilaku Tidak Terdefinisi # 1/3 , sebuah artikel oleh pengembang dentang.
Poin kunci: jika kompiler memperhatikan UB pada waktu kompilasi, itu bisa "memecah" (memancarkan asm yang mengejutkan) jalur melalui kode Anda yang menyebabkan UB bahkan jika menargetkan ABI di mana setiap bit-pola adalah representasi objek yang valid untuk bool
.
Harapkan permusuhan total terhadap banyak kesalahan oleh programmer, terutama hal-hal yang diingatkan oleh kompiler modern. Inilah sebabnya mengapa Anda harus menggunakan -Wall
dan memperbaiki peringatan. C ++ bukan bahasa yang ramah pengguna, dan sesuatu dalam C ++ bisa tidak aman bahkan jika itu akan aman dalam asm pada target yang Anda kompilasi. (mis. Overflow yang ditandatangani adalah UB dalam C ++ dan kompiler akan menganggap itu tidak terjadi, bahkan ketika mengkompilasi untuk komplemen 2 x86, kecuali jika Anda menggunakannya clang/gcc -fwrapv
.)
Kompilasi-waktu-kelihatan UB selalu berbahaya, dan sangat sulit untuk memastikan (dengan optimasi tautan-waktu) bahwa Anda telah benar-benar menyembunyikan UB dari kompiler dan karenanya dapat alasan tentang jenis asm yang akan dihasilkannya.
Tidak terlalu dramatis; sering kompiler membiarkan Anda lolos dengan beberapa hal dan memancarkan kode seperti yang Anda harapkan bahkan ketika ada sesuatu yang UB. Tapi mungkin itu akan menjadi masalah di masa depan jika compiler dev menerapkan beberapa optimasi yang memperoleh lebih banyak info tentang rentang nilai (misalnya bahwa variabel tidak negatif, mungkin memungkinkannya untuk mengoptimalkan ekstensi-tanda untuk membebaskan nol-ekstensi pada x86- 64). Misalnya, dalam gcc dan dentang saat ini, melakukan tmp = a+INT_MIN
tidak mengoptimalkan a<0
sebagai selalu-salah, hanya saja tmp
selalu negatif. (Karena INT_MIN
+ a=INT_MAX
negatif pada target komplemen 2 ini, dan a
tidak mungkin lebih tinggi dari itu.)
Jadi gcc / dentang saat ini tidak mundur untuk mendapatkan info rentang untuk input perhitungan, hanya pada hasil berdasarkan asumsi tidak ada limpahan ditandatangani: contoh pada Godbolt . Saya tidak tahu apakah ini optimasi yang sengaja "dilewatkan" atas nama keramahan pengguna atau apa.
Perhatikan juga bahwa implementasi (kompiler alias) diizinkan untuk mendefinisikan perilaku yang tidak ditentukan oleh ISO C ++ . Sebagai contoh, semua kompiler yang mendukung intrinsik Intel (seperti _mm_add_ps(__m128, __m128)
untuk vektorisasi SIMD manual) harus memungkinkan pembentukan pointer yang tidak sejajar, yaitu UB dalam C ++ bahkan jika Anda tidak melakukan dereferensi. __m128i _mm_loadu_si128(const __m128i *)
melakukan banyak unaligned dengan mengambil __m128i*
argumen yang tidak selaras , bukan a void*
atau char*
. Apakah `reinterpret_cast`ing antara pointer vektor perangkat keras dan tipe yang sesuai merupakan perilaku yang tidak terdefinisi?
GNU C / C ++ juga mendefinisikan perilaku menggeser angka bertanda negatif (bahkan tanpa -fwrapv
), secara terpisah dari aturan UB yang ditandatangani-limpahan normal. ( Ini adalah UB dalam ISO C ++ , sementara pergeseran kanan dari angka yang ditandatangani didefinisikan implementasi (logis vs aritmatika); implementasi berkualitas baik memilih aritmatika pada HW yang memiliki pergeseran aritmatika yang benar, tetapi ISO C ++ tidak menentukan). Ini didokumentasikan di bagian Integer manual GCC , bersama dengan mendefinisikan perilaku yang didefinisikan implementasi yang standar C membutuhkan implementasi untuk menentukan satu atau lain cara.
Pasti ada masalah kualitas implementasi yang diperhatikan pengembang kompiler; mereka umumnya tidak mencoba membuat kompiler yang sengaja dimusuhi, tetapi mengambil keuntungan dari semua lubang UB di C ++ (kecuali yang mereka pilih untuk didefinisikan) untuk mengoptimalkan yang lebih baik kadang-kadang hampir tidak bisa dibedakan.
Catatan Kaki 1 : 56 bit bagian atas dapat berupa sampah yang harus diabaikan oleh callee, seperti biasa untuk tipe yang lebih sempit daripada register.
( ABI lain memang membuat pilihan berbeda di sini . Beberapa memang membutuhkan tipe integer sempit menjadi nol atau diperpanjang untuk mengisi register ketika diteruskan ke atau dikembalikan dari fungsi, seperti MIPS64 dan PowerPC64. Lihat bagian terakhir dari jawaban x86-64 ini yang membandingkan vs. ISA sebelumnya .)
Misalnya, seorang penelepon mungkin telah menghitung a & 0x01010101
dalam RDI dan menggunakannya untuk hal lain, sebelum menelepon bool_func(a&1)
. Penelepon dapat mengoptimalkan jauh &1
karena sudah melakukan itu ke byte rendah sebagai bagian dari and edi, 0x01010101
, dan ia tahu callee diperlukan untuk mengabaikan byte tinggi.
Atau jika bool dilewatkan sebagai argumen ke-3, mungkin penelepon yang mengoptimalkan ukuran kode memuatnya, mov dl, [mem]
bukannya movzx edx, [mem]
menghemat 1 byte dengan biaya ketergantungan salah pada nilai RDX yang lama (atau efek register parsial lainnya, tergantung pada model CPU). Atau untuk argumen pertama, mov dil, byte [r10]
alih-alih movzx edi, byte [r10]
, karena keduanya memerlukan awalan REX.
Inilah sebabnya mengapa memancarkan dentang movzx eax, dil
di Serialize
, bukan sub eax, edi
. (Untuk argumen integer, dentang melanggar aturan ABI ini, sebagai gantinya tergantung pada perilaku tidak berdokumen dari gcc dan dentang ke nol atau perpanjangan tanda integer sempit menjadi 32 bit. Merupakan tanda atau ekstensi nol yang diperlukan saat menambahkan offset 32bit ke pointer untuk ABI x86-64?
Jadi saya tertarik untuk melihat bahwa itu tidak melakukan hal yang sama untuk bool
.)
Catatan kaki 2: Setelah bercabang, Anda hanya akan memiliki 4-byte mov
-dimateate, atau 4-byte + 1-byte store. Panjangnya tersirat dalam lebar toko + offset.
OTOH, memcpy glibc akan melakukan dua beban / toko 4-byte dengan tumpang tindih yang tergantung pada panjang, jadi ini benar-benar membuat semuanya bebas dari cabang-cabang kondisional di boolean. Lihat L(between_4_7):
blok di memcpy / memmove glibc. Atau setidaknya, lakukan cara yang sama untuk boolean di percabangan memcpy untuk memilih ukuran chunk.
Jika inlining, Anda bisa menggunakan 2x mov
-immediate + cmov
dan offset bersyarat, atau Anda bisa meninggalkan data string dalam memori.
Atau jika mencari Intel Ice Lake ( dengan fitur Fast Short REP MOV ), yang sebenarnya rep movsb
mungkin optimal. glibc memcpy
mungkin mulai digunakan rep movsb
untuk ukuran kecil pada CPU dengan fitur itu, menghemat banyak percabangan.
Alat untuk mendeteksi UB dan penggunaan nilai yang tidak diinisialisasi
Di gcc dan dentang, Anda dapat mengkompilasi dengan -fsanitize=undefined
menambahkan run-time instrumentation yang akan memperingatkan atau kesalahan pada UB yang terjadi saat runtime. Itu tidak akan menangkap variabel unitial. (Karena itu tidak menambah ukuran tipe untuk memberi ruang bagi bit "tidak diinisialisasi").
Lihat https://developers.redhat.com/blog/2014/10/16/gcc-undefined-behavior-sanitizer-ubsan/
Untuk menemukan penggunaan data yang tidak diinisialisasi, ada Sanitizer Alamat dan Memory Sanitizer di dentang / LLVM. https://github.com/google/sanitizers/wiki/MemorySanitizer menunjukkan contoh-contoh clang -fsanitize=memory -fPIE -pie
pendeteksian memori yang tidak diinisialisasi. Ini mungkin bekerja paling baik jika Anda mengkompilasi tanpa optimasi, jadi semua membaca variabel akhirnya memuat dari memori dalam asm. Mereka menunjukkan itu digunakan di -O2
dalam kasus di mana beban tidak akan optimal. Saya belum mencobanya sendiri. (Dalam beberapa kasus, misalnya tidak menginisialisasi akumulator sebelum menjumlahkan array, dentang -O3 akan memancarkan kode yang menjumlahkan ke register vektor yang tidak pernah diinisialisasi. Jadi dengan optimisasi, Anda dapat memiliki kasus di mana tidak ada memori yang dibaca terkait dengan UB Tapi-fsanitize=memory
mengubah asm yang dihasilkan, dan mungkin menghasilkan cek untuk ini.)
Ini akan mentolerir penyalinan memori yang tidak diinisialisasi, dan juga logika sederhana dan operasi aritmatika dengannya. Secara umum, MemorySanitizer secara diam-diam melacak penyebaran data yang tidak diinisialisasi dalam memori, dan melaporkan peringatan ketika cabang kode diambil (atau tidak diambil) tergantung pada nilai yang tidak diinisialisasi.
MemorySanitizer mengimplementasikan subset fungsionalitas yang ditemukan di Valgrind (alat Memcheck).
Seharusnya berfungsi untuk kasus ini karena panggilan ke glibc memcpy
dengan length
dihitung dari memori yang tidak diinisialisasi akan (di dalam perpustakaan) menghasilkan cabang berdasarkan length
. Jika itu meringkas versi tanpa cabang yang hanya digunakan cmov
, mengindeks, dan dua toko, itu mungkin tidak akan berfungsi.
Valgrind'smemcheck
juga akan mencari masalah seperti ini, sekali lagi tidak mengeluh jika program hanya menyalin sekitar data yang tidak diinisialisasi. Tetapi ia mengatakan akan mendeteksi kapan "lompatan kondisional atau bergerak tergantung pada nilai yang tidak diinisialisasi", untuk mencoba menangkap perilaku yang terlihat secara eksternal yang tergantung pada data yang tidak diinisialisasi.
Mungkin ide di balik tidak menandai hanya sebuah beban adalah bahwa struct dapat memiliki padding, dan menyalin seluruh struct (termasuk padding) dengan beban vektor yang luas / toko bukan kesalahan bahkan jika anggota individu hanya ditulis satu per satu. Pada tingkat asm, informasi tentang apa yang padding dan apa yang sebenarnya merupakan bagian dari nilai telah hilang.