Debugging Memori dengan Valgrind

Menjalankan valgrind dan membaca laporan kebocoran yang dihasilkannya.

Daftar isi

Kesalahan memori yang dibahas sepanjang modul ini sering tidak meninggalkan jejak ketika program dijalankan biasa, sehingga penulisnya hanya melihat program yang sehat atau berhenti tanpa penjelasan. Valgrind adalah alat analisis memori yang menjalankan program sambil memeriksa setiap aksesnya, sehingga akses tidak sah, pemakaian nilai yang belum diisi, dan kebocoran dilaporkan lengkap dengan lokasi barisnya. Topik ini membahas cara menjalankan Valgrind dengan alat Memcheck-nya, membaca laporan kebocoran yang dihasilkan, dan mengenali laporan program yang bersih. Karena Valgrind tidak tersedia pada sandbox ini, perintahnya diperlihatkan tanpa keluaran, sedangkan program C-nya tetap dapat dikompilasi dan dijalankan seperti biasa.

Apa yang diperiksa Memcheck#

Memcheck adalah alat bawaan Valgrind yang langsung berjalan tanpa opsi tambahan. Memcheck menjalankan program di atas prosesor tiruan, sehingga setiap pembacaan, penulisan, dan pembebasan memori dapat diperiksa. Keluarannya berupa daftar error berikut jejak pemanggilan yang menunjuk baris kode sumber. Kategori yang paling sering muncul pada program C adalah akses melewati batas blok, penggunaan variabel yang belum diinisialisasi, pembebasan yang tidak tepat, dan kebocoran yang terdeteksi saat program berakhir.

Pemeriksaan yang menyeluruh ini ada harganya, yaitu program berjalan puluhan kali lebih lambat dan menggunakan memori yang jauh lebih banyak dibandingkan eksekusi biasa. Karena itu Valgrind lazim dijalankan pada saat pengujian, bukan ketika program dipakai pengguna. Program C pada topik ini tetap dijalankan secara langsung untuk menunjukkan keluarannya yang normal, sedangkan pemanggilan valgrind-nya hanya diperlihatkan sebagai perintah yang siap diketik pada mesin yang memilikinya.

Alur kerjanya berputar pada empat langkah, yaitu kompilasi dengan informasi debug, pemanggilan program lewat valgrind, pembacaan laporan, dan perbaikan kode. Diagram berikut merangkum putaran tersebut sebelum tiap langkahnya dibahas.

Kompilasidengan -gJalankan lewatvalgrindBacalaporanPerbaikikodenyaulangi sampai laporan bersihPemeriksaan dilakukan pada setiap perubahan besar, bukan hanya sekali sebelum program dirilis.
Alur kerja pemeriksaan memori dengan Valgrind

Menyiapkan program dan menjalankannya#

Kompilasi program dengan opsi -g sebelum diperiksa, karena opsi itu menyertakan informasi debug sehingga laporan memuat nama berkas dan nomor baris, bukan alamat mentah. Program pertama adalah versi yang bocor, yaitu menghitung rata-rata nilai dengan benar tetapi tidak membebaskan bloknya.

Perintah valgrind pada contoh menggunakan opsi --leak-check=full, yaitu tingkat pemeriksaan kebocoran yang paling rinci. Dengan opsi itu, setiap blok yang pasti bocor atau kemungkinan bocor dilaporkan satu per satu beserta jejak alokasinya, sedangkan tanpa opsi tersebut laporan hanya merangkum jumlahnya di akhir. Opsi lain yang sering dipakai adalah --track-origins=yes untuk menelusuri asal nilai yang belum diinisialisasi, dan keduanya cukup untuk mayoritas kebutuhan pemula.

c rekap-nilai-bocor.c
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int *nilai = malloc(4 * sizeof(int));
    if (nilai == NULL) {
        return 1;
    }

    nilai[0] = 72;
    nilai[1] = 88;
    nilai[2] = 64;
    nilai[3] = 91;

    int jumlah = 0;
    for (int i = 0; i < 4; i = i + 1) {
        jumlah = jumlah + nilai[i];
    }
    printf("Rata-rata: %.1f\n", (double)jumlah / 4);

    /* free(nilai) lupa ditulis: blok 16 byte tidak pernah dibebaskan */
    return 0;
}
text Keluaran
Rata-rata: 78.8

Dari luar, program tampak sempurna karena keluarannya benar dan berakhir normal. Kerusakannya baru terlihat ketika program dijalankan di bawah Valgrind seperti berikut.

bash
# tidak-dijalankan
# valgrind tidak tersedia pada sandbox ini; perintahnya dijalankan pada mesin yang memilikinya
gcc -g rekap-nilai-bocor.c -o rekap-nilai-bocor
valgrind --leak-check=full ./rekap-nilai-bocor

Laporan yang dihasilkan dimulai dengan baris berawalan ==angka==, dan angka di situ adalah nomor proses yang biasanya tidak penting. Bagian berjudul HEAP SUMMARY menunjukkan blok yang masih terpasang saat program berakhir beserta jejak alokasinya, yaitu satu blok 16 byte yang dipesan pada baris 6 berkas rekap-nilai-bocor.c. Bagian berikutnya berjudul LEAK SUMMARY merangkum seluruh kebocoran ke dalam beberapa kategori.

Membaca laporan kebocoran#

Kategori pada LEAK SUMMARY membagi blok yang tersisa menurut kemampuan program menjangkaunya kembali, dan setiap kategori menuntut tindakan yang berbeda.

KategoriMakna
definitely losttidak ada pointer yang menjangkau blok, pasti bocor dan wajib diperbaiki
indirectly lostblok yang hanya bisa dicapai melalui blok lain yang hilang
possibly losthanya bagian tengah blok yang dicapai pointer, perlu ditinjau
still reachablemasih ada pointer yang menunjuk blok saat program berakhir

Kategori definitely lost adalah sasaran utama karena bloknya pasti tidak terjangkau, dan dengan --leak-check=full laporan mencantumkan jejak alokasi setiap blok semacam itu. Kategori still reachable sering bukan bug, misalnya buffer milik pustaka standar yang sengaja dipertahankan sampai program berakhir. Apabila tidak ada blok yang tertinggal sama sekali, laporan diakhiri dengan kalimat All heap blocks were freed -- no leaks are possible, dan itulah sasaran akhir setiap perbaikan.

Perbaikan untuk program di atas hanya satu baris, yaitu menambahkan free(nilai); sebelum return, lalu kompilasi ulang dan jalankan ulang Valgrind sampai kalimat penutupnya bersih. Alur kerja yang sama berlaku untuk kesalahan akses memori, yaitu baca laporan dari error pertama, perbaiki, dan ulangi, karena error yang berikutnya sering hanya akibat dari yang pertama.

Program bersih sebagai pembanding#

Program berikut melakukan perhitungan yang sama dengan pembebasan yang lengkap, sehingga perbedaannya hanya satu baris di akhir main.

c rekap-nilai.c
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int *nilai = malloc(4 * sizeof(int));
    if (nilai == NULL) {
        return 1;
    }

    nilai[0] = 72;
    nilai[1] = 88;
    nilai[2] = 64;
    nilai[3] = 91;

    int jumlah = 0;
    for (int i = 0; i < 4; i = i + 1) {
        jumlah = jumlah + nilai[i];
    }
    printf("Rata-rata: %.1f\n", (double)jumlah / 4);

    free(nilai);
    return 0;
}
text Keluaran
Rata-rata: 78.8

Keluaran keduanya identik, dan justru itulah intinya, yaitu kebocoran tidak mengubah hasil program yang pendek. Di bawah Valgrind, laporan versi bersih ini tidak memuat blok yang tertinggal dan berakhir dengan kalimat bahwa tidak ada kemungkinan kebocoran. Kebiasaan membandingkan keduanya melatih telinga untuk laporan yang sehat, sehingga perbedaan kecil pada laporan berikutnya langsung terasa.

Eksekusi lewat Valgrind yang puluhan kali lebih lambat juga menentukan strategi pengujiannya. Gunakan data uji yang kecil saat memeriksa, karena pembacaan laporan tidak memerlukan data besar, lalu jalankan pengujian kinerja tanpa valgrind pada data yang sebenarnya. Pemisahan ini menjaga pemeriksaan memori tetap cepat dilakukan setiap ada perubahan, alih-alih ditunda karena dianggap memakan waktu.

Kesalahan umum#

Kesalahan pada pemakaian Valgrind kebanyakan berupa kebiasaan membaca laporan yang keliru, bukan salah mengetik perintahnya. Empat butir berikut menutup pemeriksaan ini.

  • Memeriksa program tanpa opsi -g. Laporan tetap keluar, tetapi jejaknya berisi alamat alih-alih nama berkas dan baris, sehingga perbaikannya sulit ditelusuri. Kompilasi dengan -g adalah langkah pertama sebelum pemanggilan valgrind.
  • Memperlakukan angka ==…== sebagai bagian penting. Angka di antara tanda sama dengan hanyalah nomor proses yang berubah pada setiap eksekusi. Abaikan angkanya dan fokuskan pembacaan pada jenis error beserta jejak pemanggilannya.
  • Menindak semua kategori kebocoran dengan cara yang sama. Menambahkan free pada blok still reachable milik pustaka justru tidak mungkin karena alamatnya tidak dipegang oleh kode program. Perbaiki definitely lost terlebih dahulu, tinjau possibly lost, lalu pelajari sisa kategori sebelum bertindak.
  • Mengharapkan Valgrind menemukan semua jenis bug. Alat ini fokus pada memori dan sebagian perilaku program di sekitarnya, sehingga hasil hitungan yang salah karena rumus keliru tetap lolos tanpa laporan. Kombinasikan pemeriksaannya dengan pengujian fungsional yang memeriksa benar-salahnya keluaran.

Ringkasan#

  • Memcheck menjalankan program di atas prosesor tiruan dan melaporkan akses memori tidak sah, nilai yang belum diisi, serta kebocoran saat program berakhir.
  • Kompilasi dengan -g membuat laporan memuat nama berkas dan nomor baris, dan opsi --leak-check=full melengkapi laporan dengan jejak alokasi tiap blok.
  • Kategori LEAK SUMMARY memisahkan blok yang pasti bocor, kemungkinan bocor, dan masih terjangkau, dengan sasaran akhir berupa laporan yang menyatakan tidak ada kemungkinan kebocoran.
  • Program yang bocor dan program yang bersih dapat menghasilkan keluaran yang sama, sehingga pemeriksaan alat tidak dapat digantikan oleh pengamatan keluaran.

Referensi#