Bug memori

Kebocoran, double-free, use-after-free, dan cara membaca keluaran crash.

Daftar isi

Bug memori adalah kelompok kesalahan yang muncul ketika program mengelola memori dinamis secara manual, dan gejalanya sering muncul jauh dari penyebabnya. Kebocoran, double-free, dan use-after-free adalah tiga anggotanya yang paling sering, sementara pesan crash yang tampak samar menjadi tanda pertamanya. Topik ini merangkum masing-masing bug, cara gejalanya terbaca, dan perangkat yang membantu menemukannya.

Kebocoran memori#

Kebocoran memori (memory leak) terjadi ketika memori hasil new tidak pernah dilepas dan tidak ada pointer yang menyimpan alamatnya. Memori yang bocor tidak kembali ke sistem sampai program berakhir, dan pada program yang berjalan lama, kebocoran berulang menggerus memori yang tersedia secara perlahan. Penyebabnya umumnya bukan niat buruk, melainkan jalur eksekusi yang melompati delete, baik karena pengembalian lebih awal maupun karena exception.

cpp Contoh: kebocoran memori
// tidak-dijalankan (memori yang dialokasikan tidak pernah dilepas)
void proses() {
    int* data = new int[1000];
    // data dipakai, lalu fungsi berakhir tanpa delete[]
}  // alamatnya hilang, memori tetap tertahan sampai program berakhir

Contoh di atas membocorkan 1000 int pada setiap pemanggilan, dan pemanggilan yang berulang memperbesar kebocorannya. Kerugiannya bertumpuk, yaitu memori yang tidak kembali dan pelacakan yang sulit karena tidak ada error yang muncul pada saat kejadiannya. Alat pemindai memori, yang dibahas pada bagian akhir, melaporkan kebocoran ini secara eksplisit saat program berakhir.

Gejala kebocoran paling mudah dikenali dari pemakaian memori yang naik terus selama program berjalan. Pada program pemrosesan data yang bocor pada setiap pekerjaannya, misalnya setiap berkas, pemakaian memori membesar secara perlahan hingga sistem kehabisan memori dan bergantung pada ruang tukar (swap). Pada titik itu seluruh sistem melambat, sementara pesan error programnya tetap tidak menunjuk ke baris yang bocor.

Double-free dan use-after-free#

Double-free adalah menjalankan delete dua kali atas alamat yang sama. Pelepasan pertama mengembalikan memori ke pengelolanya, sedangkan pelepasan kedua bekerja pada memori yang sudah bukan milik program, sehingga struktur internal pengelola memori dapat rusak. Gejalanya berkisar dari kerusakan diam-diam sampai penghentian program oleh sistem, dan semua bentuknya muncul jauh setelah baris penyebabnya.

cpp Contoh: double-free
// tidak-dijalankan (melepas memori yang sama dua kali adalah undefined behavior)
void lepasDuaKali() {
    int* p = new int{1};
    delete p;
    delete p;  // perilaku tak terdefinisi
}

Use-after-free adalah membaca atau menulis memori yang sudah dilepas. Pointer yang menyimpan alamat lama masih terlihat sah, tetapi memori pada alamat itu bisa saja sudah dipakai alokasi lain, sehingga nilainya berubah tanpa sebab atau datanya rusak. Kesalahan ini sulit ditemukan karena gejalanya menyebar ke bagian program yang sama sekali tidak terkait dengan penyebabnya.

cpp Contoh: use-after-free
// tidak-dijalankan (membaca memori yang sudah dilepas adalah undefined behavior)
void bacaLagi() {
    int* p = new int{9};
    delete p;
    std::cout << *p;  // pointer masih menyimpan alamat yang sudah tidak sah
}

Kedua kesalahan ini tidak dijamin menghentikan program dengan pesan yang jelas. Runtime C++ tidak menandai setiap byte yang dilepas. Compiler yang mengoptimasi mengasumsikan kode tanpa undefined behavior saat menyusun ulang perhitungan, sehingga gejala dapat menghilang pada satu build dan muncul pada build lain. Ketiadaan crash bukan bukti ketiadaan bug, dan itulah alasan pemeriksaan dilakukan dengan alat, bukan dengan pengamatan.

Pola aman sehari-hari#

Tiga kebiasaan menutup sebagian besar celah di atas, yaitu menetralkan pointer setelah delete, memeriksa nullptr sebelum dereference, dan memastikan setiap alokasi memiliki tepat satu pelepasan. Program berikut menerapkan ketiganya pada satu siklus alokasi yang utuh.

cpp pola-aman.cpp
#include <iostream>

int main() {
    int* data = new int[3]{5, 10, 15};

    int jumlah = 0;
    for (int i = 0; i < 3; ++i) {
        jumlah += data[i];
    }
    std::cout << "Jumlah: " << jumlah << '\n';

    delete[] data;
    data = nullptr;

    if (data == nullptr) {
        std::cout << "Pointer sudah dinetralkan setelah delete.\n";
    }
    return 0;
}
text Keluaran
Jumlah: 30
Pointer sudah dinetralkan setelah delete.

Program di atas memakai memori dinamis tanpa membuka celah bug memori. Pelepasan dijalankan tepat satu kali, pointer langsung dinetralkan, dan pemeriksaan di akhir membuktikan keadaan program yang konsisten. Pola yang sama diterapkan pada setiap objek dinamis, sedangkan cara C++ modern untuk mengotomatiskannya dibahas pada topik pratinjau smart pointer.

Ketiga kebiasaan itu tidak menghapus kebutuhan memeriksa kode, tetapi mempersempit tempat kesalahannya bersembunyi. Pointer yang selalu dinetralkan membuat pemakaian setelah lepas tertangkap sebagai akses terhadap nullptr, yang gejalanya pasti dan seketika. Sisa risiko kemudian berpindah ke keputusan desain, yaitu siapa pemilik setiap objek, dan keputusan itulah yang dijawab secara sistematis oleh smart pointer.

Membaca keluaran crash dan sanitizer#

Program yang melakukan dereference terhadap alamat yang tidak sah umumnya dihentikan sistem operasi dengan pesan segmentation fault. Pesan ini hanya menyatakan bahwa akses memori ditolak, tanpa menyebut baris mana yang salah, sehingga informasinya terbatas. Dua langkah yang membantu adalah menjalankan program di bawah debugger dan mengompilasi ulang dengan sanitizer memori.

AddressSanitizer dari GCC mendeteksi use-after-free, double-free, dan kebocoran pada saat kejadiannya, lalu mencetak laporan yang memuat jalur pemanggilannya. Laporan semacam itu memuat angka alamat yang berbeda pada setiap eksekusi, sehingga cara membacanya adalah melihat jenis error, nama fungsi, dan nomor barisnya, bukan angka alamatnya. Kompilasinya dilakukan dengan menambahkan flag -fsanitize=address bersama flag -g agar informasi debugging tersedia.

bash Perintah kompilasi dengan AddressSanitizer
# tidak-dijalankan (laporan ASan memuat alamat yang berubah-ubah, sehingga keluarannya tidak ditempel)
c++ -g -fsanitize=address -o /tmp/tes-uaf main.cpp
/tmp/tes-uaf

Pesan segmentation fault juga muncul dalam bentuk yang berbeda-beda antar lingkungan, misalnya dilengkapi keterangan core dumped pada banyak sistem berbasis Unix. Angka yang menyertainya, seperti alamat yang ditolak, baru berguna setelah program dikompilasi ulang dengan informasi debugging. Kebiasaan yang lebih murah tetap berada di hulu, yaitu meminimalkan pemakaian pointer pemilik mentah sebagaimana disarankan pada topik new dan delete.

Debugger memberi gambaran yang lebih langsung saat program berhenti karena crash. Dengan informasi debugging dari flag -g, debugger menampilkan baris yang sedang dieksekusi saat akses ditolak, bersama isi variabel dan jalur pemanggilannya. Kombinasi -g dan sanitizer biasanya cukup untuk melacak bug memori tanpa memasang perangkat lunak tambahan.

Ringkasan#

  • Kebocoran terjadi bila memori dinamis tidak pernah dilepas dan alamatnya hilang, dan gejalanya baru terasa pada program yang berjalan lama.
  • Double-free dan use-after-free adalah undefined behavior yang gejalanya muncul jauh dari penyebabnya, sehingga keduanya sulit dilacak tanpa alat bantu.
  • Pesan segmentation fault hanya menandakan akses memori ditolak, sedangkan AddressSanitizer melaporkan jenis error beserta baris penyebabnya.
  • Menetralkan pointer setelah delete, memeriksa nullptr sebelum dereference, dan mencocokkan setiap alokasi dengan tepat satu pelepasan menutup sebagian besar celah.

Referensi#