Statistical debugging memberi Anda daftar predicates yang diurutkan, bukan akar masalah. Anda menginstrumentasikan setiap branch dan null check, menjalankan seratus ribu eksekusi, dan algoritma memberi Anda papan peringkat. buffer_idx > max_len di parser.c:144 memiliki importance 0,94. Begitu juga log_file != NULL di main.c:38. Salah satunya adalah bug. Yang lain hanya kebetulan benar setiap kali program crash.
Korelasi tinggi berarti sebuah predicate dan crash berdampingan. Ini tidak berarti predicate tersebut menyebabkan crash. Jika setiap crash melewati jalur cleanup yang sama, setiap predicate pada jalur itu akan terlihat bersalah. Anda memerlukan cara untuk memisahkan pelaku dari para penonton.
Apa yang sebenarnya diukur oleh skor korelasi
Metric standarnya adalah importance, diperkenalkan oleh Liblit et al. dalam Cooperative Bug Isolation. Importance adalah hasil kali dua angka: increase dan support.
Increase adalah perbedaan antara tingkat crash ketika sebuah predicate benar dan ketika salah. Jika p != NULL benar dalam 99% dari run yang crash dan 98% dari run yang tidak crash, increase-nya adalah 0,01. Predicate itu tidak berguna bahkan jika ia aktif di setiap crash.
Support adalah fraksi dari semua run di mana predicate tersebut benar. Ini menjaga predictor yang langka tetapi sempurna dari mendominasi peringkat. Sebuah predicate yang hanya aktif sekali tetapi run tersebut crash mendapatkan increase tinggi dan support mendekati nol.
importance(pred) = (P(crash | pred=True) - P(crash | pred=False)) * P(pred=True)
Ini menyaring noise dengan baik. Ini tidak melokalisasi bug. Sebuah predicate dengan importance tinggi mungkin adalah bug, pemeriksaan keselamatan yang menangkapnya, atau operasi umum di setiap jalur ke crash.
Mengapa predicates yang terlihat bersalah seringkali tidak bersalah
Pertimbangkan kesalahan off-by-one di baris 42 di mana batas loop menggunakan <= alih-alih <. Crash terjadi dua puluh baris kemudian. Kondisi loop aktif di setiap iterasi. Pemeriksaan batas di baris 45 aktif di setiap iterasi. Null check di baris 10 aktif sekali saat startup. Semuanya memiliki support tinggi dan semuanya muncul dalam run yang crash.
Masalahnya adalah jarak temporal dan kausal. Predicate yang dieksekusi beberapa saat sebelum crash lebih mungkin terkait daripada yang dari startup. Predicate di dalam fungsi yang sama lebih mungkin terkait daripada yang di utility logger. Statistical debugging mengabaikan ini kecuali Anda menambahkannya kembali.
Cara menyaring berdasarkan reachability dan jarak
Perbaikan pertama adalah control-flow reachability. Sebuah predicate yang selalu benar dalam run yang crash tetapi juga selalu benar dalam run yang tidak crash yang mencapai kode yang sama tidak mencurigakan. Ia hanyalah properti dari jalur itu.
Hitung probabilitas bersyarat. Alih-alih bertanya “seberapa sering predicate ini crash?”, tanyakan “seberapa sering ia crash mengingat bahwa program mencapai titik ini dalam control flow?” Jika tingkat crash tidak berubah ketika Anda mensyaratkan mencapai fungsi tersebut, predicate tersebut tidak menambahkan informasi.
Perbaikan kedua adalah jarak spasial. Setelah memberi peringkat berdasarkan importance, peringkat ulang berdasarkan jarak ke lokasi crash. Ini adalah heuristik. Bug bisa menyebar jauh. Tetapi sebagian besar bug memory corruption dan null dereference berada dalam beberapa baris dari predicate yang pertama menyimpang dari perilaku yang benar.
Berikut adalah implementasi ringan yang menghitung importance dan menyaring berdasarkan jarak:
from dataclasses import dataclass
from typing import List, Set
@dataclass(frozen=True)
class Predicate:
pid: str
file: str
line: int
expr: str
@dataclass
class Run:
crashed: bool
crash_file: str = ""
crash_line: int = 0
predicates: Set[Predicate] = None
def importance(pred: Predicate, runs: List[Run]) -> float:
total = len(runs)
with_pred = [r for r in runs if pred in r.predicates]
without_pred = [r for r in runs if pred not in r.predicates]
if not with_pred or not without_pred:
return 0.0
crash_with = sum(1 for r in with_pred if r.crashed) / len(with_pred)
crash_without = sum(1 for r in without_pred if r.crashed) / len(without_pred)
support = len(with_pred) / total
return (crash_with - crash_without) * support
def avg_distance(pred: Predicate, runs: List[Run]) -> float:
crash_runs = [r for r in runs if r.crashed]
if not crash_runs:
return float('inf')
dists = []
for r in crash_runs:
if pred.file != r.crash_file:
return float('inf')
dists.append(abs(pred.line - r.crash_line))
return sum(dists) / len(dists)
def rank_predicates(runs: List[Run]) -> List[tuple]:
all_preds = set()
for r in runs:
all_preds.update(r.predicates)
scored = []
for p in all_preds:
imp = importance(p, runs)
dist = avg_distance(p, runs)
# Higher importance is better, lower distance is better.
if dist == float('inf'):
score = imp * 0.1
else:
score = imp / (1 + dist / 10)
scored.append((score, imp, dist, p))
scored.sort(key=lambda x: x[0], reverse=True)
return scored
Fungsi scoring sengaja dibuat sederhana. Dalam arsitektur berlapis, gunakan penalty jarak yang lebih lemah. Dalam parser yang ketat, penalty yang kuat bekerja lebih baik.
Dari predicate ke baris eksak: triangulasi
Sebuah predicate masih bukan lokasi bug. Ia adalah ekspresi Boolean seperti i <= buf->len. Untuk sampai ke nomor baris, lihat predicate dalam konteksnya.
Pertanyaan kuncinya: apa negasi dari predicate ini, dan apakah negasi tersebut akan mencegah crash? Jika predicate-nya adalah p == NULL dan crash-nya adalah null dereference, negasinya akan mencegahnya. Itu menjadikan predicate sebagai penyebab langsung. Jika predicate-nya adalah log_level > 2 dan crash-nya adalah buffer overflow, negasinya tidak mengubah apa pun. Predicate tersebut adalah penonton.
Anda bisa mengotomatiskan ini sebagian. Untuk setiap predicate teratas, paksa salah dalam kasus reproduksi. Jika crash menghilang, Anda telah menemukan kondisi pengendali. Bug biasanya adalah assignment atau perbandingan yang membuat predicate benar sejak awal.
Peringkat statistik menyempitkan pencarian dari ribuan predicate menjadi segelintir. Sebuah test yang ditargetkan atau sesi debugger singkat menutup loop.
Trade-off: cakupan versus presisi
Semakin banyak predicates yang Anda instrumentasikan, semakin banyak kekuatan statistik yang Anda miliki, tetapi semakin banyak false positive yang Anda hasilkan. Menginstrumentasikan setiap akses memori menghasilkan jutaan predicates. Sebagian besar akan berkorelasi lemah dengan crash secara kebetulan.
Solusinya adalah selective instrumentation. Mulailah dengan null checks, bounds checks, nilai return error, dan branch conditions dalam fungsi dari stack trace. Ini paling mungkin memisahkan perilaku yang crash dari yang tidak crash.
Anda juga memerlukan cukup run. Skor importance adalah statistik sampel. Dengan seratus run, noise mendominasi. Dengan sepuluh ribu, sinyalnya mempertajam. Aturan praktis dari makalah CBI adalah setidaknya seribu run per bug. Crash yang jarang memerlukan volume.
Apa yang tidak bisa ditangkap ini
Statistical debugging menemukan bug yang muncul sebagai deviasi predicate yang observable. Ia tidak akan menemukan regresi performa, error logika yang tetap dalam batas, atau kondisi race yang instrumentation-nya mengganggu.
Ini juga secara fundamental post-hoc. Crash sudah terjadi. Anda melakukan forensik pada dataset. Nilainya adalah menyusutkan ruang pencarian dari “seluruh codebase” menjadi “mungkin lima puluh baris.”
Contoh end-to-end minimal
Berikut adalah cara chunk-potongannya cocok dengan data yang disimulasikan:
# Simulate runs for a bug at parser.c:42 (off-by-one loop bound)
runs = []
# 900 non-crashing runs
for _ in range(900):
runs.append(Run(
crashed=False,
predicates={
Predicate("p1", "parser.c", 10, "buf != NULL"),
Predicate("p2", "parser.c", 42, "i <= buf->len"), # the bug
Predicate("p3", "parser.c", 45, "i < buf->len"),
}
))
# 100 crashing runs: all hit the bug predicate
for _ in range(100):
runs.append(Run(
crashed=True,
crash_file="parser.c",
crash_line=62,
predicates={
Predicate("p1", "parser.c", 10, "buf != NULL"),
Predicate("p2", "parser.c", 42, "i <= buf->len"),
Predicate("p3", "parser.c", 45, "i < buf->len"),
Predicate("p4", "main.c", 5, "argc > 1"), # startup, irrelevant
}
))
results = rank_predicates(runs)
for score, imp, dist, pred in results[:3]:
print(f"{pred.file}:{pred.line} {pred.expr:20s} "
f"score={score:.3f} importance={imp:.3f} avg_dist={dist:.1f}")
Menjalankan ini memberikan p2 di parser.c:42 skor tertinggi karena ia memiliki importance tinggi dan jarak yang pendek ke crash. p1 memiliki importance serupa tetapi berada lebih jauh. p4 dihukum karena berada di file yang berbeda.
Mulailah dengan crash yang sedang Anda selidiki
Anda tidak memerlukan framework kelas penelitian. Pilih crash yang sering direproduksi. Tambahkan instrumentation ke lima predicates paling menarik dalam call stack. Jalankan test suite atau traffic produksi Anda. Hitung importance dan jarak. Jika satu predicate memisahkan run yang crash dari yang tidak crash dan berada dekat dengan lokasi crash, Anda telah menyempitkan pencarian.
Statistical debugging bukan pengganti untuk stack traces atau debugger. Ia adalah pengganti untuk membaca setiap baris kode antara main dan segfault. Korelasi membawa Anda ke lingkungan. Reachability, jarak, dan pemeriksaan cepat terhadap negasi membawa Anda ke pintu.