Undefined behavior (UB) adalah konstruksi kode C yang tidak memiliki perilaku terdefinisi oleh standar. Compiler bebas mengasumsikan UB tidak terjadi—dan dapat mengoptimasi kode berdasarkan asumsi tersebut. Akibatnya, kode yang “berjalan baik” hari ini bisa gagal misterius setelah perubahan flag optimasi atau upgrade compiler. Bab ini menyusun daftar UB paling berbahaya beserta cara mendeteksi dan mencegahnya.
Apa itu UB? #
Saat compiler mengoptimasi, compiler boleh mengasumsikan program tidak mengandung UB. Asumsi ini memungkinkan transformasi yang agresif—misalnya, loop for (int i = 0; i <= N; i++) dianggap tidak akan overflow, sehingga compiler dapat menghapus pengecekan batas. Konsekuensinya: kode dengan UB dapat berjalan “normal” pada -O0 tetapi rusak pada -O2.
Tabel UB Utama #
| # | Kategori | Contoh | Bahaya |
|---|---|---|---|
| 1 | Signed integer overflow | INT_MAX + 1 | Wrap-around biasa, tetapi compiler dapat menghapus cek |
| 2 | Null pointer dereference | *p = 10; dengan p = NULL | Segfault atau perilaku tak terduga |
| 3 | Use-after-free | free(p); p->x = 1; | Korupsi heap, crash asinkron |
| 4 | Double-free | free(p); free(p); | Korupsi free list allocator |
| 5 | Buffer overflow | arr[5] saat arr[3] | Korupsi stack/heap, eksekusi kode |
| 6 | Uninitialized read | int x; printf("%d", x); | Nilai tidak terduga, info leak |
| 7 | Invalid shift | x << 32 pada int 32-bit | Hasil tidak terdefinisi |
| 8 | Strict aliasing violation | float *f = (float*)&int_var; | Compiler reorder load/store |
| 9 | Modifikasi string literal | char *s = "halo"; s[0] = 'H'; | Segfault di platform read-only |
| 10 | Modifikasi objek const | (int*)&const_var = 20; | Compiler asumsikan tidak berubah |
| 11 | Sequence point violation | printf("%d %d", i++, i++); | Urutan evaluasi tidak terdefinisi |
| 12 | Out-of-bound pointer arithmetic | &arr[-1] | UB kecuali satu past end |
| 13 | Divide by zero | int r = 5 / 0; | Trap signal (SIGFPE) |
| 14 | Returning pointer to local | return &local_var; | Stack sudah di-unwind |
| 15 | Modifying literal in strtok | strtok("a,b", ","); | String literal read-only |
| 16 | Negative size_t to malloc | malloc((size_t)-1); | Bisa alokasi 0 atau fail |
| 17 | Comparison memcmp antar struct padding | padding berisi nilai sampah | Hasil perbandingan tidak deterministik |
| 18 | Function call tanpa prototipe | int r = strlen(s); tanpa #include <string.h> | Tipe parameter assumed int |
Demonstrasi UB dan Padaban Aman #
Program berikut mendemonstrasikan beberapa UB yang dapat dijalankan dengan aman (outputnya biasanya normal, tetapi tidak dijamin) dan padanannya yang well-defined.
/* File: ub1.c - Demonstrasi UB dan padanan amannya */
#include <stdio.h>
#include <limits.h>
#include <stdint.h>
int main(void) {
/* Kasus 1: Signed integer overflow (UB) */
int a = INT_MAX;
printf("1. INT_MAX + 1 = %d (UB, biasa wrap ke negatif)\n", a + 1);
/* Kasus 2: Unsigned overflow (well-defined wrap) */
unsigned u = 0;
printf("2. unsigned 0 - 1 = %u (well-defined)\n", u - 1);
/* Kasus 3: char* aliasing (selalu legal) */
int n = 0x41414141;
unsigned char *cp = (unsigned char*)&n;
printf("3. byte ke-0 n = %d (char* aliasing OK)\n", cp[0]);
/* Kasus 4: Union type punning (legal di C99+) */
union { uint32_t i; float f; } un;
un.i = 0x40490FDBu;
printf("4. union punning 0x40490FDB -> float = %f (legal)\n", un.f);
return 0;
}Compile dan jalankan dalam tiga mode untuk membandingkan:
# Tanpa sanitasi
gcc -std=c11 -Wall -Wextra -O0 -o ub1 ub1.c && ./ub1
# Dengan optimasi (UB bisa terlihat berbeda)
gcc -std=c11 -Wall -Wextra -O2 -o ub1_opt ub1.c && ./ub1_opt
# Dengan UBSan untuk deteksi UB
gcc -std=c11 -Wall -Wextra -fsanitize=undefined -o ub1_ubsan ub1.c && ./ub1_ubsanOutput yang Dihasilkan (-O0 dan -O2 identik di sini, tetapi tidak dijamin):
1. INT_MAX + 1 = -2147483648 (UB, biasa wrap ke negatif)
2. unsigned 0 - 1 = 4294967295 (well-defined)
3. byte ke-0 n = 65 (char* aliasing OK)
4. union punning 0x40490FDB -> float = 3.141593 (legal)Output dengan UBSan (stderr):
ub1.c:9:5: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'Mari kita telaah baris-baris kode di atas:
- Baris 9 (
a + 1dengana = INT_MAX): Ini adalah UB—signed integer overflow. Output-2147483648adalah perilaku wrap-around yang umum pada arsitektur two’s complement, tetapi standar tidak menjaminnya. Compiler yang melihatif (a + 1 > a)dapat mengoptimasi menjaditrueselalu—yang akan membuat pengecekan overflow tidak berfungsi. - Baris 13 (
u - 1denganu = 0): Untuk unsigned, overflow didefinisikan sebagai wrap-around modulo2^N. Hasil0 - 1selaluUINT_MAX—tidak ada UB. - Baris 17 (
(unsigned char*)&n): Aturan strict aliasing melarang akses objek via pointer tipe yang tidak kompatibel. Pengecualian:char*,signed char*, danunsigned char*boleh alias objek tipe apapun. Inilah cara aman untuk inspeksi byte per byte. - Baris 22 (
union { uint32_t i; float f; }): Type punning via union adalah UB di C++ tetapi legal di C99+. Ketika satu field di-read setelah field lain ditulis, compiler membaca representasi bit yang sama. Cara ini lebih aman daripada*(float*)&int_varyang melanggar strict aliasing.
Deteksi dengan UBSan #
UBSan adalah tool bawaan GCC/Clang yang menginstrumentasi binary untuk mendeteksi UB saat runtime. Aktifkan dengan -fsanitize=undefined (kombinasi banyak check) atau check spesifik:
| Flag UBSan | Deteksi |
|---|---|
-fsanitize=signed-integer-overflow | Signed overflow |
-fsanitize=unsigned-integer-overflow | Unsigned overflow (tidak UB, tapi sering bug) |
-fsanitize=shift | Shift terlalu besar atau negatif |
-fsanitize=null | Null pointer dereference |
-fsanitize=alignment | Akses unaligned |
-fsanitize=bounds | Akses array out-of-bound (variable-length array) |
-fsanitize=vla-bound | VLA dengan ukuran non-positif |
-fsanitize=object-size | Akses ke objek lebih kecil dari yang diperkirakan |
-fsanitize=pointer-overflow | Pointer arithmetic overflow |
-fsanitize=undefined | Kombinasi semua di atas |
Strategi Pencegahan UB #
| Strategi | Implementasi |
|---|---|
| Gunakan tipe unsigned jika nilai tidak negatif | size_t untuk ukuran, uint32_t untuk ID |
Pakai INT64_MAX check sebelum tambah | if (a > INT64_MAX - b) ... |
| Inisialisasi semua variabel saat deklarasi | int x = 0; bukan int x; |
Hindari realloc(p, 0) untuk “free” | Pakai free(p); p = NULL; |
Set pointer ke NULL setelah free | free(p); p = NULL; |
Hindari strtok untuk string konstan | Salin dulu ke buffer mutable |
Pakai memcpy untuk type punning, bukan cast pointer | memcpy(&f, &i, sizeof f); |
Verifikasi malloc tidak return NULL | if (!p) return -1; |
Aktifkan -fanalyzer saat testing | GCC statik analyzer deteksi use-after-free |
| Pakai ASan + UBSan untuk testing | -fsanitize=address,undefined |
Daftar Bacaan #
- ISO/IEC 9899:2011 Annex J.2 — daftar lengkap UB
- cppreference.com/w/c/language/behavior — kategori UB
- clang.llvm.org/docs/UndefinedBehaviorSanitizer.html — dokumentasi UBSan
- blog.regehr.org — John Regehr menulis banyak tentang UB C