Segala sesuatu di C adalah undefined behavior
(blog.habets.se)- Undefined behavior (UB) bukanlah optimisasi jahat dari compiler, melainkan aturan bahwa compiler tidak wajib menangani jalur eksekusi yang seharusnya mustahil jika kodenya valid
- Dalam kode C/C++ yang tidak sepele, UB tersembunyi luas bukan hanya sebagai double-free atau akses di luar batas, tetapi juga dalam hal-hal halus seperti alignment, casting, inisialisasi, dan ketidakcocokan tipe
- Mengakses
int*ataustd::atomic<int>*yang tidak selaras sudah merupakan UB menurut standar, meski hasil nyata di tiap platform bisa berbeda:SIGBUS, dikoreksi kernel, atau tampak berjalan normal - Kode umum seperti memberikan signed
charkeisxdigit(), mengubahfloatmenjadiint, atau salah memakaiNULLdan argumen variadik juga mudah keluar dari standar - Basis kode lama memang tidak bisa dibuang, tetapi perlu diperbaiki dalam skala besar dengan menggabungkan deteksi UB berbasis LLM dan verifikasi pakar, karena masalahnya terlalu subtil untuk diserahkan ke programmer junior
Undefined behavior di C/C++ bukan masalah optimisasi
- Undefined behavior (UB) bukan berarti compiler “mengeksploitasi” kesalahan programmer, melainkan bahwa program boleh diasumsikan valid menurut standar
- Meski niatnya jelas bagi manusia, niat itu bisa sulit diekspresikan di tahap compiler atau antar modul
- Compiler tidak wajib menangani kasus khusus yang “mustahil terjadi” saat menghasilkan kode, dan hasil yang berbeda dari niat bisa muncul di jalur eksekusi yang melibatkan perangkat keras
- Mematikan optimisasi tidak membuat UB menjadi aman, dan tidak ada jaminan perilaku yang sama akan tetap terjaga di compiler atau arsitektur sekarang maupun di masa depan
UB tidak hanya ada di kode yang aneh
- double-free, use-after-free, akses di luar batas objek, dan akses ke memori yang belum diinisialisasi adalah UB yang sudah dikenal, tetapi tetap berulang di seluruh industri
- Ada juga banyak UB yang lebih halus dan tidak intuitif, sehingga kode C/C++ yang terlihat biasa saja pun mudah keluar dari standar
- Dalam standar C23, kata “undefined” muncul 283 kali, dan cakupannya lebih luas lagi jika memasukkan kasus yang tidak didefinisikan secara implisit
- Pada hampir semua kode C/C++ yang tidak sepele, UB ada di mana-mana, sehingga sulit menyalahkannya semata-mata pada kelalaian programmer individu
Akses ke objek yang tidak selaras
- Fungsi yang melakukan dereference
int*seperti berikut menjadi UB bila pointer tidak memiliki alignment yang benarint foo(const int* p) { return *p; } - Alignment biasanya bisa berarti alamat yang merupakan kelipatan
sizeof(int), tetapi kebutuhan sebenarnya bisa berbeda tergantung platform dan implementasi - Di Linux Alpha, dalam beberapa kasus kernel bisa menangkap trap dan meniru akses yang dimaksud lewat perangkat lunak, tetapi dalam kasus lain program bisa mati dengan
SIGBUS - Di SPARC akan muncul
SIGBUS, sedangkan di x86/amd64 umumnya tampak berjalan normal atau bahkan terlihat seperti pembacaan atomik - Di ARM, RISC-V, dan arsitektur masa depan, hasilnya tidak bisa digeneralisasi, dan arsitektur mendatang bahkan bisa saja memakai bit-bit rendah
int*untuk register khusus - Jika compiler memakai instruksi load yang berbeda, akses yang sebelumnya dikoreksi kernel mungkin tidak lagi dikoreksi
- Compiler tidak berkewajiban menghasilkan assembly yang tetap bekerja untuk pointer yang tidak selaras; akses itu sendiri sudah UB
Tipe atomik juga tetap UB jika alignment salah
- Bahkan jika memanggil
store()atauload()padastd::atomic<int>*seperti berikut, perilakunya tetap UB bila objek tidak selaras dengan benarvoid set_it(std::atomic<int>* p) { p->store(123); } int get_it(std::atomic<int>* p) { return p->load(); } - Dari sudut pandang standar, pertanyaan “apakah operasi ini tetap atomik pada objek yang tidak selaras” sebenarnya tidak valid
- Di perangkat keras nyata ini bisa menjadi masalah atomisitas, tetapi menurut standar, sebelumnya sudah UB lebih dulu
- Jika objek yang dianggap dibaca secara atomik melintasi halaman, masalahnya makin rumit, tetapi kesimpulannya tetap bukan “aman” melainkan UB
Hanya membuat pointer pun bisa jadi masalah
- Pointer yang tidak selaras bisa bermasalah bahkan sebelum didereference; sekadar cast ke tipe pointer tertentu saja bisa sudah salah
bool parse_packet(const uint8_t* bytes) { const int* magic_intp = (const int*)bytes; // UB! int magic_raw = foo(magic_intp); // Probably crashes on SPARC. int magic = ntohl(magic_raw); // this is fine, at least. […] } - Di sini masalahnya bukan pemanggilan
foo(), melainkan cast(const int*)bytes - Menurut standar, compiler juga bisa saja memberi makna khusus pada bit-bit rendah
int*, misalnya untuk garbage collection atau bit tag keamanan
Masalah memberi char ke isxdigit()
- Kode berikut terlihat sederhana, tetapi pada arsitektur yang
char-nya signed, nilainya bisa menjadi UB jika input berada di luar rentang 0–127bool bar(char ch) { return isxdigit(ch); } isxdigit()adalah fungsi untuk memeriksa apakah suatu karakter adalah digit heksadesimal, dan juga bisa menerimaEOFsebagai argumen- Menurut C23 7.4p1,
EOFbertipeintdan dapat disimpulkan sebagai nilai yang tidak dapat direpresentasikan olehunsigned char isxdigit()menerimaint, bukanchar, dan walaupun konversi daricharkeintdimungkinkan, nilai negatif dari signedcharmenimbulkan masalah- Menurut C23 6.2.5 paragraf 20, apakah
charbertanda signed ditentukan oleh implementasi - Implementasi
isxdigit()seperti berikut dapat membaca memori tak dikenal dengan indeks negatifint isxdigit(int c) { if (c == EOF) { return false; } return some_array[c]; } - Jika memori itu adalah area pemetaan I/O, dampaknya bisa melampaui nilai acak atau crash dan sampai memicu perilaku perangkat keras
- Ini lebih mungkin terjadi di sistem embedded daripada aplikasi desktop, tetapi bahkan di ruang pengguna pun ada kasus seperti driver jaringan user-space di mana perlindungannya tidak cukup kuat
Masalah cast dari float ke int
- Kode yang mengubah nilai
floatdalam detik menjadiintmilidetik seperti berikut sangat umum, tetapi mengandung UBint milliseconds(float seconds) { int tmp = (int)(seconds * 1000.0); /* WRONG */ return tmp + 1; /* WRONG separately (signed overflow is UB) */ } - C23 6.3.1.4 menetapkan bahwa saat nilai floating-point riil berhingga dikonversi ke tipe integer, perilakunya tidak didefinisikan jika bagian bulatnya tidak dapat direpresentasikan oleh tipe integer tersebut
- Untuk nilai tak berhingga pun tidak ada definisi eksplisit, sehingga itu juga menjadi UB
- Bahkan membandingkan
floatdenganINT_MAXpun tidak sesederhana yang terlihat- Melakukan cast
floatkeintbisa justru memicu UB yang ingin dihindari - Melakukan cast
INT_MAXkefloattidak menjamin nilainya dapat direpresentasikan dengan tepat - Jika
INT_MAXdibulatkan saat menjadifloatlalu berubah menjadi nilai yang tak bisa direpresentasikan olehint, perbandingan kehilangan makna representatifnya
- Melakukan cast
- Agar aman, perlu pemeriksaan
isfinite(), perbandingan dengan batas aman sepertiINT_MIN + 1000danINT_MAX - 1000, serta pemeriksaan tambahan setelah konversi sebelum penjumlahanint milliseconds(float seconds) { const float ftmp = seconds * 1000.0f; if (!isfinite(ftmp)) { return 0; } if ((float)(INT_MIN + 1000) > ftmp) { return 0; } if ((float)(INT_MAX - 1000) < ftmp) { return 0; } const int tmp = (int)ftmp; if (INT_MAX == tmp) { return 0; } return tmp + 1; } - Hanya untuk mengubah
floatkeintsaja, versi aman kodenya menjadi jauh lebih panjang
Objek di alamat 0 dan null pointer
- Di kernel OS atau kode embedded, ada situasi di mana orang ingin menempatkan objek di alamat 0
- Secara praktis, bisa dikatakan tidak ada cara yang benar-benar sesuai standar C untuk menempatkan objek nyata di alamat 0
- Dalam C 6.3.2.3, konstanta integer 0 dan
nullptryang bisa dikonversi ke pointer adalah “null pointer constant”, dan di sini dapat disebutNULL - C tidak menetapkan bahwa pointer
NULLyang nyata menunjuk ke alamat mesin 0 - Standar C berbicara tentang mesin abstrak C, bukan perangkat keras, dan hanya menjamin bahwa
NULLdan 0 akan bernilai sama saat dibandingkan - Kesamaan itu bisa saja terjadi karena integer 0 dikonversi ke nilai
NULLnative platform tersebut, yang bahkan bisa bernilai0xffff - Melakukan dereference pada null pointer adalah UB apa pun nilainya, dan ini adalah contoh representatif di C 3.4.3
- Karena itu, kita tidak bisa mengasumsikan
memset(&ptr, 0, sizeof(ptr));akan menghasilkan pointerNULL - Cara menginisialisasi struct dengan nol lalu menganggap pointer anggotanya menjadi
NULLjuga dapat menimbulkan masalah nyata bagi kebanyakan programmer - Secara historis, memang pernah ada mesin yang menggunakan pointer NULL bukan nol
Masalah menganggap ada fungsi di alamat 0
- Bahkan jika di mesin modern
NULLmenunjuk ke alamat 0 dan benar-benar ada objek atau fungsi di alamat itu, C 6.3.2.3 menyatakan bahwaNULLtidak sama dengan objek atau fungsi mana pun - Karena itu, kode berikut adalah UB
void (*func_ptr)() = NULL; func_ptr(); - Dari sudut pandang C, artinya adalah “tidak ada fungsi di sana”, dan compiler mungkin tidak punya cara untuk mengekspresikan niat lain di internalnya
- Kita tidak bisa begitu saja mengasumsikan compiler akan mengeluarkan instruksi call ke alamat yang semua bitnya nol
- Pada x86 16-bit pun tidak jelas apakah “semua nol” berarti
0000:0000atauCS:0000
Argumen variadik dan ketidakcocokan tipe
- Argumen terakhir
execl()harus berupa pointer, jadi memberikan makroNULLatau integer 0 secara langsung bisa menjadi UBexecl("/bin/sh", "sh", "-c", "date", NULL); /* WRONG */ execl("/bin/sh", "sh", "-c", "date", 0); /* WRONG */ - Bentuk yang benar adalah melakukan cast eksplisit ke tipe pointer
execl("/bin/sh", "sh", "-c", "date", (char*)NULL); - Makro
NULLbisa ditafsirkan sebagai integer 0, dan pada argumen variadik informasi tipe yang diperlukan tidak ikut diteruskan - Pada
printf()pun, jika format specifier tidak cocok dengan tipe argumen sebenarnya, itu adalah UBuint64_t blah = 123; printf("%ld\n", blah); /* WRONG */ - Untuk mencetak
uint64_t, gunakanPRIu64uint64_t blah = 123; printf("%"PRIu64"\n", blah); - Untuk mencetak
uid_t, salah satu caranya mungkin dengan cast keuintmax_tlalu memakaiPRIuMAX, tetapi bahkan apakahuid_tbertipe unsigned pun tidak selalu pasti - Dalam kasus terburuk, alih-alih
-1, yang tercetak bisa saja nilai tak bermakna
Pembagian dengan 0 dan masalah keamanan
- Fakta bahwa membagi dengan 0 adalah UB sudah dikenal luas, tetapi jika penyebut berasal dari input yang tidak tepercaya, ini menjadi masalah keamanan
- Yang penting adalah bahwa UB bisa terjadi tepat di batas validasi input, bukan sekadar sebagai error runtime biasa
Bukan UB, tetapi integer promotion juga berbahaya
- Aturan integer promotion sulit diterapkan sambil sekilas membaca kode, dan hasilnya bisa bertentangan dengan intuisi
- Pada kode berikut,
overflowedmenjadi 0, bukan 1unsigned char a = 0xff; unsigned char b = 1; unsigned char zero = 0; bool overflowed = (a + b) == zero; // overflowed is set to zero, not one. - Pada kode berikut, meski semua variabel tampak unsigned, hasilnya bukan
2147483648 (0x80000000)melainkan18446744071562067968 (ffffffff80000000)unsigned char a = 0x80; uint64_t b = a << 24; // Bonus UB(?) - Sekalipun bukan UB, aturan integer di C/C++ tetap tidak intuitif dan mudah menimbulkan cacat
Deteksi UB dengan LLM
- LLM modern, jika diminta mencari UB dalam kode C acak, hampir selalu menemukan masalah, dan hasilnya umumnya benar
- Setelah menemukan UB di kode pribadi, pendekatan yang sama diterapkan pada kode OpenBSD yang matang dan ditulis dengan disiplin ketat
- Beberapa masalah ditemukan pada alat
find, yang menjadi target pertama - Patch dikirim ke OpenBSD untuk penulisan di luar batas dan untuk bug logika yang bukan UB
- Untuk banyak UB lain yang masih tersisa, patch tidak dikirim
- Ada pengalaman masa lalu bahwa proyek OpenBSD tidak terlalu menerima laporan bug
- Ada penilaian bahwa beberapa kasus mungkin sebenarnya aman
- Jika OpenBSD ingin menghapus UB dari basis kodenya, dibutuhkan proyek yang lebih besar daripada sekadar meneruskan patch satu per satu antara LLM dan proyek
Arah realistis untuk menangani basis kode C/C++
- Basis kode C/C++ yang sudah ada tidak bisa dibuang, tetapi membiarkannya dalam keadaan rusak secara mendasar juga bukan pilihan
- UB perlu diperbaiki secara massal dengan cara yang tidak memasukkan perubahan buruk buatan AI ke commit, sekaligus tidak membebani reviewer manusia
- Pada 2026, menulis C atau C++ tanpa pengawasan UB dari LLM bisa dipandang seperti pelanggaran SOX dan tindakan yang tidak bertanggung jawab
- Jika pengembang OpenBSD pun tidak berhasil menemukan semua masalah ini selama lebih dari 30 tahun, peluang proyek lain lebih kecil lagi
- Dalam proyek pribadi, orang bisa meminta LLM untuk mencari UB, menjelaskannya bila perlu, memperbaikinya, lalu manusia memeriksa hasilnya
- Namun, untuk memverifikasi hasil tetap dibutuhkan pakar, dan para pakar biasanya sibuk dengan hal lain
- Pekerjaan ini tampak seperti pekerjaan bersih-bersih, tetapi terlalu halus untuk diserahkan kepada programmer junior yang secara tradisional sering diberi tugas semacam itu
1 komentar
Komentar Hacker News
C punya banyak perilaku tak terdefinisi yang mengejutkan dan aneh, tetapi tulisan ini tidak terlalu berhasil menunjukkannya dan hanya menggores permukaannya saja
Contoh yang lebih aneh adalah
volatile int x = 5; printf("%d in hex is 0x%x.\n", x, x);. Jikaxhanyaint, itu tidak masalah, tetapi jikavolatilemaka itu menjadi perilaku tak terdefinisi. Dalam standar C, aksesvolatileadalah efek samping bahkan hanya dengan membaca, efek samping yang tidak berurutan pada objek skalar yang sama adalah perilaku tak terdefinisi, dan evaluasi argumen fungsi tidak memiliki urutan yang ditentukan satu sama lainBiasanya data race berarti thread yang berbeda mengakses objek yang sama secara bersamaan dan setidaknya salah satunya adalah penulisan, tetapi di C situasi mirip data race bisa muncul bahkan dalam satu thread tanpa penulisan
undefined, atau semua kasus tak terdefinisi yang muncul karena penghilangan penjelasanPoinnya adalah ini tidak bisa dihindari. Setidaknya sejak C muncul pada 1972, belum pernah ada manusia yang benar-benar berhasil menghindarinya
Jika selama 54 tahun belum berhasil, maka “berusahalah lebih keras” atau “jangan membuat kesalahan” bukanlah solusi. Satu celah yang dapat dieksploitasi yang ditemukan Mythos di OpenBSD dinilai cukup baik oleh para pengembang OpenBSD, tetapi bahkan ketika alat dijalankan pada kode yang paling sederhana pun, perilaku tak terdefinisi bermunculan di mana-mana
Misalnya,
findmembaca variabel otomatisstatusyang belum diinisialisasi setelahwaitpid(&status)dan sebelum memeriksa apakahwaitpid()gagal, dan itu juga perilaku tak terdefinisi, meskipun sulit membayangkan arsitektur atau compiler tempat hal itu bisa dieksploitasiSeperti yang saya tulis di artikel, maksudnya bukan mencantumkan semua perilaku tak terdefinisi di dunia, melainkan menyampaikan bahwa semua kode C/C++ yang tidak sepele mengandung perilaku tak terdefinisi
volatileadalah hack pada sistem tipe. Seharusnya ada solusi yang lebih berprinsip, dan bahasa modern tidak seharusnya menirunya seolah “karena C melakukan itu, berarti itu ide bagus”Compiler C awal selalu menuliskan nilai ke memori, jadi jika pointer diarahkan ke perangkat keras memory-mapped I/O, setiap kali
xberubah, instruksi CPU benar-benar melakukan penulisan ke memori dan kode driver pun bekerjaTetapi ketika optimisasi muncul, compiler melihat bahwa ia hanya terus memodifikasi
xdan menyimpannya di register saja, sehingga driver rusak.volatiledi C adalah hack untuk memberi tahu compiler “jangan lakukan optimisasi itu”, sedangkan solusi yang benar, yaitu menyediakan intrinsic memory-mapped I/O di tingkat library, akan menjadi pekerjaan yang jauh lebih besarAlasan intrinsic diperlukan adalah karena ia bisa mengekspresikan secara tepat perilaku yang mungkin dan yang tidak mungkin. Pada beberapa target, penulisan 1 byte, 2 byte, dan 4 byte masing-masing memiliki perilaku berbeda, dan perangkat keras pun membedakannya. Beberapa perangkat mengharapkan penulisan RGBA 4 byte, dan jika yang dikeluarkan adalah empat penulisan 1 byte, perangkat bisa bingung atau tidak berfungsi. Beberapa target bahkan mendukung penulisan per bit. Dengan
volatilesaja, tidak ada cara untuk mengetahui apa yang sedang terjadi atau apa maknanyaSetelah program C dikompilasi lalu di-disassemble, hasilnya menjadi program assembly yang tidak memiliki perilaku tak terdefinisi. Itu karena assembly tidak punya konsep perilaku tak terdefinisi
Perilaku tak terdefinisi adalah properti program sumber, bukan properti file eksekusi. Artinya spesifikasi bahasa tempat sumber itu ditulis tidak memberikan makna pada program tersebut. Sebaliknya, file eksekusi hasil kompilasi diberi makna oleh spesifikasi mesin
Race adalah properti dari perilaku program. Jadi kita bisa mengatakan bahwa program C mengandung perilaku tak terdefinisi, tetapi kita tidak bisa mengatakan bahwa file eksekusinya benar-benar mengalami race. Tentu saja, compiler bebas mengompilasi program dengan perilaku tak terdefinisi sesuka hati, jadi bisa saja ia memperkenalkan race, tetapi jika ia mengompilasikannya tanpa membuat thread baru, maka tidak ada race
volatilejustru bahwa nilainya dapat berubah oleh sesuatu yang lain. Jika itu variabel global, “sesuatu yang lain” itu bisa berupa thread lain, interupsi, atau signal handler. Jika itu pointer yang membaca alamat tertentu, nilainya bisa berasal dari register perangkat keras yang berubahKonsep variabel
volatileitu sendiri bukan masalah. Jika suatu bahasa ingin mendukung routine interupsi dan memory-mapped I/O, compiler perlu diberi cara untuk mengetahui bahwa membaca register perangkat keras yang sama dua kali berbeda dengan membaca lokasi memori yang sama dua kaliMasalah sebenarnya adalah interaksi antara fitur bahasa dan pembatasannya tidak ditata dengan baik. Sangat konyol jika kita sudah menyatakan “nilai ini bisa berubah kapan saja”, tetapi justru karena alasan itu beberapa pemakaiannya dianggap perilaku tak terdefinisi. Seharusnya ada pengecualian untuk variabel
volatiledalam definisi “efek samping yang tidak berurutan”Banyak orang salah paham dan mengira C dan C++ itu “sangat fleksibel karena membiarkan Anda melakukan apa pun yang diinginkan”. Kenyataannya, hampir semua teknik yang tampak kuat dan keren adalah ladang ranjau perilaku tak terdefinisi
Perilaku tak terdefinisi pada pointer yang tidak aligned lebih buruk lagi. Pointer yang tidak aligned itu sendiri sudah perilaku tak terdefinisi, bukan hanya saat diakses
Jadi melakukan cast implisit dari
void* vkeint* i, misalnyai=vdi C atauf(v)ketikafmenerimaint*, juga merupakan perilaku tak terdefinisi jika pointer hasilnya tidak memenuhi syarat alignmentintPenting untuk menekankan bahwa ini masalah pada level C. Jika program C mengandung perilaku tak terdefinisi, maka secara formal program C itu tidak valid dan salah. Ini bukan masalah perangkat keras, juga tidak terkait crash atau bug
Cast dari
void*keint*biasanya tidak menghasilkan instruksi apa pun pada kode perangkat keras, dan karena tipe hanya ada di C, perangkat keras juga tidak akan crash pada cast itu. Orang bisa saja berpikir bahwa selama nilai integer dalam register masih baik-baik saja, maka tidak apa-apa, tetapi intinya bukan apakah pointer di perangkat keras benar-benar integer, melainkan bahwa pada saat cast ke pointer yang tidak aligned, program C itu sudah rusak menurut definisinyaSaya juga mencoba menyampaikan bahwa perilaku tak terdefinisi tidak berada di perangkat keras, dan tidak berkaitan dengan crash atau bug. Pada saat yang sama, saya ingin menunjukkan contoh kepada orang-orang yang berkata “tapi lihat, ini berjalan baik-baik saja”, padahal sebenarnya tidak demikian
#pragma pack(push, 1), apakah itu berarti kita tidak bisa menggunakan pointer anggota kecuali anggota itu kebetulan aligned?Perilaku tak terdefinisi jenis ini tidak masalah, dan hampir tidak ada orang yang benar-benar menganggap bug akibat perbedaan perangkat keras itu sendiri sebagai masalah besar
Namun seiring waktu, penafsiran yang agresif telah mengubah C menjadi semacam bahasa design by contract implisit, dan pembatasannya menjadi tidak terlihat. Ini menimbulkan masalah yang mirip dengan pemanggilan destructor implisit di RAII yang juga tidak terlihat
Di C, ketika Anda melakukan dereference pointer, compiler menambahkan batasan implisit bahwa pointer itu tidak boleh null ke signature fungsi. Jika Anda meneruskan pointer yang bisa null ke fungsi tanpa pemeriksaan atau assertion, bukannya muncul kesalahan karena pointer nullable dikirim, compiler justru diam-diam menyebarkan batasan non-null itu ke pointer tersebut. Jika ia bisa membuktikan bahwa batasan itu salah, fungsi itu ditandai tidak dapat dicapai, dan pemanggilan fungsi yang tidak dapat dicapai itu membuat fungsi pemanggil ikut dianggap tidak dapat dicapai
Lima tahap mempelajari perilaku tak terdefinisi di C
Penyangkalan: “Saya tahu apa yang terjadi dengan signed overflow di mesin saya”
Marah: “Compiler ini sampah! Kenapa tidak melakukan seperti yang saya suruh?”
Tawar-menawar: “Saya akan mengajukan proposal ini ke wg14 untuk memperbaiki C…”
Depresi: “Apakah ada kode C yang benar-benar bisa dipercaya?”
Penerimaan: “Jangan gunakan perilaku tak terdefinisi saja”
Akses yang tidak aligned bisa ditangani dengan packed struct. Compiler akan secara ajaib menghasilkan kode yang benar. Sebenarnya compiler memang selalu tahu cara melakukannya dengan benar, hanya saja tidak melakukannya
Aturan strict aliasing bisa diatasi dengan type punning via union. Compiler yang penting biasanya mendokumentasikan bahwa itu bekerja meskipun standar tidak mengatakannya. Atau bisa juga dimatikan dengan
-fno-strict-aliasing. Anda bisa menafsirkan ulang memori sesuka hati, dan meskipun tetap ada sudut tajam, setidaknya itu bukan berasal dari compilerOverflow bisa didefinisikan dengan
-fwrapv. Jika+,-,*diganti dengan__builtin_*_overflow, Anda juga mendapatkan pemeriksaan error eksplisit secara gratis. Antarmuka fungsionalnya juga bagus dan kode yang dihasilkan efisienPenerimaan yang sesungguhnya lebih dekat ke “orang normal tidak peduli pada standar C”. Standarnya buruk, dan yang penting adalah compiler. Compiler punya banyak fitur yang sangat berguna untuk menghindari sebagian besar masalah ini. Alasan orang tidak memakainya adalah karena mereka ingin menulis C yang “portabel” dan “standar”, dan keluar dari pola pikir itulah penerimaan yang sebenarnya
Dengan logika ini saya membuat interpreter Lisp di C freestanding dan bahkan lolos UBSan. Awalnya saya kira akan meledak, ternyata tidak, dan kalau saya bisa melakukannya maka siapa pun bisa
Selama manusia yang menulis kode, itu tidak bisa menjadi keadaan akhir. Tidak ada manusia yang bisa sepenuhnya menghindari perilaku tak terdefinisi di C/C++
Contoh-contohnya lebih mirip kasus yang bisa menjadi perilaku tak terdefinisi tergantung input atau keadaan, daripada benar-benar perilaku tak terdefinisi yang aktual
Kalau definisinya dilonggarkan sejauh itu, maka setiap pemanggilan fungsi juga perilaku tak terdefinisi karena bisa saja kehabisan ruang stack. Sebenarnya dalam arti seperti itu, hampir semua bahasa juga begitu
C sudah punya cukup banyak sisi kasar yang nyata, dan sensasionalisme seperti ini justru bisa mengalihkan perhatian, terutama bagi pemula, dan malah merugikan
STORAGE_ERRORyang didefinisikanhttp://archive.adaic.com/standards/83lrm/html/lrm-11-01.html
Di situ tertulis bahwa exception ini juga terjadi “jika selama eksekusi pemanggilan subprogram, ruang penyimpanan yang tersedia tidak mencukupi”
Pertama, apa yang terjadi ketika ruang stack habis itu bisa didefinisikan. Selain itu, tidak semua program membutuhkan stack berukuran sembarang; beberapa program hanya memerlukan ukuran konstan yang dapat dihitung sebelumnya. Bahkan ada implementasi bahasa yang sama sekali tidak menggunakan stack
Bahasa juga bisa menyediakan alat untuk memeriksa sisa ruang stack dan memberi jaminan sesuai itu. Atau memungkinkan pemasangan handler yang akan dijalankan ketika ruang stack habis
Cara berpikir yang benar adalah bahwa begitu perilaku tak terdefinisi terjadi, Anda tidak lagi berada di bawah perlindungan standar bahasa. Bisa saja semuanya tetap berjalan baik untuk sementara, bahkan mungkin selamanya. Tetapi dalam praktiknya Anda tanpa sadar menjadi bergantung pada keinginan toolchain, pergantian atau upgrade compiler, arsitektur, runtime, atau perbedaan versi libc
Pada akhirnya Anda membangun fondasi di atas pasir, dan itulah bahaya perilaku tak terdefinisi
Masalah perilaku tak terdefinisi bukan bahwa ia bisa crash di arsitektur tertentu
Masalah sebenarnya adalah compiler menganggap kode seperti itu tidak akan pernah muncul. Namun jika Anda tetap menulis kode dengan perilaku tak terdefinisi, compiler, terutama optimizer, bisa menerjemahkannya ke bentuk apa pun yang nyaman bagi jalur normal. Dan “apa pun” itu kadang bisa sangat tak terduga, seperti menghapus potongan kode yang besar
Jika ditambah tail recursion, bug itu bahkan mungkin tidak terlihat di build debug karena loop tak berujungnya tidak tercapai, lalu baru muncul saat level optimisasi dinaikkan
Yang lebih buruk adalah ketika program diam-diam terus berjalan dengan nilai sampah, memformat hard disk, atau menyerahkan kunci kerajaan kepada penyerang
Orang yang berkata “ya sudah, definisikan saja” atau jadikan unspecified behavior melewatkan inti bahwa compiler bisa menghapus bagian besar program
Jika Anda menulis kode yang menjadi perilaku tak terdefinisi untuk input tertentu, maka untuk input itu Anda memang berniat agar program tidak memiliki perilaku apa pun. Anda ingin compiler menghilangkan jalur itu lewat optimisasi, atau melakukan hal lain yang membantu perilaku kasus lain yang terdefinisi
Cukup memuaskan menaruh string log yang hanya bisa dicapai lewat perilaku tak terdefinisi, lalu melihat string itu tidak tersisa di binary
Dulu saya pernah menulis analysis pass dengan asumsi ia dijalankan di akhir pipeline transformasi, dan asumsi itu diperlukan untuk korektness. Karena setelah itu tidak ada lagi optimisasi, saya menganggapnya aman, tetapi sekarang saya tidak lagi yakin
Saya sudah memakai C selama 20 tahun, tetapi dalam 6 bulan terakhir di Hacker News saya merasa mendengar soal perilaku tak terdefinisi lebih banyak daripada sebelumnya
Dalam percakapan nyata topik ini hampir tidak pernah muncul. Orang menulis kode, kalau tidak jalan ya di-debug lalu diperbaiki atau diakali. Saya tidak mengerti kenapa topik perilaku tak terdefinisi di C terus muncul ke halaman depan seperti ini
Selalu ada sebagian kecil lulusan ilmu komputer yang menganggap mengembangkan atau memakai bahasa pemrograman baru adalah hal paling menarik di dunia, dan sebagian dari mereka terus berpikir begitu
Wajar jika orang seperti itu tertarik pada aspek desain bahasa, dan perilaku tak terdefinisi di C termasuk dalam wilayah tersebut. Hanya saja, banyak bagiannya pada awalnya lebih merupakan upaya menampung arsitektur CPU lama tanpa kehilangan performa, sehingga sulit menyebutnya “pilihan desain” seperti halnya mengatakan roda itu bulat adalah pilihan desain
Sekitar era GCC 3.2, compiler mulai memanfaatkan perilaku tak terdefinisi jauh lebih agresif untuk optimisasi, dan ada sejumlah “skandal” yang cukup terkenal karena itu. Akibatnya banyak orang bertahan lama di GCC 2.95. GCC 3.2 keluar pada 2002
Semua perusahaan terus menekankan keamanan dan paparan, yaitu bisa masuk berita, sehingga narasi yang menentang “ketidakamanan” menjadi berlebihan
Dunia baru ini mirip orang kota yang belum pernah melihat alam liar lalu takut pada mesin pemotong rumput. Bilahnya berputar? Gila!
Jika target Anda sebenarnya adalah sistem embedded kecil di menara komunikasi terpencil, maka “di mesin saya jalan” tidak ada gunanya. Tentu sebagian besar orang tidak mengerjakan itu, dan kebanyakan developer di sini mungkin web developer, tetapi ini tetap diskusi yang menarik meskipun belum pernah mengalaminya sendiri. Bahkan mungkin justru lebih menarik karena itu
Compiler bisa punya bug yang membuat perilaku yang menurut spesifikasi seharusnya berjalan ternyata tidak berjalan, ada banyak ekstensi yang tidak punya padanan di standar, dan ada juga perilaku yang menurut standar tak terdefinisi tetapi oleh implementasi tertentu diberi hasil yang bermakna
Saya umumnya setuju dengan pengantarnya, tetapi contohnya kurang bagus dan keseluruhan tulisannya terasa seperti kemasan untuk mendorong coding dengan LLM
Terkesan seperti orang yang ingin menulis kode sesuka hati lalu berharap hasilnya sama di semua lingkungan. Jika bahasa dibuat seperti itu, maka hilanglah keuntungan bahwa Anda bisa menulis sesuai platform ketika memang dibutuhkan
Kode C++ di tulisan ini sebagian sudah tidak idiomatis selama lebih dari 10 tahun, dan hari ini bisa dianggap bau kode
Bahasanya telah berevolusi cukup jauh dari saat pertama kali dibuat. Begitu terlihat banyak raw pointer dan akses pointer langsung, sudah jelas bahwa sebagian tulisan itu perlu dibaca dengan sedikit filter
Masalah jelas lainnya adalah sudut pandang yang menggabungkan C dan C++ seolah keduanya hampir sama. Saat ini kedua bahasa itu sebenarnya sudah cukup jauh berbeda
std::atomic, bukanatomic_intApakah pemahaman tentang perilaku tak terdefinisi di C seperti ini benar?
Program P memiliki himpunan input A yang tidak memicu perilaku tak terdefinisi, dan himpunan komplemen B yang memicunya
Compiler yang benar mengompilasi P menjadi executable P'. Untuk semua input di A, P' harus berperilaku sama dengan P
Tetapi untuk input mana pun di B, perilaku P' tidak memiliki persyaratan apa pun
Contoh konkret perilaku tak terdefinisi yang disebabkan pointer tidak aligned: https://pzemtsov.github.io/2016/11/06/bug-story-alignment-on...