Valgrind adalah framework instrumentasi dinamis yang menjalankan binary di atas virtual machine internal, mengintercept setiap instruksi mesin untuk melacak penggunaan memori, threading, dan cache. Berbeda dengan sanitizers yang menambahkan instrumentasi pada saat kompilasi, Valgrind bekerja pada binary yang sudah jadi—tidak perlu kompilasi ulang. Valgrind paling terkenal lewat tool memcheck yang mendeteksi memory leak, uninitialized read, dan invalid access. Pada chapter ini kita akan mempelajari tool memcheck dan callgrind, membaca laporan valgrind, serta membandingkan Valgrind dengan AddressSanitizer.

Menjalankan Valgrind #

Cara memakai valgrind sangat sederhana: prefix perintah program dengan valgrind. Valgrind memuat binary ke dalam VM-nya, menjalankan instruksi satu per satu dengan instrumentasi, lalu mencetak laporan di akhir. Tidak perlu kompilasi ulang—binary biasa bisa langsung dipakai.

bash
gcc -g -O0 -std=c11 program.c -o program
valgrind --leak-check=full ./program

Output yang Dihasilkan (format resmi valgrind memcheck):

text
==12345== Memcheck, a memory error detector
==12345== Copyright (C) 2002-2024, and GNU GPL'd, by Julian Seward et al.
==12345== Using Valgrind-3.23.0 and LibVEX
==12345== Command: ./program
==12345==
(program output here)
==12345==
==12345== HEAP SUMMARY:
==12345==     in use at exit: 5 bytes in 1 blocks
==12345==   total heap usage: 2 allocs, 1 frees, 21 bytes allocated
==12345==
==12345== 5 bytes in 1 blocks are definitely lost in loss record 1 of 1
==12345==    at 0x4848898: malloc (vg_replace_malloc.c:431)
==12345==    by 0x401142: salin_nama (program.c:6)
==12345==    by 0x401166: main (program.c:12)
==12345==
==12345== LEAK SUMMARY:
==12345==    definitely lost: 5 bytes in 1 blocks
==12345==    indirectly lost: 0 bytes in 0 blocks
==12345==      possibly lost: 0 bytes in 0 blocks
==12345==    still reachable: 0 bytes in 0 blocks
==12345==         suppressed: 0 bytes in 0 blocks
==12345== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

Mari kita bedah elemen kunci pada laporan valgrind di atas:

  1. ==12345== Memcheck, a memory error detector: nomor dalam ==...== adalah PID proses yang dijalankan valgrind. Tool yang aktif adalah Memcheck—tool default yang fokus pada bug memori.
  2. HEAP SUMMARY: in use at exit: 5 bytes in 1 blocks: ringkasan memori heap saat program keluar. Ada 5 byte dalam 1 alokasi yang belum dibebaskan.
  3. total heap usage: 2 allocs, 1 frees, 21 bytes allocated: selama eksekusi, program melakukan 2 alokasi dan 1 free (total 21 byte dialokasikan). Selisih 2 - 1 = 1 blok yang bocor.
  4. 5 bytes in 1 blocks are definitely lost in loss record 1 of 1: klasifikasi leak definitely lost. Valgrind membedakan beberapa kategori leak:
    • definitely lost: blok yang tidak direferensikan pointer manapun (analog direct leak di LSan).
    • indirectly lost: blok yang hanya direferensikan oleh blok yang juga lost.
    • possibly lost: blok yang pointer-nya mungkin hilang, valgrind tidak yakin.
    • still reachable: blok yang masih punya pointer global, biasanya tidak dianggap leak.
  5. at 0x4848898: malloc (vg_replace_malloc.c:431) ... by 0x401142: salin_nama (program.c:6): backtrace alokasi. Valgrind mengganti malloc libc dengan versi sendiri (vg_replace_malloc.c) agar bisa melacak alokasi. Backtrace kemudian menunjukkan salin_nama pada baris 6 sebagai pemanggil malloc.
  6. ERROR SUMMARY: 1 errors from 1 contexts: ringkasan akhir—1 error dalam 1 context (lokasi source unik). Exit code program menjadi non-zero.

Program Contoh dengan Leak #

Mari kita lihat program yang menghasilkan laporan di atas. Program mengalokasikan buffer string, lalu lupa membebaskan salah satu.

c
/* File: /tmp/test_valgrind1.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static char *salin_nama(const char *src) {
    size_t n = strlen(src) + 1;
    char *out = (char *)malloc(n);
    if (out) memcpy(out, src, n);
    return out;
}
int main(void) {
    char *nama1 = salin_nama("Budi");
    char *nama2 = salin_nama("Siti");
    printf("nama1 = %s\n", nama1);
    free(nama1);
    /* nama2 sengaja di-leak untuk demo */
    return 0;
}
bash
gcc -g -O0 -std=c11 -Wall -Wextra /tmp/test_valgrind1.c -o /tmp/test_valgrind1
valgrind --leak-check=full /tmp/test_valgrind1

Valgrind akan menampilkan laporan dengan struktur seperti di atas, menunjukkan bahwa nama2 (5 byte) bocor di salin_nama pada baris 6, dipanggil dari main baris 12.

Deteksi Uninitialized Read #

Selain leak, memcheck mendeteksi uninitialized read—pembacaan variabel yang belum diberi nilai. Bug ini sering menyebabkan hasil program tidak deterministik (berbeda setiap run).

c
/* File: /tmp/test_valgrind2.c */
#include <stdio.h>
int main(void) {
    int x;
    if (x > 10) {  /* bug: x belum diinisialisasi */
        printf("x besar: %d\n", x);
    } else {
        printf("x kecil\n");
    }
    return 0;
}
bash
gcc -g -O0 -std=c11 /tmp/test_valgrind2.c -o /tmp/test_valgrind2
valgrind /tmp/test_valgrind2

Output yang Dihasilkan (format resmi valgrind):

text
==12345== Conditional jump or move depends on uninitialised value(s)
==12345==    at 0x401135: main (test_valgrind2.c:4)
==12345==  Uninitialised value was created by a stack allocation
==12345==    at 0x401126: main (test_valgrind2.c:2)

Mari kita uraikan dua bagian laporan valgrind untuk uninitialized read:

  1. Conditional jump or move depends on uninitialised value(s): valgrind menandai bahwa percabangan kondisional (if (x > 10)) bergantung pada nilai yang belum diinisialisasi. Valgrind tidak bisa memastikan cabang mana yang akan dipilih—hasilnya acak.
  2. at 0x401135: main (test_valgrind2.c:4): baris 4 adalah if (x > 10). Alamat instruksi (0x401135) menunjuk ke instruksi percabangan dalam binary.
  3. Uninitialised value was created by a stack allocation ... at 0x401126: main (test_valgrind2.c:2): valgrind menelusuri kapan variabel uninitialized dibuat. Baris 2 adalah awal fungsi main tempat stack frame (yang berisi x) dialokasikan. Informasi ini membantu menemukan deklarasi variabel yang lupa diberi nilai awal.

Flag Penting Valgrind #

FlagFungsi
--leak-check=fulllaporan leak lengkap dengan backtrace (default: summary)
--show-leak-kinds=alltampilkan semua kategori leak (definite, indirect, possible, reachable)
--track-origins=yestelusuri asal-usul uninitialized value (lebih lambat tapi informatif)
--verbosecetak info detail tentang eksekusi valgrind
--trace-children=yesikuti proses anak hasil fork/exec
--error-exitcode=42exit code program bila ada error (default: 0 bila program sukses)
--tool=callgrindganti tool dari memcheck ke callgrind (profiling)
--tool=helgrindganti tool ke helgrind (deteksi data race)

Tool Selain memcheck #

Valgrind adalah framework dengan beberapa tool selain memcheck:

  • memcheck (default): deteksi bug memori (leak, uninitialized read, invalid access).
  • callgrind: profil call graph dan jumlah eksekusi tiap fungsi. Output bisa dilihat dengan callgrind_annotate atau kcachegrind.
  • cachegrind: profil cache hit/miss untuk identifikasi bottleneck memori.
  • helgrind: deteksi data race pada program multithreaded.
  • massif: profil heap usage dari waktu ke waktu—berguna untuk menemukan titik konsumsi memori tertinggi.
  • drd dan exp-dhat: alternatif untuk thread error dan heap block profiling.

Contoh penggunaan callgrind:

bash
valgrind --tool=callgrind --callgrind-out-file=/tmp/callgrind.out ./program
callgrind_annotate /tmp/callgrind.out

Perbandingan Valgrind vs AddressSanitizer #

Karena sanitizers (ASan, UBSan, LSan) dan Valgrind sering dipakai untuk tujuan yang sama, berikut perbandingan praktis:

AspekValgrindASan/LSan
Cara kerjaVM-based, instrumentasi binary saat runtimeCompiler-based, instrumentasi saat kompilasi
Memerlukan kompilasi ulangtidakya (perlu -fsanitize=...)
Perlambatan10-50x2x
Overhead memori2-3x2-3x
Deteksi uninitialized readya (memcheck)tidak (perlu MemorySanitizer, hanya clang)
Deteksi leak sementarayatidak (LSan hanya periksa saat exit)
Deteksi bug thread (helgrind)yaterpisah (ThreadSanitizer)
Profil performa (callgrind)yatidak
Support platformLinux, macOSLinux, macOS, Windows, Android