Apakah standar C ++ memungkinkan bool yang tidak diinisialisasi untuk crash suatu program?

500

Saya tahu bahwa "perilaku tidak terdefinisi" di C ++ dapat memungkinkan kompilator melakukan apa pun yang diinginkannya. Namun, saya mengalami crash yang mengejutkan saya, karena saya berasumsi bahwa kode itu cukup aman.

Dalam kasus ini, masalah sebenarnya hanya terjadi pada platform tertentu menggunakan kompiler tertentu, dan hanya jika optimasi diaktifkan.

Saya mencoba beberapa hal untuk mereproduksi masalah dan menyederhanakannya secara maksimal. Berikut adalah ekstrak fungsi yang disebut Serialize, yang akan mengambil parameter bool, dan menyalin string trueatau falseke buffer tujuan yang ada.

Apakah fungsi ini dalam ulasan kode, tidak akan ada cara untuk mengatakan bahwa itu, pada kenyataannya, bisa macet jika parameter bool adalah nilai yang tidak diinisialisasi?

// Zero-filled global buffer of 16 characters
char destBuffer[16];

void Serialize(bool boolValue) {
    // Determine which string to print based on boolValue
    const char* whichString = boolValue ? "true" : "false";

    // Compute the length of the string we selected
    const size_t len = strlen(whichString);

    // Copy string into destination buffer, which is zero-filled (thus already null-terminated)
    memcpy(destBuffer, whichString, len);
}

Jika kode ini dijalankan dengan optimasi 5.0.0 clang 5.0, maka akan / bisa macet.

Operator ternary yang diharapkan boolValue ? "true" : "false"tampak cukup aman bagi saya, saya berasumsi, "Apa pun nilai sampah boolValuetidak masalah, karena bagaimanapun akan menilai benar atau salah."

Saya telah menyiapkan contoh Compiler Explorer yang menunjukkan masalah dalam pembongkaran, di sini contoh lengkapnya. Catatan: untuk repro masalah ini, kombinasi yang saya temukan yang berhasil adalah dengan menggunakan Dentang 5.0.0 dengan optimasi -O2.

#include <iostream>
#include <cstring>

// Simple struct, with an empty constructor that doesn't initialize anything
struct FStruct {
    bool uninitializedBool;

   __attribute__ ((noinline))  // Note: the constructor must be declared noinline to trigger the problem
   FStruct() {};
};

char destBuffer[16];

// Small utility function that allocates and returns a string "true" or "false" depending on the value of the parameter
void Serialize(bool boolValue) {
    // Determine which string to print depending if 'boolValue' is evaluated as true or false
    const char* whichString = boolValue ? "true" : "false";

    // Compute the length of the string we selected
    size_t len = strlen(whichString);

    memcpy(destBuffer, whichString, len);
}

int main()
{
    // Locally construct an instance of our struct here on the stack. The bool member uninitializedBool is uninitialized.
    FStruct structInstance;

    // Output "true" or "false" to stdout
    Serialize(structInstance.uninitializedBool);
    return 0;
}

Masalah muncul karena pengoptimal: Itu cukup pintar untuk menyimpulkan bahwa string "benar" dan "salah" hanya berbeda panjangnya dengan 1. Jadi, alih-alih benar-benar menghitung panjangnya, ia menggunakan nilai bool itu sendiri, yang seharusnya secara teknis menjadi 0 atau 1, dan berjalan seperti ini:

const size_t len = strlen(whichString); // original code
const size_t len = 5 - boolValue;       // clang clever optimization

Walaupun ini "pintar", jadi untuk pertanyaan, pertanyaan saya adalah: Apakah standar C ++ memungkinkan kompiler untuk menganggap bool hanya dapat memiliki representasi numerik internal '0' atau '1' dan menggunakannya sedemikian rupa?

Atau apakah ini kasus implementasi yang didefinisikan, dalam hal mana implementasi tersebut mengasumsikan bahwa semua bools hanya akan mengandung 0 atau 1, dan nilai lainnya adalah wilayah perilaku yang tidak terdefinisi?

Remz
sumber
200
Itu pertanyaan yang bagus. Ini adalah ilustrasi yang kuat tentang bagaimana perilaku tidak terdefinisi bukan hanya masalah teoretis. Ketika orang mengatakan apa pun bisa terjadi sebagai hasil dari UB, "apa" itu bisa sangat mengejutkan. Orang mungkin berasumsi bahwa perilaku tidak terdefinisi masih bermanifestasi dengan cara yang dapat diprediksi, tetapi hari ini dengan pengoptimal modern itu sama sekali tidak benar. OP meluangkan waktu untuk membuat MCVE, menyelidiki masalah secara menyeluruh, memeriksa pembongkaran, dan mengajukan pertanyaan yang jelas dan langsung tentang hal itu. Tidak bisa meminta lebih.
John Kugelman
7
Perhatikan bahwa persyaratan yang "tidak nol dievaluasi untuk true" adalah aturan tentang operasi Boolean termasuk "penugasan ke bool" (yang mungkin secara implisit meminta a static_cast<bool>()tergantung pada spesifikasi). Namun itu bukan persyaratan tentang representasi internal yang booldipilih oleh kompiler.
Euro Micelli
2
Komentar bukan untuk diskusi panjang; percakapan ini telah dipindahkan ke obrolan .
Samuel Liew
3
Pada catatan yang sangat terkait, ini adalah sumber ketidakcocokan biner yang "menyenangkan". Jika Anda memiliki ABI A yang bernilai nol sebelum memanggil suatu fungsi, tetapi mengkompilasi fungsi sedemikian rupa sehingga mengasumsikan parameter berbantalan nol, dan ABI B adalah kebalikannya (bukan nol-pad, tetapi tidak menganggap nol parameter -padded), sebagian besar akan berfungsi, tetapi fungsi menggunakan B ABI akan menyebabkan masalah jika memanggil fungsi menggunakan ABI A yang mengambil parameter 'kecil'. IIRC Anda memiliki ini di x86 dengan dentang dan ICC.
TLW
1
@ TTW: Walaupun Standar tidak mengharuskan implementasi menyediakan sarana pemanggilan atau dipanggil dengan kode luar, akan sangat membantu untuk memiliki sarana untuk menentukan hal-hal seperti itu untuk implementasi di mana mereka relevan (implementasi di mana rincian seperti itu tidak yang relevan dapat mengabaikan atribut tersebut).
supercat

Jawaban:

285

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 boolsebagai fungsi arg dalam register diwakili oleh pola-bit false=0dantrue=1 dalam bit 8 yang rendah dari register 1 . Dalam memori, booladalah 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 !mybooldengan xor eax,1membalik bit rendah: Setiap kode yang mungkin dapat membalik sedikit / integer / bool antara 0 dan 1 dalam instruksi CPU tunggal . Atau mengkompilasi a&&bke bitwise DAN untuk booljenis. 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 memcpysebagai 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 maindengan push rax(yang lebih kecil dan karena berbagai alasan tentang seefisien sub rsp, 8), jadi apa pun sampah di AL pada entri mainadalah nilai yang digunakan untuk itu uninitializedBool. Inilah sebabnya mengapa Anda benar-benar mendapatkan nilai yang tidak adil 0.

5U - random garbagedapat 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=0dan 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 memcpymemasukkan boolke dalam unsigned char, yang diizinkan untuk Anda lakukan karena char*bisa alias apa saja. Dan unsigned chardijamin 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 inlinekata kunci.)

Jadi penyusun dapat memancarkan hanya retatau ud2(instruksi ilegal) sebagai definisi untuk main, karena jalur eksekusi dimulai dari atas yang maintak 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 ud2di UB, bukannya mencoba menghasilkan kode untuk jalur eksekusi yang tidak masuk akal. Atau untuk kasus-kasus seperti jatuh dari ujung non- voidfungsi, gcc terkadang akan menghilangkan retinstruksi. 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 -Walldan 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_MINtidak mengoptimalkan a<0sebagai selalu-salah, hanya saja tmpselalu negatif. (Karena INT_MIN+ a=INT_MAXnegatif pada target komplemen 2 ini, dan atidak 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 & 0x01010101dalam RDI dan menggunakannya untuk hal lain, sebelum menelepon bool_func(a&1). Penelepon dapat mengoptimalkan jauh &1karena 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, dildi 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 + cmovdan 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 movsbmungkin optimal. glibc memcpymungkin 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=undefinedmenambahkan 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 -piependeteksian 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 -O2dalam 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 memcpydengan lengthdihitung 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.

Peter Cordes
sumber
2
Saya telah melihat kasus yang lebih buruk di mana variabel mengambil nilai tidak dalam kisaran integer 8 bit, tetapi hanya dari seluruh register CPU. Dan Itanium memiliki yang lebih buruk lagi, penggunaan variabel yang tidak diinisialisasi dapat langsung crash.
Joshua
2
@ Yosua: oh benar, bagus, spekulasi eksplisit Itanium akan menandai nilai register dengan "tidak angka", sehingga menggunakan kesalahan nilai.
Peter Cordes
11
Selain itu, ini juga menggambarkan mengapa para featurebug UB diperkenalkan dalam desain bahasa C dan C ++ di tempat pertama: karena memberikan compiler persis jenis kebebasan, yang kini telah diizinkan paling kompiler modern untuk melakukan berkualitas tinggi ini optimisasi yang menjadikan C / C ++ bahasa tingkat menengah berkinerja tinggi.
The_Sympathizer
2
Maka perang antara penulis kompiler C ++ dan programmer C ++ mencoba menulis program yang bermanfaat berlanjut. Jawaban ini, benar-benar komprehensif dalam menjawab pertanyaan ini, juga dapat digunakan sebagai salinan iklan yang meyakinkan untuk vendor alat analisis statis ...
davidbak
4
@The_Sympathizer: UB disertakan untuk memungkinkan implementasi berperilaku dengan cara apa pun yang paling bermanfaat bagi pelanggan mereka . Itu tidak dimaksudkan untuk menyarankan bahwa semua perilaku harus dianggap sama bermanfaatnya.
supercat
56

Compiler diperbolehkan untuk berasumsi bahwa nilai boolean yang diteruskan sebagai argumen adalah nilai boolean yang valid (yaitu yang telah diinisialisasi atau dikonversi ke trueatau false). The truenilai tidak harus sama dengan bilangan bulat 1 - memang, bisa ada berbagai representasi dari truedan false- tetapi parameter harus beberapa representasi valid dari salah satu dari dua nilai, di mana "representasi yang sah" adalah implementation- didefinisikan.

Jadi jika Anda gagal menginisialisasi a bool, atau jika Anda berhasil menimpanya melalui beberapa pointer dari tipe yang berbeda, maka asumsi kompiler akan salah dan Perilaku Tidak Terdefinisi akan terjadi. Anda telah diperingatkan:

50) Menggunakan nilai bool dengan cara yang dijelaskan oleh Standar Internasional ini sebagai "tidak terdefinisi", seperti dengan memeriksa nilai objek otomatis yang tidak diinisialisasi, dapat menyebabkannya berperilaku seolah-olah itu tidak benar atau salah. (Catatan kaki untuk paragraf 6 dari §6.9.1, Jenis-Jenis Mendasar)

rici
sumber
11
" trueNilai tidak harus sama dengan integer 1" agak menyesatkan. Tentu, pola bit yang sebenarnya bisa menjadi sesuatu yang lain, tetapi ketika secara implisit dikonversi / dipromosikan (satu-satunya cara Anda akan melihat nilai selain true/ false), trueselalu 1, dan falseselalu0 . Tentu saja, kompiler seperti itu juga tidak akan dapat menggunakan trik yang coba digunakan oleh kompiler ini (menggunakan fakta bahwa boolpola bit aktual hanya bisa 0atau 1), jadi agak tidak relevan dengan masalah OP.
ShadowRanger
4
@ShadowRanger Anda selalu dapat memeriksa representasi objek secara langsung.
TC
7
@shadowranger: maksud saya adalah implementasinya bertanggung jawab. Jika itu membatasi representasi valid trueke pola bit 1, itu hak prerogatifnya. Jika ia memilih beberapa set representasi lain, maka memang tidak bisa menggunakan optimasi yang disebutkan di sini. Jika ia memilih representasi tertentu, maka ia bisa. Itu hanya perlu konsisten secara internal. Anda dapat memeriksa representasi dari booldengan menyalinnya ke dalam array byte; itu bukan UB (tapi itu adalah implementasi yang ditentukan)
rici
3
Ya, mengoptimalkan kompiler (mis. Implementasi C ++ dunia nyata) terkadang akan memancarkan kode yang tergantung pada boolpola bit yang dimiliki 0atau 1. Mereka tidak booleanize a boolsetiap kali mereka membacanya dari memori (atau register yang memiliki fungsi arg). Itulah yang dikatakan jawaban ini. contoh : gcc4.7 + dapat mengoptimalkan return a||buntuk or eax, edidalam suatu fungsi kembali bool, atau MSVC dapat mengoptimalkan a&buntuk test cl, dl. x86's testadalah bitwise and , jadi jika cl=1dan dl=2uji set flag menurut cl&dl = 0.
Peter Cordes
5
Poin tentang perilaku tidak terdefinisi adalah bahwa kompiler diperbolehkan untuk menarik kesimpulan yang jauh lebih banyak tentang hal itu, misalnya untuk menganggap bahwa jalur kode yang akan mengarah pada mengakses nilai yang tidak diinisialisasi tidak pernah diambil sama sekali, karena memastikan bahwa justru merupakan tanggung jawab programmer . Jadi ini bukan hanya tentang kemungkinan bahwa nilai level rendah bisa berbeda dari nol atau satu.
Holger
52

Fungsi itu sendiri sudah benar, tetapi dalam program pengujian Anda, pernyataan yang memanggil fungsi menyebabkan perilaku tidak terdefinisi dengan menggunakan nilai variabel yang tidak diinisialisasi.

Bug ada di fungsi panggilan, dan itu bisa dideteksi oleh tinjauan kode atau analisis statis dari fungsi panggilan. Menggunakan tautan penjelajah kompiler Anda, kompiler gcc 8.2 mendeteksi bug. (Mungkin Anda bisa mengajukan laporan bug terhadap dentang yang tidak menemukan masalah).

Perilaku tidak terdefinisi berarti apa pun dapat terjadi, termasuk program menabrak beberapa baris setelah peristiwa yang memicu perilaku tidak terdefinisi.

NB. Jawaban untuk "Bisakah perilaku tidak terdefinisi menyebabkan _____?" selalu "Ya". Itulah definisi perilaku yang tidak terdefinisi.

MM
sumber
2
Apakah klausa pertama benar? Apakah hanya menyalinbool pemicu yang tidak diinisialisasi ke UB?
Joshua Green
10
@JoshuaGreen, lihat [dcl.init] / 12 "Jika nilai yang tidak ditentukan dihasilkan oleh evaluasi, perilaku tersebut tidak ditentukan kecuali dalam kasus berikut:" (dan tidak ada kasus yang memiliki pengecualian untuk bool). Menyalin memerlukan evaluasi sumber
MM
8
@ JoshuaGreen Dan alasannya adalah Anda mungkin memiliki platform yang memicu kesalahan perangkat keras jika Anda mengakses beberapa nilai yang tidak valid untuk beberapa jenis. Ini kadang-kadang disebut "representasi perangkap".
David Schwartz
7
Itanium, meskipun tidak jelas, adalah CPU yang masih dalam produksi, memiliki nilai jebakan, dan memiliki dua setidaknya kompiler C ++ semi-modern (Intel / HP). Secara harfiah memiliki true, falsedan not-a-thingnilai untuk boolean.
MSalters
3
Di sisi lain, jawaban untuk "Apakah standar mengharuskan semua penyusun untuk memproses sesuatu dengan cara tertentu" umumnya "tidak", bahkan / terutama dalam kasus di mana jelas bahwa setiap penyusun kualitas harus melakukannya; semakin jelas sesuatu itu, semakin sedikit yang harus ada bagi penulis Standar untuk benar-benar mengatakannya.
supercat
23

Sebuah bool hanya diperbolehkan untuk memegang nilai-nilai dependen implementasi yang digunakan secara internal untuk truedan false, dan kode yang dihasilkan dapat mengasumsikan bahwa ia hanya akan menampung satu dari dua nilai ini.

Biasanya, implementasi akan menggunakan integer 0untuk falsedan 1untuk true, untuk menyederhanakan konversi antara booldan int, dan membuat if (boolvar)menghasilkan kode yang sama dengan if (intvar). Dalam hal itu, orang dapat membayangkan bahwa kode yang dihasilkan untuk ternary dalam penugasan akan menggunakan nilai sebagai indeks menjadi array pointer ke dua string, yaitu mungkin dikonversi menjadi sesuatu seperti:

// the compile could make asm that "looks" like this, from your source
const static char *strings[] = {"false", "true"};
const char *whichString = strings[boolValue];

Jika boolValuetidak diinisialisasi, sebenarnya bisa menyimpan nilai integer apa pun, yang kemudian akan menyebabkan akses di luar batas stringsarray.

Barmar
sumber
1
@SidS Terima kasih. Secara teoritis, representasi internal dapat menjadi kebalikan dari bagaimana mereka dilemparkan ke / dari bilangan bulat, tetapi itu akan bertentangan.
Barmar
1
Anda benar, dan contoh Anda juga akan macet. Namun itu "terlihat" untuk tinjauan kode bahwa Anda menggunakan variabel yang tidak diinisialisasi sebagai indeks ke array. Juga, itu akan crash bahkan dalam debug (misalnya beberapa debugger / compiler akan menginisialisasi dengan pola tertentu untuk membuatnya lebih mudah untuk melihat ketika crash). Dalam contoh saya, bagian yang mengejutkan adalah bahwa penggunaan bool tidak terlihat: Pengoptimal memutuskan untuk menggunakannya dalam perhitungan yang tidak ada dalam kode sumber.
Remz
3
@Remz Saya hanya menggunakan array untuk menunjukkan kode apa yang dihasilkan bisa setara dengan, tidak menyarankan siapa pun yang benar-benar akan menulis itu.
Barmar
1
@Remz Menyusun ulang booluntuk intdengan *(int *)&boolValuedan mencetaknya untuk tujuan debugging, lihat apakah itu selain 0atau 1ketika crash. Jika itu masalahnya, itu cukup banyak mengkonfirmasi teori bahwa kompiler mengoptimalkan inline-jika sebagai array yang menjelaskan mengapa ia crash.
Havenard
2
@MSalters: std::bitset<8>tidak memberi saya nama yang bagus untuk semua flag saya yang berbeda. Tergantung pada apa mereka, itu mungkin penting.
Martin Bonner mendukung Monica
15

Merangkum pertanyaan Anda banyak, Anda bertanya Apakah standar C ++ memungkinkan kompiler untuk menganggap a boolhanya dapat memiliki representasi numerik internal '0' atau '1' dan menggunakannya sedemikian rupa?

Standar tidak mengatakan apa-apa tentang representasi internal a bool. Itu hanya mendefinisikan apa yang terjadi ketika casting boolke int(atau sebaliknya). Sebagian besar, karena konversi integral ini (dan fakta bahwa orang-orang sangat bergantung pada mereka), kompiler akan menggunakan 0 dan 1, tetapi tidak harus (meskipun harus menghormati kendala dari setiap ABI tingkat bawah yang digunakannya. ).

Jadi, kompiler, ketika melihat a boolberhak untuk mempertimbangkan bahwa kata tersebut boolmengandung salah satu dari pola bit ' true' atau ' false' dan melakukan apa pun rasanya. Jadi jika nilai-nilai untuk truedan falseyang 1 dan 0, masing-masing, compiler memang diperbolehkan untuk mengoptimalkan strlenuntuk 5 - <boolean value>. Perilaku menyenangkan lainnya dimungkinkan!

Seperti yang berulang kali dinyatakan di sini, perilaku tidak terdefinisi memiliki hasil yang tidak ditentukan. Termasuk tetapi tidak terbatas pada

  • Kode Anda berfungsi seperti yang Anda harapkan
  • Kode Anda gagal secara acak
  • Kode Anda tidak dijalankan sama sekali.

Lihat Apa yang harus diketahui setiap programmer tentang perilaku tidak terdefinisi

Tom Tanner
sumber