Standar C tidak menetapkan apa pun yang harus terjadi ketika program melakukan pelanggaran tertentu, misalnya membaca array melewati batasnya atau meluapkan bilangan bulat bertanda. Keadaan ini disebut undefined behavior (perilaku tak terdefinisi). Konsekuensinya bukan selalu crash yang mudah dikenali. Program dapat memberikan hasil yang salah diam-diam, tampak berjalan normal, atau berubah perilakunya ketika opsi optimasi diubah. Topik ini merangkum katalog undefined behavior yang paling sering terjadi, menampilkan peringatan compiler sebagai lini pertama pemeriksaan, lalu memperkenalkan sanitizer UBSan dan AddressSanitizer yang menemukan pelanggaran pada saat program berjalan.
Katalog undefined behavior yang paling sering#
Pelanggaran yang tercatat pada standar berjumlah ratusan butir, tetapi kategori berikut menyumbang sebagian besar kasus pada program C sehari-hari. Tabel berikut merangkum kategori yang paling sering ditemui beserta akibat yang lazim menyertainya.
| Jenis pelanggaran | Contoh | Akibat yang lazim |
|---|---|---|
| Signed integer overflow | INT_MAX + 1 | hasil melingkar atau hilang saat optimasi |
| Akses di luar batas array | nilai[3] pada int nilai[3] | data rusak diam-diam atau crash |
| Dereference NULL | *p saat p berisi NULL | crash dengan kode keluar 139 |
| Pergeseran melebihi lebar tipe | 1 << 40 pada int | hasil tidak terjamin |
Pelanggaran kontrak free | membebaskan blok yang sama dua kali | kerusakan heap yang sulit dilacak |
Dua kategori pertama cukup dituliskan dalam tiga baris, dan keduanya sering lolos karena program yang memuatnya tetap bisa menghasilkan keluaran yang kelihatan benar. Potongan berikut memperlihatkan keduanya sekaligus.
// tidak-dijalankan
int nilai[3] = {10, 20, 30};
int lewat = nilai[3]; /* akses di luar batas array */
int besar = INT_MAX + 1; /* signed integer overflow, butuh limits.h */Pelanggaran semacam ini berbahaya justru karena standar tidak mewajibkan pesan error apa pun. Compiler bebas menganggap kasus tersebut tidak pernah terjadi, sehingga optimasi boleh membuang cabang kode yang seharusnya berjalan, menyusun ulang pernyataan, atau menyimpan hasil perhitungan yang berbeda antar-build. Program yang diuji pada satu mesin dengan satu flag tidak memberi jaminan apa pun tentang perilakunya pada mesin lain. Kejadian nyata dari kategori terakhir dibahas pada modul Memori Dinamis.
Peringatan compiler sebagai lini pertama#
Banyak pelanggaran terdeteksi sebelum program dijalankan, tanpa alat tambahan apa pun. Program berikut berjalan normal dan mencetak hasil yang benar, padahal menyimpan dua cacat sekaligus, yaitu variabel yang tidak pernah digunakan dan fungsi yang punya jalur tanpa nilai kembali.
#include <stdio.h>
int selisih(int a, int b)
{
if (a >= b) {
return a - b;
}
/* jalur b >= a lupa mengembalikan nilai */
}
int main(void)
{
int sisa;
printf("Selisih: %d\n", selisih(10, 4));
return 0;
}Selisih: 6Kompilasi tanpa opsi tambahan tidak menghasilkan pesan apa pun, sehingga kedua cacat itu lolos begitu saja. Opsi -Wall menyalakan kelompok peringatan yang paling berguna, sedangkan -Wextra menambahkan kelompok yang lebih teliti, dan keduanya memunculkan pesan berikut ketika kompilasi diulang.
# mengompilasi ulang dengan peringatan yang lebih teliti
gcc -Wall -Wextra peringatan.c -o peringatan-wperingatan.c: In function ‘main’:
peringatan.c:13:9: warning: unused variable ‘sisa’ [-Wunused-variable]
13 | int sisa;
| ^~~~
peringatan.c: In function ‘selisih’:
peringatan.c:9:1: warning: control reaches end of non-void function [-Wreturn-type]
9 | }
| ^Setiap pesan menyebut berkas, baris, kolom, dan kategori peringatan, serta menampilkan baris kodenya, sehingga perbaikannya tinggal mengikuti petunjuk tersebut. Opsi -Werror menaikkan status setiap peringatan menjadi error, sehingga kompilasi tidak menghasilkan program sampai semua peringatan dituntas.
# mengompilasi dengan peringatan diperlakukan sebagai error
gcc -Wall -Wextra -Werror peringatan.c -o peringatan-e
echo "kode keluar: $?"peringatan.c: In function ‘main’:
peringatan.c:13:9: error: unused variable ‘sisa’ [-Werror=unused-variable]
13 | int sisa;
| ^~~~
peringatan.c: In function ‘selisih’:
peringatan.c:9:1: error: control reaches end of non-void function [-Werror=return-type]
9 | }
| ^
cc1: all warnings being treated as errors
kode keluar: 1UBSan: pemeriksa semantik pada saat berjalan#
Peringatan compiler tidak menjangkau semuanya, misalnya overflow yang melibatkan nilai yang baru diketahui saat program berjalan. UBSan, singkatan dari UndefinedBehaviorSanitizer, menyisipkan pemeriksaan runtime melalui opsi -fsanitize=undefined, sehingga pelanggaran dilaporkan lengkap dengan berkas, baris, dan kolomnya tepat ketika terjadi. Program berikut melakukan overflow pada INT_MAX, dan berkasnya tidak dijalankan langsung tanpa sanitizer karena hasilnya tidak dapat dipastikan.
// tidak-dijalankan
#include <stdio.h>
#include <limits.h>
int main(void)
{
int besar = INT_MAX;
int hasil = besar + 1;
printf("Hasil: %d\n", hasil);
return 0;
}Kompilasi dengan -fsanitize=undefined lalu menjalankan programnya memberikan laporan berikut, dan program tetap berjalan sampai selesai setelah laporan itu. Perhatikan urutan dua baris pada keluaran, karena laporan runtime muncul lebih dahulu dibanding cetakan programnya.
# mengompilasi dan menjalankan dengan pemeriksaan UBSan
gcc -fsanitize=undefined ub-overflow.c -o ub-overflow && ./ub-overflowub-overflow.c:8:9: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'
Hasil: -2147483648Laporan tersebut menyebut lokasi pelanggaran secara persis, sedangkan baris Hasil menunjukkan bahwa program dibiarkan berlanjut setelahnya. Perilaku berlanjut ini adalah pilihan bawaan UBSan, dan opsi -fno-sanitize-recover=all mengubahnya menjadi berhenti pada pelanggaran pertama agar tidak ada laporan yang terlewat. Pemeriksaan UBSan mencakup semantik bahasa seperti overflow dan pergeseran, tetapi tidak memeriksa alamat memori, dan pekerjaan itu menjadi bagian AddressSanitizer berikutnya.
AddressSanitizer: pemeriksa memori pada saat berjalan#
AddressSanitizer, yang diaktifkan melalui -fsanitize=address, memeriksa setiap akses memori terhadap batas bloknya, termasuk alokasi heap dari malloc. Program berikut menulis satu elemen di luar blok yang dialokasikan. Berkasnya kembali tidak dijalankan langsung karena laporannya memuat alamat memori serta nomor proses yang berubah pada setiap eksekusi.
// tidak-dijalankan
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int *nilai = malloc(3 * sizeof(int));
if (nilai == NULL) {
return 1;
}
nilai[3] = 99; /* menulis di luar blok 12 byte */
printf("Terisi\n");
free(nilai);
return 0;
}# tidak-dijalankan
# laporan AddressSanitizer memuat alamat dan PID, sehingga pola berikut dijalankan pada mesin sendiri
gcc -fsanitize=address oob-heap.c -o oob-heap && ./oob-heapPada program ini, AddressSanitizer menghentikan eksekusi dan mencetak laporan yang dimulai dengan baris ERROR: AddressSanitizer: heap-buffer-overflow diikuti alamat yang diakses serta keterangan WRITE of size 4. Bagian laporan berikutnya menampilkan jejak pemanggilan (stack trace) bernomor #0, #1, dan seterusnya yang menunjuk berkas dan baris, lalu diakhiri ringkasan berawalan SUMMARY. Berbeda dari laporan UBSan yang memuat angka pasti, laporan ini tidak dapat ditempel sebagai keluaran yang stabil. Isi petunjuknya tetap sama, yaitu jenis pelanggaran dan lokasi baris penyebabnya. Kedua sanitizer dapat digabungkan dalam satu kompilasi melalui -fsanitize=address,undefined, dan kebiasaan menyertakannya pada build pengujian menutup celah yang tidak terjangkau peringatan compiler.
Kesalahan umum#
- Menganggap satu eksekusi yang benar sebagai bukti kode sehat. Undefined behavior dapat tidak berakibat apa pun pada satu compiler, satu opsi, atau satu mesin, tetapi jaminannya sudah hilang. Periksa dengan flag peringatan dan sanitizer, bukan dengan satu kali jalankan.
- Menumpaskan peringatan alih-alih memperbaikinya. Menambahkan cast atau
(void)membuat pesan hilang sementara penyebabnya tetap ada. Perbaiki penyebabnya, atau tuliskan komentar yang menjelaskan mengapa baris tersebut aman. - Mengaktifkan sanitizer hanya menjelang rilis. Pelanggaran yang menumpuk sepanjang pengembangan menjadi sulit dipisahkan satu sama lain. Sertakan
-fsanitize=address,undefinedpada build pengujian harian, dan pisahkan build itu dari build rilis yang membutuhkan kecepatan. - Menindaklanjuti laporan kedua sebelum yang pertama. Pelanggaran memori dapat memicu pelanggaran berikutnya, sehingga laporan di belakangnya sering hanya akibat. Perbaiki laporan pertama, kompilasi ulang, lalu jalankan kembali.
Ringkasan#
- Undefined behavior menghapus semua jaminan standar atas perilaku program, dan bentuk akibatnya berkisar dari hasil salah diam-diam sampai crash.
- Opsi
-Wall -Wextramemunculkan peringatan berisi berkas dan baris, sedangkan-Werrormenghentikan kompilasi sampai semua peringatan dituntas. - UBSan melaporkan pelanggaran semantik seperti overflow saat program berjalan, sedangkan AddressSanitizer memeriksa batas setiap akses memori.
- Kedua sanitizer saling melengkapi dan dapat digabungkan melalui
-fsanitize=address,undefinedpada build pengujian.
Referensi#
- Warning Options — GCC, the GNU Compiler Collection
— Daftar
-Wall,-Wextra, dan-Werrorbeserta kategori peringatan yang dicakupnya. - Instrumentation Options — GCC, the GNU Compiler Collection
— Opsi
-fsanitize=addressdan-fsanitize=undefinedbeserta pemeriksaan runtime masing-masing. - Undefined behavior — cppreference.com — Katalog perilaku tak terdefinisi pada standar C beserta kategorinya.