Bug pointer jarang terlihat pada baris yang salah, karena gejalanya sering muncul jauh dari penyebabnya. Program dapat berhenti mendadak dengan pesan singkat, dapat berjalan normal sekalipun rusak, atau berperilaku berbeda pada setiap eksekusi. Topik ini merangkum tiga penyebab crash yang paling lazim, yaitu pointer dangling, pengembalian alamat variabel lokal, dan dereference NULL, lalu melatih membaca pesan crash beserta kode keluarnya.
Pointer dangling setelah free#
Pointer dangling adalah pointer yang masih menyimpan alamat memori yang sudah tidak dimilikinya lagi. Kasus yang paling sering terjadi setelah free, ketika memori dikembalikan kepada pengelola heap tetapi variabel pointernya masih diisi alamat lama. Membaca atau menulis lewat pointer tersebut adalah penggunaan setelah pembebasan (use after free). Akibatnya tidak dapat diprediksi, mulai dari program tampak normal, data yang rusak diam-diam, hingga crash di tempat yang jauh dari baris penyebab.
// tidak-dijalankan
int *data = malloc(3 * sizeof *data);
free(data);
/* *data = 5; tidak sah: memori telah dikembalikan ke pengelola heap */Potongan di atas sengaja tidak dijalankan karena hasilnya tidak dapat direproduksi secara adil. Kebiasaan pencegahannya sederhana, yaitu mengisi data = NULL; segera setelah free, sehingga penggunaan lanjutan terdeteksi oleh pemeriksaan NULL, bukan berlalu diam-diam. Skenario lain dari bug yang sama dibahas pada modul Memori Dinamis beserta perangkat pemeriksa kebocoran memori.
Alamat variabel lokal yang dikembalikan#
Jebakan kedua terjadi ketika fungsi mengembalikan alamat variabel lokalnya. Variabel lokal lahir saat fungsi dipanggil dan mati saat fungsi kembali, sehingga alamat yang dikembalikan menunjuk memori yang isinya tidak lagi dijamin. Program yang menggunakan hasilnya dapat kebetulan mencetak nilai yang benar, tetapi kebetulan itu hilang begitu pola pemanggilan berubah.
// tidak-dijalankan
int *ambil_alamat(void)
{
int lokal = 5; /* masa hidup berakhir saat fungsi kembali */
return &lokal; /* alamat tidak lagi sah untuk dipakai */
}
/* int *p = ambil_alamat(); *p tidak lagi terjamin isinya */Kompilator sudah membantu mendeteksi pola ini sebelum program berjalan. Mengompilasi potongan tersebut dengan gcc -Wall -Wextra memunculkan peringatan warning: function returns address of local variable [-Wreturn-local-addr]. Peringatan semacam ini wajib ditindaklanjuti, bukan diabaikan. Alternatif yang benar bergantung kebutuhan, yaitu mengembalikan nilai biasa, menerima alamat keluaran dari pemanggil sebagaimana pola pass by reference, atau mengalokasikan memori yang masa hidupnya dikendalikan oleh program.
Dereference NULL dan kode keluar 139#
Dereference NULL adalah crash yang paling mudah direproduksi, karena alamat 0 memang sengaja dipetakan sebagai tidak dapat diakses. Program berikut mencetak satu baris terlebih dahulu, lalu membaca isi pointer NULL, dan berhenti sebelum pernyataan return tercapai.
// tidak-dijalankan
#include <stdio.h>
#include <stddef.h>
int main(void)
{
printf("Program berjalan, segera melakukan dereference NULL\n");
fflush(stdout);
int *p = NULL;
return *p;
}Program ini tidak dijalankan langsung oleh verifikator karena pesan crash bercampur dengan nomor proses yang berubah setiap eksekusi. Blok berikut mengompilasi berkas yang sama dari bundle, menyembunyikan pesan bawaan shell sejenak, lalu mencatat kode keluar program.
# kompilasi program demo dari berkas di bundle
gcc nol-demo.c -o nol-demo
# pesan crash bawaan shell disembunyikan sementara agar keluarannya murni
exec 2>/dev/null
./nol-demo
status=$?
exec 2>&1
echo "kode keluar: $status"Program berjalan, segera melakukan dereference NULL
kode keluar: 139Pada terminal biasa, shell juga mencetak baris berisi kata Segmentation fault diikuti nama program, misalnya Segmentation fault ./nol-demo. Pada banyak sistem, keterangan core dumped ditambahkan apabila cadangan inti diizinkan. Angka 139 adalah hasil rumus 128 ditambah nomor sinyal, yaitu SIGSEGV bernomor 11. Kaidah 128 + nomor sinyal inilah kunci membaca hampir semua kode keluar crash di terminal.
Keterangan core dumped menyatakan bahwa isi memori program saat mati sempat dituliskan ke berkas untuk diperiksa kemudian, dan keterangan itu hilang apabila sistem menonaktifkan pembuatan cadangan inti. Berkas cadangan tersebut bermanfaat ketika program diperiksa dengan debugger, dan kehadirannya dikendalikan oleh pengaturan sistem, bukan oleh program itu sendiri. Untuk tujuan pembelajaran, nama sinyal dan kode keluar sudah cukup untuk mengarahkan penyelidikan.
Mencegah dengan pemeriksaan NULL#
Crash NULL seharusnya tidak pernah terjadi pada program yang sehat, karena pointer yang mungkin berisi NULL dapat diperiksa terlebih dahulu. Pola yang sama berlaku untuk nilai kembalian fungsi yang bisa gagal, sebagaimana cari pada topik deklarasi pointer. Program berikut menuntaskan pembacaan pointer dengan nilai pengganti apabila pointer yang diterima tidak sah.
#include <stdio.h>
#include <stddef.h>
/* Membaca isi p secara aman, dengan nilai pengganti bila p NULL. */
int aman_baca(const int *p, int pengganti)
{
if (p == NULL) {
return pengganti;
}
return *p;
}
int main(void)
{
int nilai = 42;
int *sah = &nilai;
int *rusak = NULL;
printf("aman_baca(sah, 0) : %d\n", aman_baca(sah, 0));
printf("aman_baca(rusak, 0) : %d\n", aman_baca(rusak, 0));
return 0;
}aman_baca(sah, 0) : 42
aman_baca(rusak, 0) : 0Pemeriksaan berada di satu tempat, yaitu di dalam aman_baca, sehingga pemanggil tidak perlu mengulang if yang sama. Pada program yang lebih besar, letak pemeriksaan dipilih di batas antarmuka, yaitu di awal fungsi yang menerima pointer dari luar, sedangkan fungsi internal boleh mensyaratkan pointer yang sah melalui dokumentasinya. Dengan pembagian tanggung jawab seperti ini, pemeriksaan tidak tersebar acak dan mudah diaudit.
Membaca pesan crash secara sistematis#
Pesan crash memberi tiga informasi sekaligus, yaitu nama sinyal, nama program yang mati, dan kode keluar pada saat diperiksa. Langkah membacanya dapat diringkas sebagai berikut. Catat nama sinyal, cocokkan kode keluar dengan rumus 128 + nomor sinyal, lalu telusuri pointer yang sedang digunakan di sekitar lokasi yang terakhir dicetak program.
| Kode keluar | Sinyal | Penyebab yang paling sering |
|---|---|---|
| 139 | SIGSEGV (11) | Akses memori pada alamat yang tidak sah |
| 136 | SIGFPE (8) | Kesalahan aritmetika, misalnya pembagian dengan nol |
| 134 | SIGABRT (6) | Pembatalan oleh program sendiri, misalnya kegagalan assert |
Setelah sinyal diketahui, penyelidikan bergerak dari gejala ke penyebab. Langkahnya meliputi reproduksi crash dengan masukan yang sama, penambahan cetakan sementara di sekitar lokasi terakhir, dan pemeriksaan setiap pointer yang terlibat. Kebiasaan membaca kode keluar juga mempercepat komunikasi ketika bug dilaporkan orang lain, karena penyebab awal langsung terbaca dari angka kode keluar. Modul C Profesional melanjutkan alur ini dengan debugger GDB dan pemeriksa memori AddressSanitizer, yang mampu menunjuk baris penyebab secara langsung.
Ringkasan#
- Pointer dangling muncul ketika pointer masih memegang alamat yang telah dikembalikan lewat
free, dan kebiasaan mengisi NULL setelah pembebasan membuat bug tersebut terdeteksi. - Mengembalikan alamat variabel lokal menghasilkan pointer yang sah hanya sampai fungsi kembali, dan GCC menandainya dengan peringatan
-Wreturn-local-addr. - Dereference NULL menghentikan program dengan
SIGSEGV, terbaca dari kode keluar 139 yang mengikuti rumus 128 + nomor sinyal.
Referensi#
- signal(7) — Linux manual page — Daftar sinyal Linux, termasuk SIGSEGV yang menjadi dasar kode keluar 139.
- Warning Options — GCC, the GNU Compiler Collection
— Opsi
-Walldan-Wextrabeserta peringatan pengembalian alamat variabel lokal.