Blok heap tidak memiliki pembersih otomatis, sehingga kesalahan pengelolaannya jarang terlihat pada saat kode ditulis dan baru terasa ketika program berjalan lama atau berperilaku aneh. Topik ini membahas tiga bentuk kesalahan yang paling sering muncul. Kebocoran memori (memory leak) terjadi ketika alamat blok hilang, double free ketika satu blok dibebaskan dua kali, dan use-after-free ketika blok yang sudah dibebaskan masih digunakan. Setiap bentuk dibahas bersama contohnya dan kebiasaan yang mencegahnya.
Kebocoran: alamat yang tertimpa#
Kebocoran memori terjadi ketika sebuah blok masih terpasang di heap, tetapi tidak ada pointer yang menyimpan alamatnya. Blok itu tidak dapat digunakan karena alamatnya tidak diketahui, dan tidak dapat dibebaskan karena free menuntut alamat yang sah. Penyebabnya hampir selalu satu, yaitu satu-satunya pointer yang menunjuk blok ditimpa atau hilang sebelum blok tersebut dibebaskan. Memori yang bocor tetap terpakai sampai program berakhir, dan pada program yang berjalan lama seperti server, kebocoran yang kecil dapat menumpuk menjadi kehabisan memori.
Program berikut memuat kebocoran yang paling sederhana sekaligus paling sering terjadi. Blok dipesan dua kali melalui pointer nilai tanpa pembebasan di antaranya, sehingga blok pertama kehilangan satu-satunya pencatat alamat.
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int *nilai = malloc(100 * sizeof(int));
if (nilai == NULL) {
return 1;
}
nilai[0] = 7;
/* alamat blok pertama tertimpa tanpa dibebaskan lebih dahulu */
nilai = malloc(100 * sizeof(int));
if (nilai == NULL) {
return 1;
}
nilai[0] = 8;
printf("nilai[0] = %d\n", nilai[0]);
printf("Blok pertama berukuran %zu byte dan tidak pernah dibebaskan.\n", 100 * sizeof(int));
free(nilai);
return 0;
}nilai[0] = 8
Blok pertama berukuran 400 byte dan tidak pernah dibebaskan.Tidak ada pesan error dan program berakhir normal, tetapi 400 byte tetap terpasang tanpa pemilik sampai sistem operasi membersihkan seluruh memori program saat berakhir. Diagram berikut memperlihatkan keadaan pointer pada saat penimpaan terjadi, yaitu blok pertama masih terpasang sementara alamatnya sudah tidak disimpan oleh pointer mana pun.
Perbaikannya sesederhana penyebabnya, yaitu bebaskan blok lama sebelum alamat blok ditimpa, atau simpan setiap alamat pada pointer terpisah bila keduanya masih diperlukan. Pembebasan terlebih dahulu tidak merusak apa pun karena blok lama sudah selesai dipakai pada saat penimpaan dilakukan. Apabila isi blok lama masih dibutuhkan, salin dulu nilainya ke blok baru sebelum alamat lamanya dilepas, sehingga urutan simpan, ganti, lalu bebas menjadi pola yang aman.
Skenario kebocoran yang lazim#
Kebocoran jarang tampak sejelas contoh di atas. Pada kode nyata, blok biasanya hilang lewat salah satu dari tiga skenario berikut, dan ketiganya sama-sama berakar pada alamat yang tidak terjaga.
- Pointer ditimpa tanpa pembebasan. Bentuknya persis seperti program contoh, termasuk varian yang lebih samar di dalam perulangan, yaitu satu alokasi per putaran tanpa free, sehingga kebocorannya menumpuk selama program berjalan.
- Jalur keluar yang melompati free. Fungsi yang memesan beberapa blok lalu kembali lebih awal karena error menyisakan blok-blok yang belum dibebaskan. Setiap jalur kembali pada fungsi tersebut harus melewati pembebasan yang sama.
- Alamat hanya tersimpan di tempat yang tergantikan. Alamat blok disimpan pada variabel lokal yang ditimpa di tengah fungsi, atau pada daftar yang elemennya dihapus tanpa membebaskan isinya terlebih dahulu.
Ketiga skenario menunjukkan pola yang sama, yaitu tidak ada satu pihak yang jelas bertanggung jawab atas setiap alamat. Aturan kepemilikan yang mengiringi keluarga malloc memberikan solusinya, yakni setiap blok memiliki satu pemilik yang pasti membebaskannya pada semua jalur eksekusi.
Pointer dangling dan use-after-free#
Setelah free dijalankan, blok sudah dikembalikan kepada pustaka standar, tetapi nilai pointer tidak berubah dan tetap menyimpan alamat lama. Pointer dalam keadaan semacam itu disebut pointer menggantung (dangling pointer), dan setiap pembacaan atau penulisan melalui alamatnya disebut use-after-free. Perilakunya tidak terdefinisi oleh standar C, sehingga hasilnya dapat berupa nilai yang masih tampak benar, nilai yang sudah tertimpa data lain, atau program yang berhenti mendadak.
// tidak-dijalankan
int *data = malloc(sizeof(int));
if (data == NULL) {
return 1;
}
*data = 55;
free(data);
return *data; /* membaca alamat yang sudah dikembalikan: tidak terdefinisi */Contoh di atas sengaja tidak dijalankan dan keluarannya tidak ditampilkan karena hasilnya memang tidak dapat diandalkan. Pada satu eksekusi angka 55 mungkin masih terbaca utuh, sedangkan pada eksekusi lain blok itu sudah dipakai oleh pemesanan baru. Sifat samar inilah yang membuatnya berbahaya, karena kesalahannya bisa lolos pengujian lalu muncul di lingkungan yang berbeda. Alat seperti Valgrind dan sanitizer milik kompilator dirancang khusus untuk menangkapnya secara pasti.
Pointer menggantung juga dapat terbentuk tanpa sengaja di tengah program yang panjang, misalnya ketika sebuah blok dibebaskan di dalam perulangan sementara pointer yang sama masih dipakai pada putaran berikutnya. Kebiasaan menugaskan NULL pada pointer segera setelah pembebasannya membuat kondisi ini terdeteksi lebih cepat, karena alamat kosong langsung menghentikan program alih-alih diam-diam membaca memori yang sudah tidak dimiliki program. Deteksi dini seperti ini jauh lebih murah daripada menelusuri kerusakan data yang penyebabnya sudah jauh tertinggal.
Double free#
Double free adalah pemanggilan free dua kali pada alamat blok yang sama. Blok pertama sudah dikembalikan kepada pustaka standar, sehingga pembebasan kedua memerintahkan pustaka melepaskan sesuatu yang sudah tidak dimiliki program. Akibatnya tidak terdefinisi, tetapi pustaka standar GNU C mengenali pola ini pada kasus yang umum dan menghentikan program dengan pesan yang jelas.
Program berikut memuat kesalahan tersebut. Programnya sengaja dibuat untuk dihentikan oleh pustaka standar, sehingga eksekusinya diperlihatkan lewat perintah terminal di bawahnya.
// tidak-dijalankan
#include <stdio.h>
#include <stdlib.h>
static void lepas(void *p)
{
free(p);
}
int main(void)
{
int *data = malloc(sizeof(int));
if (data == NULL) {
return 1;
}
*data = 55;
printf("Sebelum free: *data = %d\n", *data);
fflush(stdout);
lepas(data);
lepas(data); /* kesalahan: dibebaskan dua kali */
return 0;
}# kompilasi program yang mengandung double-free
gcc double-free.c -o double-free
# jalankan lalu tampung keluarannya ke berkas agar pesan errornya utuh
# (baris Aborted yang ditambahkan shell memuat nomor proses sehingga tidak ikut ditampilkan)
{ ./double-free > keluaran.txt 2>&1; } 2>/dev/null
# tampilkan isi berkas keluaran
cat keluaran.txtSebelum free: *data = 55
free(): double free detected in tcache 2Pesan free(): double free detected in tcache 2 berasal dari pustaka standar GNU C yang mencatat blok yang baru saja dilepas, sehingga pembebasan kedua dapat dikenali dan program dihentikan sebelum kerusakannya meluas. Perlindungan seperti ini membantu, tetapi jangan diandalkan sebagai jaminan, karena standar C hanya menyatakan perilakunya tidak terdefinisi dan pustaka lain boleh berperilaku berbeda. Satu-satunya pertahanan yang benar adalah memastikan setiap blok dibebaskan tepat satu kali.
Penyebab yang paling sering di balik double free adalah kepemilikan yang tidak jelas, misalnya dua fungsi sama-sama dianggap bertanggung jawab membebaskan blok yang sama. Situasi lain yang lazim adalah pembebasan pada dua jalur error yang berpotensi dijalankan berurutan, atau pointer yang dimasukkan ke dua daftar sekaligus sehingga pembersihan daftarnya saling bertabrakan. Semua bentuknya kembali ke aturan yang sama, yaitu satu blok hanya dimiliki satu pihak, dan pembebasan hanya dilakukan oleh pemiliknya.
Kebiasaan pencegahan#
Ketiga kesalahan di atas berbagi akar yang sama, yaitu alamat yang tidak dikelola dengan disiplin, sehingga pencegahannya juga berupa kebiasaan yang sama.
- Tetapkan satu pemilik untuk setiap blok. Tentukan sejak desain siapa yang wajib membebaskan setiap blok, dan catat aturan itu pada komentar fungsi yang mengoper alamat. Pemindahan kepemilikan boleh dilakukan asalkan penerimanya jelas.
- Pasangkan pemesanan dengan pembebasan pada jalur yang sama. Setiap malloc dan free sebaiknya berada pada fungsi yang sama atau pada pasangan fungsi yang sengaja diberi nama berpasangan, sehingga audit visualnya mudah. Jalur kembali karena error tetap harus melewati pembebasan.
- Tugaskan
NULLpada pointer setelah pembebasan. Penugasandata = NULL;setelah free membuat pembebasan kedua menjadi free padaNULLyang aman, sedangkan pemakaian yang tersisa cepat terlihat karena mengakses alamat kosong langsung menghentikan program. - Periksa program dengan alat khusus secara berkala. Kebocoran yang tidak terlihat pada satu eksekusi kecil dapat ditemukan pasti oleh alat analisis memori, dan pemeriksaannya cukup dilakukan pada setiap perubahan besar.
Ringkasan#
- Kebocoran memori terjadi ketika satu-satunya alamat sebuah blok hilang, sehingga blok tetap terpasang namun tidak terjangkau.
- Pointer menggantung masih menyimpan alamat blok yang telah dibebaskan, dan pemakaiannya berperilaku tidak terdefinisi.
- Double free membebaskan blok yang sama dua kali, dan pustaka GNU C menghentikan program ketika polanya dikenali.
- Satu pemilik per blok, pemasangan free pada semua jalur kembali, dan penugasan
NULLsetelah pembebasan mencegah ketiganya.
Referensi#
- malloc(3) — Linux man-pages project — Kontrak free beserta catatan perilaku yang tidak terdefinisi saat dipanggil ulang.
- C memory management library — cppreference.com — Halaman tiap fungsi alokasi memori beserta syarat pemakaiannya.