Pada titik mana di loop apakah integer overflow menjadi perilaku tidak terdefinisi?

86

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 aakan 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.)

jcoder.dll
sumber
7
Bertanya-tanya apakah kompiler dapat bekerja yang atidak digunakan (kecuali untuk menghitung sendiri) dan cukup hapusa
4386427
12
Anda mungkin menikmati My Little Optimizer: Undefined Behavior is Magic dari CppCon tahun ini. Ini semua tentang apa yang dapat dilakukan kompiler pengoptimalan berdasarkan perilaku yang tidak ditentukan.
TartanLlama

Jawaban:

108

Jika Anda tertarik dengan jawaban teoretis murni, standar C ++ memungkinkan perilaku tidak terdefinisi menjadi "perjalanan waktu":

[intro.execution]/5: Implementasi yang sesuai dengan menjalankan program yang terbentuk dengan baik akan menghasilkan perilaku yang dapat diamati yang sama sebagai salah satu kemungkinan eksekusi dari instance yang sesuai dari mesin abstrak dengan program yang sama dan input yang sama. Namun, jika eksekusi semacam itu mengandung operasi yang tidak ditentukan, Standar Internasional ini tidak mensyaratkan pelaksanaan yang menjalankan program itu dengan masukan itu (bahkan tidak berkaitan dengan operasi sebelum operasi tidak ditentukan pertama)

Dengan demikian, jika program Anda berisi perilaku tidak terdefinisi, maka perilaku seluruh program Anda tidak terdefinisi.

TartanLlama
sumber
4
@KeithThompson: Tapi kemudian, sneeze()fungsinya sendiri tidak ditentukan pada apa pun dari kelas Demon(yang nasal variasinya adalah subkelasnya), membuat semuanya tetap melingkar.
Sebastian Lenartowicz
1
Tapi printf mungkin tidak bisa kembali, jadi dua babak pertama ditetapkan karena sampai selesai belum jelas akan ada UB. Lihat stackoverflow.com/questions/23153445/…
usr
1
Inilah sebabnya mengapa kompiler secara teknis memiliki hak untuk mengeluarkan "nop" untuk kernel Linux (karena kode bootstrap bergantung pada perilaku yang tidak ditentukan): blog.regehr.org/archives/761
Crashworks
3
@Crashworks Dan itulah mengapa Linux ditulis, dan dikompilasi sebagai, C. yang tidak dapat dibawa (yaitu superset dari C yang membutuhkan kompilator tertentu dengan opsi tertentu, seperti -fno-strict-aliasing)
user253751
3
@usr Saya berharap itu didefinisikan jika printftidak kembali, tetapi jika printfakan kembali, maka perilaku yang tidak ditentukan dapat menyebabkan masalah sebelum printfdipanggil. Karenanya, perjalanan waktu. printf("Hello\n");dan kemudian baris berikutnya dikompilasi sebagaiundoPrintf(); launchNuclearMissiles();
user253751
31

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:

  • compiler dapat mengasumsikan bahwa bagian-bagian dari kode yang berisi Undefined Behavior tidak pernah dijalankan, dan dengan demikian menganggap jalur eksekusi yang mengarah padanya adalah kode mati. Lihat Apa yang harus diketahui setiap programmer C tentang perilaku tidak terdefinisi oleh Chris Lattner.
  • penaut dapat berasumsi bahwa dengan adanya beberapa definisi dari simbol yang lemah (dikenali dari namanya), semua definisi adalah identik berkat Aturan Satu Definisi
  • loader (jika Anda menggunakan pustaka dinamis) dapat mengasumsikan hal yang sama, sehingga mengambil simbol pertama yang ditemukannya; ini biasanya (ab) digunakan untuk mencegat panggilan menggunakan LD_PRELOADtrik di Unix
  • eksekusi mungkin gagal (SIGSEV) jika Anda menggunakan pointer yang menggantung

Inilah 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 .

Matthieu M.
sumber
3
Inilah yang membuatku khawatir. Dalam kode asli saya, ini rumit tetapi saya mungkin memiliki kasus di mana itu akan selalu meluap. Dan saya tidak terlalu peduli tentang itu, tetapi saya khawatir kode yang "benar" juga akan terpengaruh oleh ini. Jelas saya harus memperbaikinya, tetapi untuk memperbaikinya membutuhkan pemahaman :)
jcoder
8
@jcoder: Ada satu jalan keluar penting di sini. Kompilator tidak diizinkan untuk menebak data masukan. Selama setidaknya ada satu masukan yang Undefined Behaviornya tidak terjadi, kompilator harus memastikan bahwa masukan khusus ini masih menghasilkan keluaran yang benar. Semua pembicaraan menakutkan tentang optimasi berbahaya hanya berlaku untuk UB yang tak terhindarkan . Secara praktis, jika Anda akan menggunakan argcsebagai jumlah pengulangan, kasus argc=1ini tidak menghasilkan UB dan kompilator akan dipaksa untuk menanganinya.
MSalters
@jcoder: Dalam hal ini, ini bukan kode mati. Kompilator, bagaimanapun, bisa cukup pintar untuk menyimpulkan bahwa itidak dapat bertambah lebih dari Nkali dan oleh karena itu nilainya dibatasi.
Matthieu M.
4
@jcoder: Jika f(good);melakukan sesuatu X dan f(bad);memunculkan perilaku tidak terdefinisi, maka program yang baru saja dipanggil f(good);dijamin untuk melakukan X, tetapi f(good); f(bad);tidak dijamin untuk melakukan X.
4
@Hurkyl lebih menarik lagi, jika kode Anda adalah if(foo) f(good); else f(bad);, kompiler cerdas akan membuang perbandingan dan menghasilkan dan tanpa syarat foo(good).
John Dvorak
28

Compiler C atau C ++ yang mengoptimalkan secara agresif yang menargetkan bit 16 intakan mengetahui bahwa perilaku penambahan 1000000000ke suatu intjenis 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 ints 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 bit intatau lebih tinggi akan mengetahui bahwa loop adalah tak terbatas ( itidak berubah) dan sehingga aakhirnya akan meluap. Jadi sekali lagi, itu bisa mengoptimalkan output ke int 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.

Batsyeba
sumber
3
Apakah diizinkan oleh standar untuk melakukan apa pun yang diinginkannya bahkan sebelum perilaku yang tidak ditentukan terwujud? Dimanakah ini dinyatakan?
jimifiki
4
kenapa 16 bit? Saya kira OP sedang mencari overflow bertanda 32 bit.
4386427
8
@jimifiki Dalam standar. C ++ 14 (N4140) 1.3.24 "perilaku yang ditentukan = perilaku yang tidak diwajibkan oleh Standar ini." Ditambah catatan panjang yang menguraikan. Tetapi intinya adalah bahwa bukan perilaku "pernyataan" yang tidak terdefinisi, melainkan perilaku program. Artinya, selama UB dipicu oleh suatu aturan dalam standar (atau tidak adanya aturan), maka standar tersebut berhenti berlaku untuk program secara keseluruhan. Jadi bagian mana pun dari program dapat berperilaku seperti yang diinginkannya.
Angew tidak lagi bangga dengan SO
5
Pernyataan pertama salah. Jika int16-bit, penambahan akan dilakukan di long(karena operand literal memiliki tipe long) di mana itu didefinisikan dengan baik, kemudian dikonversi oleh konversi yang ditentukan implementasi kembali ke int.
R .. GitHub STOP HELPING ICE
2
@usr perilaku printfdidefinisikan oleh standar untuk selalu kembali
MM
11

Secara 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 ++.

bwDraco
sumber
Bagaimana jika bagian UB berada dalam satu 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?
mlvljr
1
Standar tidak memberlakukan persyaratan apa pun pada perilaku yang tidak ditentukan, jadi secara teori , ya, itu meracuni seluruh program. Namun, dalam praktiknya , setiap compiler pengoptimalan kemungkinan besar hanya akan menghapus kode yang mati, jadi mungkin tidak akan berpengaruh pada eksekusi. Namun, Anda tetap tidak boleh mengandalkan perilaku ini.
bwDraco
Senang mengetahui, thanx :)
mlvljr
9

Untuk memahami mengapa perilaku tidak terdefinisi dapat 'menjelajah waktu' seperti yang dikatakan @TartanLlama secara memadai , mari kita lihat aturan 'seolah-olah':

1.9 Eksekusi program

1 Deskripsi semantik dalam Standar Internasional ini mendefinisikan mesin abstrak nondeterministik berparameter. Standar Internasional ini tidak mensyaratkan struktur implementasi yang sesuai. Secara khusus, mereka tidak perlu menyalin atau meniru struktur mesin abstrak. Sebaliknya, implementasi yang sesuai diperlukan untuk meniru (hanya) perilaku yang dapat diamati dari mesin abstrak seperti yang dijelaskan di bawah ini.

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.

alain
sumber
6

Dengan asumsi int32-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.

R .. GitHub BERHENTI ICE BANTUAN
sumber
6

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.

Cort Ammon
sumber
Saya tidak menganggap kompiler yang sangat mencengangkan yang terkadang berperilaku seolah-olah aritmatika bertanda dilakukan pada jenis yang jangkauannya melampaui "int", terutama mengingat bahwa bahkan ketika melakukan pembuatan kode langsung pada x86 ada kalanya melakukannya lebih efisien daripada memotong perantara hasil. Yang lebih mencengangkan adalah ketika overflow memengaruhi penghitungan lain , yang dapat terjadi di gcc meskipun kode menyimpan produk dari dua nilai uint16_t ke dalam uint32_t - operasi yang seharusnya tidak memiliki alasan yang masuk akal untuk bertindak mengejutkan dalam build non-sanitasi.
supercat
Tentu saja, pemeriksaan yang benar adalah 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)
Random832
@ Random832 Saya meninggalkan satu ton. Saya telah mencoba mengutip konteks yang lebih besar, tetapi karena saya kehilangan sumber saya, saya menemukan parafrase konteks membuat saya lebih bermasalah, jadi saya mengabaikannya. Saya benar-benar perlu menemukan laporan bug yang gagal itu sehingga saya dapat mengutipnya dengan benar. Ini benar-benar contoh yang ampuh tentang bagaimana Anda dapat berpikir Anda menulis kode dengan satu cara, dan membuatnya dikompilasi sepenuhnya berbeda.
Cort Ammon
Ini adalah masalah terbesar saya dengan perilaku tidak terdefinisi. Terkadang tidak mungkin untuk menulis kode yang benar, dan ketika kompilator mendeteksinya, secara default tidak memberi tahu Anda bahwa kode itu dipicu oleh perilaku tidak terdefinisi. Dalam hal ini pengguna hanya ingin melakukan aritmatika - penunjuk atau tidak - dan semua kerja keras mereka untuk menulis kode aman telah dibatalkan. Setidaknya harus ada cara untuk memberi keterangan pada bagian kode - tidak ada pengoptimalan yang mewah di sini. C / C ++ digunakan di terlalu banyak area kritis untuk memungkinkan situasi berbahaya ini berlanjut demi pengoptimalan
John McGrath
4

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.

supercat
sumber
4

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.

Steve Summit
sumber
namun, inilah masalahnya ... kompiler dapat menggunakan perilaku tidak terdefinisi untuk mengoptimalkan tetapi MEREKA SECARA UMUM TIDAK MEMBERITAHU ANDA. Jadi jika kita memiliki alat yang luar biasa ini sehingga Anda harus menghindari melakukan X dengan cara apa pun, mengapa kompilator tidak dapat memberi Anda peringatan sehingga Anda dapat memperbaikinya?
Jason S
1

Satu hal yang tidak dipertimbangkan oleh contoh Anda adalah pengoptimalan. adisetel dalam perulangan tetapi tidak pernah digunakan, dan pengoptimal dapat menyelesaikannya. Dengan demikian, sah bagi pengoptimal untuk membuang asepenuhnya, dan dalam hal ini semua perilaku yang tidak ditentukan lenyap seperti korban boojum.

Namun tentu saja ini sendiri tidak ditentukan, karena pengoptimalan tidak ditentukan. :)

Graham
sumber
1
Tidak ada alasan untuk mempertimbangkan pengoptimalan saat menentukan apakah perilaku tidak ditentukan.
Keith Thompson
2
Fakta bahwa program berperilaku seperti yang mungkin diasumsikan oleh seseorang tidak berarti perilaku tidak terdefinisi "menghilang". Perilakunya masih belum ditentukan dan Anda hanya mengandalkan keberuntungan. Fakta bahwa perilaku program dapat berubah berdasarkan opsi compiler adalah indikator kuat bahwa perilaku tersebut tidak terdefinisi.
Jordan Melo
@JordanMelo Karena banyak dari jawaban sebelumnya membahas tentang pengoptimalan (dan OP secara khusus menanyakan tentang itu), saya telah menyebutkan fitur pengoptimalan yang tidak tercakup dalam jawaban sebelumnya. Saya juga menunjukkan bahwa meskipun pengoptimalan dapat menghapusnya, ketergantungan pada pengoptimalan untuk bekerja dengan cara tertentu lagi-lagi tidak ditentukan. Saya pasti tidak merekomendasikannya! :)
Graham
@KeithThompson Tentu, tetapi OP secara khusus bertanya tentang pengoptimalan dan pengaruhnya terhadap perilaku tidak terdefinisi yang akan dia lihat di platformnya. Perilaku spesifik tersebut dapat hilang, bergantung pada pengoptimalan. Seperti yang saya katakan dalam jawaban saya, ketidaktentuan tidak akan.
Graham
0

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:

Namun, jika Standar C mengenali keberadaan terpisah dari "nilai tak terdefinisi" (yang pembuatannya tidak sepenuhnya melibatkan "perilaku tak terdefinisi"), maka orang yang melakukan pengujian kompilator dapat menulis kasus uji seperti berikut, dan dia juga bisa mengharapkan (atau mungkin menuntut) bahwa implementasi yang sesuai harus, paling tidak, mengkompilasi kode ini (dan mungkin juga mengizinkannya untuk dieksekusi) tanpa "kegagalan".

int array1[5];
int array2[5];
int *p1 = &array1[0];
int *p2 = &array2[0];

int foo()
{
int i;
i = (p1 > p2); /* Must this be "successfully translated"? */
1/0; /* Must this be "successfully translated"? */
return 0;
}

Jadi pertanyaan intinya adalah: Haruskah kode di atas "berhasil diterjemahkan" (apa pun artinya)? (Lihat catatan kaki yang dilampirkan pada subpasal 5.1.1.3.)

dan tanggapannya adalah:

Standar C menggunakan istilah "nilai tak tentu" bukan "nilai tak terdefinisi". Penggunaan objek bernilai tak tentu menghasilkan perilaku tak terdefinisi. Catatan kaki untuk sub-klausul 5.1.1.3 menunjukkan bahwa implementasi bebas untuk menghasilkan sejumlah diagnostik selama program yang valid masih diterjemahkan dengan benar. Jika ekspresi yang evaulasinya akan menghasilkan perilaku tidak terdefinisi muncul dalam konteks di mana ekspresi konstan diperlukan, program yang memuatnya tidak sepenuhnya sesuai. Lebih lanjut, jika setiap eksekusi yang mungkin dari program tertentu akan menghasilkan perilaku tidak terdefinisi, program yang diberikan tidak sepenuhnya sesuai. Implementasi yang sesuai tidak boleh gagal untuk menerjemahkan program yang benar-benar sesuai hanya karena beberapa kemungkinan eksekusi dari program itu akan menghasilkan perilaku yang tidak terdefinisi. Karena foo mungkin tidak pernah dipanggil, contoh yang diberikan harus berhasil diterjemahkan dengan implementasi yang sesuai.

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:

Implementasi yang sesuai yang menjalankan program yang dibentuk dengan baik akan menghasilkan perilaku yang dapat diamati yang sama sebagai salah satu dari kemungkinan eksekusi dari instance yang sesuai dari mesin abstrak dengan program yang sama dan input yang sama. Namun, jika eksekusi semacam itu berisi operasi yang tidak ditentukan, dokumen ini tidak menempatkan persyaratan pada implementasi yang mengeksekusi program tersebut dengan input tersebut (bahkan tidak terkait dengan operasi sebelum operasi pertama yang tidak ditentukan).

Shafik Yaghmour
sumber
Fakta bahwa menjalankan suatu fungsi akan memanggil UB hanya dapat mempengaruhi cara program berperilaku ketika diberi beberapa masukan tertentu jika setidaknya satu kemungkinan eksekusi program ketika diberi masukan itu akan memanggil UB. Fakta bahwa memanggil suatu fungsi akan memanggil UB tidak mencegah program untuk memiliki perilaku yang ditentukan saat itu diberi masukan yang tidak memungkinkan fungsi tersebut dipanggil.
supercat
@supercat Saya percaya itulah jawaban saya untuk C setidaknya.
Shafik Yaghmour
Saya pikir hal yang sama berlaku untuk teks kutipan re C ++, karena frase "Setiap eksekusi seperti itu" mengacu pada cara program dapat mengeksekusi dengan input tertentu yang diberikan. Jika masukan tertentu tidak dapat menghasilkan fungsi yang dijalankan, saya tidak melihat apa pun dalam teks kutipan yang mengatakan bahwa apa pun dalam fungsi seperti itu akan menghasilkan UB.
supercat
-2

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 volatilevariabel 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):

Implementasi yang sesuai dengan menjalankan program yang dibentuk dengan baik akan menghasilkan perilaku yang dapat diamati yang sama sebagai salah satu dari kemungkinan eksekusi dari mesin abstrak yang sesuai dengan program yang sama dan input yang sama.
Namun, jika eksekusi semacam itu mengandung operasi yang tidak ditentukan, Standar Internasional ini tidak mensyaratkan pelaksanaan yang menjalankan program tersebut dengan masukan tersebut (bahkan tidak berkaitan dengan operasi sebelum operasi tidak ditentukan pertama).

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 foodan menunggu masukan Anda terlepas dari perilaku tidak terdefinisi yang mengikutinya:

printf("foo");
getchar();
*(char*)1 = 1;

Namun, perhatikan juga bahwa tidak ada jaminan yang fooakan 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 keluaran foodan 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 akibatnya printfdapat 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.

pengguna541686
sumber
1
Anda benar-benar salah membaca standar. Ia mengatakan perilaku saat menjalankan program tidak ditentukan. Titik. Jawaban ini 100% salah. Standarnya sangat jelas - menjalankan program dengan input yang menghasilkan UB di titik mana pun dalam aliran eksekusi naif tidak ditentukan.
David Schwartz
@DavidSchwartz: Jika Anda mengikuti interpretasi Anda sampai pada kesimpulan logisnya, Anda harus menyadari bahwa itu tidak masuk akal. Input bukanlah sesuatu yang sepenuhnya ditentukan saat program dimulai. Masukan ke program (bahkan hanya kehadirannya ) di setiap baris diperbolehkan untuk bergantung pada semua efek samping dari program sampai baris tersebut. Oleh karena itu, program tidak dapat menghindari timbulnya efek samping yang muncul sebelum jalur UB, karena hal tersebut memerlukan interaksi dengan lingkungannya sehingga mempengaruhi tercapainya jalur UB atau tidak.
pengguna541686
3
Itu tidak masalah. Betulkah. Sekali lagi, Anda hanya kekurangan imajinasi. Misalnya, jika kompilator dapat mengetahui bahwa tidak ada kode yang sesuai yang dapat membedakannya, ia dapat memindahkan kode yang merupakan UB sedemikian rupa sehingga bagian yang dijalankan UB sebelum keluaran yang Anda harapkan secara naif "sebelumnya".
David Schwartz
2
@Mehrdad: Mungkin cara yang lebih baik untuk mengatakan sesuatu adalah dengan mengatakan bahwa UB tidak dapat melakukan perjalanan waktu kembali melewati titik terakhir di mana sesuatu bisa terjadi di dunia nyata yang akan membuat perilaku didefinisikan. Jika sebuah implementasi dapat menentukan dengan memeriksa buffer input bahwa tidak mungkin 1000 panggilan berikutnya ke getchar () dapat diblokir, dan juga dapat menentukan bahwa UB akan terjadi setelah panggilan ke 1000, maka tidak akan diperlukan untuk melakukan salah satu dari panggilan. Namun, jika implementasi menetapkan bahwa eksekusi tidak akan meneruskan getchar () sampai semua keluaran sebelumnya memiliki ...
supercat
2
... telah dikirim ke terminal 300-baud, dan setiap kontrol-C yang terjadi sebelumnya akan menyebabkan getchar () menaikkan sinyal bahkan jika ada karakter lain di buffer sebelumnya, maka implementasi seperti itu tidak bisa pindahkan UB apapun melewati output terakhir sebelum getchar (). Apa yang rumit adalah mengetahui dalam kasus apa kompilator harus diharapkan melewati pemrogram jaminan perilaku apa pun yang mungkin ditawarkan oleh implementasi perpustakaan di luar yang diamanatkan oleh Standar.
supercat