Ini adalah contoh untuk menggambarkan pertanyaan saya yang melibatkan beberapa kode yang jauh lebih rumit yang tidak dapat saya posting di sini.
#include <stdio.h>
int main()
{
int a = 0;
for (int i = 0; i < 3; i++)
{
printf("Hello\n");
a = a + 1000000000;
}
}
Program ini berisi perilaku tidak terdefinisi di platform saya karena a
akan meluap pada loop ke-3.
Apakah itu membuat seluruh program memiliki perilaku tidak terdefinisi, atau hanya setelah luapan benar-benar terjadi ? Mungkinkah kompilator bekerja keluar yang a
akan meluap sehingga dapat mendeklarasikan seluruh loop tidak terdefinisi dan tidak repot-repot menjalankan printfs meskipun semuanya terjadi sebelum overflow?
(Diberi tag C dan C ++ meskipun berbeda karena saya akan tertarik dengan jawaban untuk kedua bahasa jika keduanya berbeda.)
c++
c
undefined-behavior
integer-overflow
jcoder.dll
sumber
sumber
a
tidak digunakan (kecuali untuk menghitung sendiri) dan cukup hapusa
Jawaban:
Jika Anda tertarik dengan jawaban teoretis murni, standar C ++ memungkinkan perilaku tidak terdefinisi menjadi "perjalanan waktu":
Dengan demikian, jika program Anda berisi perilaku tidak terdefinisi, maka perilaku seluruh program Anda tidak terdefinisi.
sumber
sneeze()
fungsinya sendiri tidak ditentukan pada apa pun dari kelasDemon
(yang nasal variasinya adalah subkelasnya), membuat semuanya tetap melingkar.printf
tidak kembali, tetapi jikaprintf
akan kembali, maka perilaku yang tidak ditentukan dapat menyebabkan masalah sebelumprintf
dipanggil. Karenanya, perjalanan waktu.printf("Hello\n");
dan kemudian baris berikutnya dikompilasi sebagaiundoPrintf(); launchNuclearMissiles();
Pertama, izinkan saya mengoreksi judul pertanyaan ini:
Perilaku Tidak Terdefinisi bukan (secara khusus) dari ranah eksekusi.
Perilaku Tidak Terdefinisi memengaruhi semua langkah: kompilasi, penautan, pemuatan, dan eksekusi.
Beberapa contoh untuk memperkuat ini, perlu diingat bahwa tidak ada bagian yang lengkap:
LD_PRELOAD
trik di UnixInilah yang sangat menakutkan tentang Perilaku Tidak Terdefinisi: hampir tidak mungkin untuk memprediksi, sebelumnya, perilaku persis apa yang akan terjadi, dan prediksi ini harus ditinjau kembali di setiap pembaruan rantai alat, OS yang mendasarinya, ...
Saya sarankan menonton video ini oleh Michael Spencer (Pengembang LLVM): CppCon 2016: My Little Optimizer: Undefined Behavior is Magic .
sumber
argc
sebagai jumlah pengulangan, kasusargc=1
ini tidak menghasilkan UB dan kompilator akan dipaksa untuk menanganinya.i
tidak dapat bertambah lebih dariN
kali dan oleh karena itu nilainya dibatasi.f(good);
melakukan sesuatu X danf(bad);
memunculkan perilaku tidak terdefinisi, maka program yang baru saja dipanggilf(good);
dijamin untuk melakukan X, tetapif(good); f(bad);
tidak dijamin untuk melakukan X.if(foo) f(good); else f(bad);
, kompiler cerdas akan membuang perbandingan dan menghasilkan dan tanpa syaratfoo(good)
.Compiler C atau C ++ yang mengoptimalkan secara agresif yang menargetkan bit 16
int
akan mengetahui bahwa perilaku penambahan1000000000
ke suatuint
jenis tidak ditentukan .Hal ini diizinkan oleh salah satu standar untuk melakukan apa pun yang diinginkannya yang dapat mencakup penghapusan seluruh program, keluar
int main(){}
.Tapi bagaimana dengan
int
s yang lebih besar ? Saya belum tahu kompiler yang melakukan ini (dan saya bukan ahli dalam desain kompilator C dan C ++ dengan cara apa pun), tetapi saya membayangkan bahwa kadang - kadang kompiler yang menargetkan 32 bitint
atau lebih tinggi akan mengetahui bahwa loop adalah tak terbatas (i
tidak berubah) dan sehinggaa
akhirnya akan meluap. Jadi sekali lagi, itu bisa mengoptimalkan output keint main(){}
. Poin yang ingin saya sampaikan di sini adalah bahwa saat pengoptimalan compiler menjadi semakin agresif, konstruksi perilaku yang semakin tidak terdefinisi memanifestasikan dirinya dengan cara yang tidak terduga.Fakta bahwa perulangan Anda tidak terbatas tidak dengan sendirinya tidak terdefinisi karena Anda menulis ke keluaran standar di badan perulangan.
sumber
int
16-bit, penambahan akan dilakukan dilong
(karena operand literal memiliki tipelong
) di mana itu didefinisikan dengan baik, kemudian dikonversi oleh konversi yang ditentukan implementasi kembali keint
.printf
didefinisikan oleh standar untuk selalu kembaliSecara teknis, di bawah standar C ++, jika sebuah program berisi perilaku tidak terdefinisi, perilaku seluruh program, bahkan pada waktu kompilasi (bahkan sebelum program dijalankan), tidak ditentukan.
Dalam praktiknya, karena kompilator mungkin menganggap (sebagai bagian dari pengoptimalan) bahwa luapan tidak akan terjadi, setidaknya perilaku program pada iterasi ketiga dari perulangan (dengan asumsi mesin 32-bit) tidak akan ditentukan, meskipun itu kemungkinan Anda akan mendapatkan hasil yang benar sebelum iterasi ketiga. Namun, karena perilaku seluruh program secara teknis tidak ditentukan, tidak ada yang menghentikan program untuk menghasilkan keluaran yang benar-benar salah (termasuk tidak ada keluaran), mogok saat runtime pada titik mana pun selama eksekusi, atau bahkan gagal untuk mengompilasi sama sekali (karena perilaku tidak terdefinisi meluas ke waktu kompilasi).
Perilaku tidak terdefinisi memberi kompiler lebih banyak ruang untuk dioptimalkan karena mereka menghilangkan asumsi tertentu tentang apa yang harus dilakukan kode. Dengan demikian, program yang mengandalkan asumsi yang melibatkan perilaku tidak terdefinisi tidak dijamin akan berfungsi seperti yang diharapkan. Karena itu, Anda tidak boleh mengandalkan perilaku tertentu yang dianggap tidak ditentukan menurut standar C ++.
sumber
if(false) {}
ruang lingkup? Apakah itu meracuni seluruh program, karena compiler mengasumsikan semua cabang berisi ~ bagian logika yang terdefinisi dengan baik, dan dengan demikian beroperasi pada asumsi yang salah?Untuk memahami mengapa perilaku tidak terdefinisi dapat 'menjelajah waktu' seperti yang dikatakan @TartanLlama secara memadai , mari kita lihat aturan 'seolah-olah':
Dengan ini, kita bisa melihat program sebagai 'kotak hitam' dengan masukan dan keluaran. Input bisa berupa input pengguna, file, dan banyak hal lainnya. Outputnya adalah 'perilaku yang dapat diamati' yang disebutkan dalam standar.
Standar hanya mendefinisikan pemetaan antara input dan output, tidak ada yang lain. Ini dilakukan dengan mendeskripsikan 'contoh kotak hitam', tetapi secara eksplisit mengatakan kotak hitam lain dengan pemetaan yang sama juga valid. Artinya, isi kotak hitam tidak relevan.
Dengan pemikiran ini, tidak masuk akal untuk mengatakan bahwa perilaku tidak terdefinisi terjadi pada saat tertentu. Dalam contoh implementasi kotak hitam, kita dapat mengatakan di mana dan kapan itu terjadi, tetapi kotak hitam yang sebenarnya bisa menjadi sesuatu yang sangat berbeda, jadi kita tidak dapat mengatakan di mana dan kapan itu terjadi lagi. Secara teoritis, kompilator dapat misalnya memutuskan untuk menghitung semua masukan yang mungkin, dan menghitung sebelumnya keluaran yang dihasilkan. Kemudian perilaku tidak terdefinisi akan terjadi selama kompilasi.
Perilaku tidak terdefinisi adalah tidak adanya pemetaan antara input dan output. Suatu program dapat memiliki perilaku tidak terdefinisi untuk beberapa masukan, tetapi perilaku terdefinisi untuk masukan lainnya. Maka pemetaan antara input dan output tidak lengkap; ada masukan yang tidak ada pemetaan ke keluarannya.
Program dalam pertanyaan memiliki perilaku tak terdefinisi untuk masukan apa pun, sehingga pemetaannya kosong.
sumber
Dengan asumsi
int
32-bit, perilaku tidak terdefinisi terjadi pada iterasi ketiga. Jadi, jika, misalnya, loop hanya dapat dijangkau secara kondisional, atau dapat dihentikan secara kondisional sebelum iterasi ketiga, tidak akan ada perilaku yang tidak ditentukan kecuali iterasi ketiga benar-benar tercapai. Namun, jika terjadi perilaku tidak terdefinisi, semua keluaran program tidak terdefinisi, termasuk keluaran yang "di masa lalu" relatif terhadap pemanggilan perilaku tak terdefinisi. Misalnya, dalam kasus Anda, ini berarti tidak ada jaminan untuk melihat 3 pesan "Halo" di keluaran.sumber
Jawaban TartanLlama benar. Perilaku tidak terdefinisi dapat terjadi kapan saja, bahkan selama waktu kompilasi. Ini mungkin tampak tidak masuk akal, tetapi ini adalah fitur utama yang memungkinkan kompiler melakukan apa yang perlu mereka lakukan. Tidak selalu mudah untuk menjadi kompiler. Anda harus melakukan apa yang dikatakan spesifikasi, setiap saat. Namun, terkadang sangat sulit untuk membuktikan bahwa perilaku tertentu sedang terjadi. Jika Anda ingat masalah terputus-putus, agak sepele untuk mengembangkan perangkat lunak yang Anda tidak dapat membuktikan apakah itu menyelesaikan atau memasuki loop tak terbatas ketika diberi masukan tertentu.
Kami dapat membuat penyusun menjadi pesimis, dan terus-menerus menyusun dalam ketakutan bahwa instruksi berikutnya mungkin menjadi salah satu dari masalah yang tersendat-sendat seperti masalah, tetapi itu tidak masuk akal. Sebagai gantinya kami memberikan kompilator sebuah pass: pada topik "perilaku tidak terdefinisi" ini, mereka dibebaskan dari tanggung jawab apa pun. Perilaku tidak terdefinisi terdiri dari semua perilaku yang sangat jahat sehingga kami kesulitan memisahkannya dari masalah berhenti yang benar-benar jahat dan yang lainnya.
Ada contoh yang suka saya posting, meskipun saya akui saya kehilangan sumbernya, jadi saya harus memparafrasekannya. Itu dari versi MySQL tertentu. Di MySQL, mereka memiliki buffer melingkar yang diisi dengan data yang disediakan pengguna. Mereka, tentu saja, ingin memastikan bahwa datanya tidak melebihi buffer, jadi mereka melakukan pemeriksaan:
if (currentPtr + numberOfNewChars > endOfBufferPtr) { doOverflowLogic(); }
Ini terlihat cukup waras. Namun, bagaimana jika numberOfNewChars benar-benar besar, dan melimpah? Kemudian itu membungkus dan menjadi penunjuk yang lebih kecil dari
endOfBufferPtr
, sehingga logika luapan tidak akan pernah dipanggil. Jadi mereka menambahkan cek kedua, sebelum yang itu:if (currentPtr + numberOfNewChars < currentPtr) { detectWrapAround(); }
Sepertinya Anda menangani kesalahan buffer overflow, bukan? Namun, bug dikirimkan yang menyatakan bahwa buffer ini meluap pada versi tertentu dari Debian! Penyelidikan yang cermat menunjukkan bahwa versi Debian ini adalah yang pertama menggunakan versi gcc yang paling mutakhir. Pada versi gcc ini, compiler mengenali bahwa currentPtr + numberOfNewChars tidak bisa menjadi pointer yang lebih kecil dari currentPtr karena overflow untuk pointer adalah perilaku yang tidak terdefinisi! Itu sudah cukup bagi gcc untuk mengoptimalkan seluruh pemeriksaan, dan tiba-tiba Anda tidak terlindungi dari buffer overflows meskipun Anda menulis kode untuk memeriksanya!
Ini adalah perilaku spesifikasi. Semuanya legal (meskipun dari apa yang saya dengar, gcc mengembalikan perubahan ini di versi berikutnya). Ini bukan apa yang saya anggap sebagai perilaku intuitif, tetapi jika Anda sedikit melebarkan imajinasi Anda, mudah untuk melihat bagaimana sedikit variasi dari situasi ini dapat menjadi masalah yang tersendat-sendat bagi penyusun. Karena itu, penulis spesifikasi menjadikannya "Perilaku Tidak Terdefinisi" dan menyatakan bahwa kompilator dapat melakukan apa saja sesuka hatinya.
sumber
if(numberOfNewChars > endOfBufferPtr - currentPtr)
, asalkan numberOfNewChars tidak boleh negatif dan currentPtr selalu menunjuk ke suatu tempat di dalam buffer, Anda bahkan tidak memerlukan pemeriksaan "sampul" yang konyol. (Saya tidak berpikir kode yang Anda berikan memiliki harapan untuk bekerja dalam buffer melingkar - Anda telah meninggalkan apa pun yang diperlukan untuk itu dalam parafrase, jadi saya mengabaikan kasus itu juga)Di luar jawaban teoritis, pengamatan praktisnya adalah bahwa untuk waktu yang lama, penyusun telah menerapkan berbagai transformasi pada loop untuk mengurangi jumlah pekerjaan yang dilakukan di dalamnya. Misalnya, diberikan:
for (int i=0; i<n; i++) foo[i] = i*scale;
kompiler mungkin mengubahnya menjadi:
int temp = 0; for (int i=0; i<n; i++) { foo[i] = temp; temp+=scale; }
Dengan demikian menghemat perkalian dengan setiap iterasi loop. Bentuk pengoptimalan tambahan, yang disusun oleh penyusun dengan berbagai tingkat agresivitas, akan mengubahnya menjadi:
if (n > 0) { int temp1 = n*scale; int *temp2 = foo; do { temp1 -= scale; *temp2++ = temp1; } while(temp1); }
Bahkan pada mesin dengan wraparound silent on overflow, hal itu dapat gagal berfungsi jika ada beberapa angka yang kurang dari n yang, jika dikalikan dengan skala, akan menghasilkan 0. Itu juga bisa berubah menjadi loop tanpa akhir jika skala dibaca dari memori lebih dari sekali dan sesuatu mengubah nilainya secara tidak terduga (dalam kasus apa pun di mana "skala" dapat mengubah mid-loop tanpa memanggil UB, kompiler tidak akan diizinkan untuk melakukan pengoptimalan).
Meskipun sebagian besar pengoptimalan seperti itu tidak akan mengalami masalah dalam kasus di mana dua jenis unsigned pendek dikalikan untuk menghasilkan nilai antara INT_MAX + 1 dan UINT_MAX, gcc memiliki beberapa kasus di mana perkalian dalam satu loop dapat menyebabkan loop keluar lebih awal . Saya belum memperhatikan perilaku seperti itu yang berasal dari instruksi perbandingan dalam kode yang dihasilkan, tetapi dapat diamati dalam kasus di mana kompiler menggunakan overflow untuk menyimpulkan bahwa sebuah loop dapat dieksekusi paling banyak 4 kali atau kurang; itu tidak secara default menghasilkan peringatan dalam kasus di mana beberapa input akan menyebabkan UB dan yang lainnya tidak, bahkan jika kesimpulannya menyebabkan batas atas dari loop diabaikan.
sumber
Perilaku tidak terdefinisi, menurut definisi, adalah area abu-abu. Anda tidak bisa memprediksi apa yang akan atau tidak akan melakukan - itulah yang "perilaku undefined" berarti .
Sejak jaman dahulu, programmer selalu berusaha menyelamatkan sisa-sisa definisi dari situasi yang tidak ditentukan. Mereka memiliki beberapa kode yang benar-benar ingin mereka gunakan, tetapi ternyata tidak ditentukan, jadi mereka mencoba untuk membantah: "Saya tahu ini tidak ditentukan, tapi pasti, paling buruk, melakukan ini atau ini; tidak akan pernah melakukan itu . " Dan terkadang argumen ini kurang lebih benar - tetapi seringkali, argumen itu salah. Dan saat penyusun menjadi lebih pintar dan lebih pintar (atau, beberapa orang mungkin berkata, lebih licik dan lebih licik), batasan pertanyaan terus berubah.
Jadi sungguh, jika Anda ingin menulis kode yang dijamin berfungsi, dan itu akan terus berfungsi untuk waktu yang lama, hanya ada satu pilihan: hindari perilaku tidak terdefinisi dengan cara apa pun. Sungguh, jika Anda mencoba-coba, itu akan kembali menghantui Anda.
sumber
Satu hal yang tidak dipertimbangkan oleh contoh Anda adalah pengoptimalan.
a
disetel dalam perulangan tetapi tidak pernah digunakan, dan pengoptimal dapat menyelesaikannya. Dengan demikian, sah bagi pengoptimal untuk membuanga
sepenuhnya, dan dalam hal ini semua perilaku yang tidak ditentukan lenyap seperti korban boojum.Namun tentu saja ini sendiri tidak ditentukan, karena pengoptimalan tidak ditentukan. :)
sumber
Karena pertanyaan ini memiliki dua tag C dan C ++ saya akan mencoba dan membahas keduanya. C dan C ++ menggunakan pendekatan berbeda di sini.
Dalam C implementasi harus dapat membuktikan bahwa perilaku tidak terdefinisi akan dipanggil untuk memperlakukan seluruh program seolah-olah memiliki perilaku tak terdefinisi. Dalam contoh OP, akan tampak sepele bagi kompiler untuk membuktikannya dan oleh karena itu seolah-olah seluruh program tidak terdefinisi.
Kita dapat melihat ini dari Defect Report 109 yang pada intinya bertanya:
dan tanggapannya adalah:
Dalam C ++ pendekatannya tampak lebih santai dan akan menyarankan program memiliki perilaku yang tidak terdefinisi terlepas dari apakah penerapannya dapat membuktikannya secara statis atau tidak.
Kami memiliki [intro.abstrac] p5 yang mengatakan:
sumber
Jawaban teratas adalah kesalahpahaman yang salah (tapi umum):
Perilaku tak terdefinisi adalah properti run-time *. Ini TIDAK BISA "perjalanan waktu"!
Operasi tertentu ditentukan (oleh standar) memiliki efek samping dan tidak dapat dioptimalkan. Operasi yang melakukan I / O atau yang mengakses
volatile
variabel termasuk dalam kategori ini.Namun , ada peringatan: UB bisa saja berperilaku, termasuk perilaku yang membatalkan operasi sebelumnya. Ini dapat memiliki konsekuensi yang serupa, dalam beberapa kasus, untuk mengoptimalkan kode sebelumnya.
Faktanya, ini konsisten dengan kutipan di jawaban teratas (penekanan saya):
Ya, kutipan ini mengatakan "bahkan tidak berkaitan dengan operasi sebelum operasi pertama yang tidak ditentukan" , tetapi perhatikan bahwa ini secara khusus tentang kode yang sedang dijalankan , tidak hanya dikompilasi.
Bagaimanapun, perilaku tidak terdefinisi yang sebenarnya tidak tercapai tidak melakukan apa-apa, dan agar baris yang berisi UB benar-benar tercapai, kode yang mendahuluinya harus dieksekusi terlebih dahulu!
Jadi ya, sekali UB dijalankan , semua efek dari operasi sebelumnya menjadi tidak terdefinisi. Tapi sampai itu terjadi, eksekusi program sudah terdefinisi dengan baik.
Namun, perhatikan bahwa semua eksekusi program yang mengakibatkan terjadinya hal ini dapat dioptimalkan untuk program yang setara , termasuk program apa pun yang menjalankan operasi sebelumnya tetapi kemudian membatalkan efeknya. Akibatnya, kode sebelumnya dapat dioptimalkan kapan pun melakukannya akan setara dengan efeknya yang dibatalkan ; jika tidak, tidak bisa. Lihat contoh di bawah.
* Catatan: Ini tidak bertentangan dengan UB yang terjadi pada waktu kompilasi . Jika kompiler memang dapat membuktikan bahwa kode UB akan selalu dieksekusi untuk semua input, maka UB dapat memperpanjang waktu kompilasi. Namun, ini membutuhkan pengetahuan bahwa semua kode sebelumnya pada akhirnya kembali , yang merupakan persyaratan yang kuat. Sekali lagi, lihat di bawah untuk contoh / penjelasan.
Untuk membuat ini konkret, perhatikan bahwa kode berikut harus mencetak
foo
dan menunggu masukan Anda terlepas dari perilaku tidak terdefinisi yang mengikutinya:printf("foo"); getchar(); *(char*)1 = 1;
Namun, perhatikan juga bahwa tidak ada jaminan yang
foo
akan tetap ada di layar setelah UB terjadi, atau karakter yang Anda ketikkan tidak lagi berada di buffer input; kedua operasi ini dapat "diurungkan", yang memiliki efek serupa dengan "perjalanan waktu" UB.Jika
getchar()
garis itu tidak ada, itu akan menjadi hukum bagi garis yang akan dioptimalkan pergi jika dan hanya jika yang akan dibedakan dari keluaranfoo
dan kemudian "un-melakukan" itu.Apakah keduanya tidak bisa dibedakan atau tidak akan bergantung sepenuhnya pada implementasinya (yaitu pada compiler dan library standar Anda). Misalnya, dapatkah Anda
printf
memblokir utas Anda di sini sambil menunggu program lain membaca hasilnya? Atau akankah segera kembali?Jika dapat memblokir di sini, maka program lain dapat menolak untuk membaca keluaran penuhnya, dan mungkin tidak pernah kembali, dan akibatnya UB mungkin tidak pernah benar-benar terjadi.
Jika dapat segera kembali ke sini, maka kita tahu ia harus kembali, dan oleh karena itu mengoptimalkannya sama sekali tidak dapat dibedakan dari menjalankannya dan kemudian menghentikan pengaruhnya.
Tentu saja, karena compiler mengetahui perilaku apa yang diperbolehkan untuk versi tertentu
printf
, ia dapat mengoptimalkannya, dan akibatnyaprintf
dapat dioptimalkan dalam beberapa kasus dan tidak pada yang lain. Tapi, sekali lagi, pembenarannya adalah bahwa ini tidak bisa dibedakan dengan UB yang tidak melakukan operasi sebelumnya, bukan kode sebelumnya yang "diracuni" karena UB.sumber