Undefined Behavior (UB) adalah situasi di mana standar C tidak memberi batasan apa pun pada perilaku program. Compiler bebas melakukan apa saja: menghasilkan hasil acak, crash, menghapus kode yang “tidak mungkin” terjadi, atau bahkan memformat hard disk secara teoretis. Berbeda dari implementation-defined (yang didokumentasikan) dan unspecified (yang memilih dari opsi valid), UB tidak memiliki opsi valid—program menjadi tidak terdefinisi sepenuhnya. Pada chapter ini kita akan memeriksa UB paling umum—signed overflow, null dereference, use-after-free—dan memakai UBSan (Undefined Behavior Sanitizer) untuk mendeteksinya saat runtime.
Signed Overflow adalah UB #
Operasi aritmatika pada signed int yang meluap adalah UB. Misalnya INT_MAX + 1 secara matematis tidak muat di int—standar tidak menentukan hasilnya. Compiler bebas memilih apakah akan wrap-around (two’s complement), memunculkan trap, atau mengoptimasi berdasarkan asumsi “overflow tidak terjadi”.
#include <stdio.h>
#include <limits.h>
int main(void) {
/* Signed overflow: UB */
int a = INT_MAX;
printf("INT_MAX = %d\n", a);
int b = a + 1;
printf("INT_MAX + 1 = %d (UB: signed overflow)\n", b);
/* Unsigned overflow: well-defined (modulo arithmetic) */
unsigned int u = 0;
printf("\nu = 0\n");
u = u - 1;
printf("u - 1 = %u (OK: unsigned wrap-around)\n", u);
/* Null pointer dereference: UB (jangan diaktifkan) */
int *p = NULL;
(void)p; /* supaya tidak warning unused */
/* *p = 42; -- jangan diaktifkan, akan crash */
return 0;
}Output yang Dihasilkan:
INT_MAX = 2147483647
INT_MAX + 1 = -2147483648 (UB: signed overflow)
u = 0
u - 1 = 4294967295 (OK: unsigned wrap-around)Pada output, INT_MAX + 1 = -2147483648—GCC memilih two’s complement wrap-around. Namun ini BUKAN jaminan. Compiler lain dapat memilih trap (crash), atau lebih buruk lagi: compiler dapat berasumsi a + 1 > a selalu true untuk int signed (karena compiler mengasumsikan tidak ada UB), lalu mengoptimalkan loop bound check yang mengandalkan perilaku overflow.
Berbeda dengan unsigned int, overflow pada unsigned adalah well-defined—standar menetapkan modulo arithmetic 2^N. u - 1 ketika u = 0 menghasilkan UINT_MAX (4294967295 pada 32-bit), bukan UB.
Mari kita telaah baris-baris kode di atas:
- Baris 7 (
int a = INT_MAX;):a= 2147483647, nilai maksimumint32-bit. - Baris 9 (
int b = a + 1;): penambahanINT_MAX + 1meluap—UB. Pada GCC x86-64, hasil terlihat-2147483648karena two’s complement, tetapi tidak dijamin. - Baris 14 (
unsigned int u = 0;): variabelunsigned. Overflow didefinisikan sebagai modulo arithmetic. - Baris 16 (
u = u - 1;):0 - 1padaunsigned int32-bit menghasilkanUINT_MAX = 4294967295. Bukan UB—standar mendefinisikan ini. - Baris 19 (
int *p = NULL;): pointerNULL. Dereferensi*padalah UB—kernel akan mengirimSIGSEGV. Baris ini hanya mendeklarasikan pointer tanpa dereferensi.
Use-After-Free dan Buffer Overflow #
Bug memori paling berbahaya di C adalah use-after-free, double-free, buffer overflow, dan null dereference. Semua adalah UB—program mungkin crash, mungkin juga terus berjalan dengan data rusak yang dapat dieksploitasi untuk serangan keamanan.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
/* Use-after-free: UB */
int *p = malloc(sizeof(int));
if (!p) return 1;
*p = 42;
printf("sebelum free: *p = %d\n", *p);
free(p);
/* Akses setelah free adalah UB */
/* *p = 99; -- jangan diaktifkan */
/* Buffer overflow: UB */
int arr[3] = {1, 2, 3};
printf("\narr = {%d, %d, %d}\n", arr[0], arr[1], arr[2]);
/* arr[5] = 100; -- jangan diaktifkan */
/* Null deref: UB */
int *q = NULL;
if (q != NULL) {
*q = 100;
} else {
printf("\nq == NULL, tidak dereferensi (aman)\n");
}
return 0;
}Output yang Dihasilkan:
sebelum free: *p = 42
arr = {1, 2, 3}
q == NULL, tidak dereferensi (aman)Bagian penting dari contoh ini adalah kode yang tidak diaktifkan—tiga baris yang dikomentari (*p = 99, arr[5] = 100, *q = 100) adalah UB. Mengaktifkannya akan menyebabkan program berperilaku tidak terduga: crash SIGSEGV, korupsi data, atau diam-diam merusak memori yang baru saja dialokasikan. Branch if (q != NULL) di akhir menunjukkan pola defensif yang benar: selalu periksa NULL sebelum dereferensi.
Mendeteksi UB dengan Sanitizer #
GCC dan Clang menyediakan UBSan (Undefined Behavior Sanitizer) dan ASan (Address Sanitizer) untuk mendeteksi UB saat runtime. Aktifkan dengan flag -fsanitize=undefined,address.
gcc -std=c11 -Wall -Wextra -fsanitize=undefined,address -o test_uf1 test_uf1.c
./test_uf1Output dengan UBSan untuk contoh signed overflow:
test_uf1.c:8:9: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'
INT_MAX = 2147483647
INT_MAX + 1 = -2147483648 (UB: signed overflow)
...UBSan menambahkan instrumentation yang memeriksa setiap operasi aritmatika, shift, dan konversi. Bila operasi meluap, UBSan mencetak pesan diagnostik ke stderr dan (tergantung flag) melanjutkan eksekusi atau menghentikan program. ASan bekerja serupa untuk bug memori: use-after-free, double-free, buffer overflow, leak. Kombinasi keduanya mendeteksi sebagian besar UB di C.
Daftar UB Umum di C #
- Signed integer overflow:
INT_MAX + 1,INT_MIN - 1,-INT_MIN. - Shift negatif atau terlalu besar:
1 << -1,1 << 32(untukint32-bit). - Null pointer dereference:
*NULL,((struct Foo *)NULL)->field. - Use-after-free: akses pointer setelah
free. - Double-free:
free(p); free(p);. - Buffer overflow: akses
arr[i]di luar batas. - Reading uninitialized variable:
int x; printf("%d", x);. - Strict aliasing violation: cast
int *kefloat *lalu dereferensi. - Modifying string literal:
char *s = "halo"; s[0] = 'H';. - Calling function with wrong prototype: cast function pointer ke tipe yang berbeda.
- Division by zero:
int x = 1/0;. - Integer conversion ke tidak dapat direpresentasikan:
(int)1e100.