Debugging dan alat pendukung

GDB dan debugger IDE, ditambah alur kerja sanitizer yang dapat dijalankan.

Daftar isi

Topik ini membahas alur kerja debugging C++ secara berlapis, mulai dari peringatan compiler yang diperiksa saat build, debugger GDB beserta breakpoint, sampai sanitizer UBSan yang menangkap kesalahan pada saat program berjalan. Berkas contoh yang sama diperiksa oleh setiap lapisan sehingga perbedaan perannya terlihat langsung. Setelah membaca topik ini, pembaca mengetahui alat mana yang dipanggil lebih dahulu ketika program berperilaku menyimpang, serta cara membaca pesan yang dihasilkan masing-masing alat.

Alur kerja debugging yang berlapis#

Debugging adalah proses menemukan penyebab perilaku program yang menyimpang dari rencananya. Proses itu tidak bertumpu pada satu alat, melainkan pada lapisan pemeriksaan yang urutannya ditentukan oleh biayanya. Peringatan compiler paling murah karena hadir tanpa menjalankan program apa pun, sanitizer menambahkan pemeriksaan runtime ke dalam biner, dan debugger memberi kendali penuh atas eksekusi program namun menuntut sesi interaktif. Program kecil yang menyimpang biasanya cukup diperiksa dua lapis pertama, sedangkan bug yang sulit direproduksi baru dipindahkan ke debugger.

Berkas berikut dipakai sepanjang topik ini. Berkas tersebut mewakili modul yang sedang dikerjakan, yaitu berisi fungsi yang parameternya belum diperhitungkan, perbandingan bilangan dengan tipe yang tercampur, dan satu operasi berlebih yang tidak terdeteksi tanpa alat.

cpp main.cpp
// tidak-dijalankan (memuat kode bermasalah; dibangun melalui dua blok bash di bawah)
#include <iostream>

double rataRata(int banyak, double total) {
    // parameter banyak belum diperhitungkan dalam perhitungan
    double hasil = total / 2.0;
    std::cout << "rata-rata: " << hasil << '\n';
    return hasil;
}

int main() {
    int cadangan;
    int nilai = -5;
    unsigned int batas = 3;
    if (nilai < batas) {
        std::cout << "nilai berada di bawah batas\n";
    }
    int poin = 2000000000;
    int ganda = poin * 2;
    std::cout << "poin ganda: " << ganda << '\n';
    return 0;
}

Ketiga masalah itu lolos dari kompilasi biasa karena semuanya sah secara sintaks. Pemeriksaan lapis demi lapis yang berikut menunjukkan bagaimana setiap alat menangkap bagian yang berbeda.

GDB dan breakpoint#

Debugger diuraikan lebih dahulu karena pemakaiannya paling banyak melibatkan langkah interaktif, sedangkan dua lapisan yang lebih murah diterapkan pada berkas contoh pada bagian berikutnya.

GDB (GNU Debugger) adalah debugger baris perintah yang mengeksekusi program dengan kendali penuh dari penggunanya. LLDB adalah padanannya pada ekosistem LLVM, dan debugger yang terpasang di IDE pada umumnya merupakan antarmuka grafis di atas salah satu dari keduanya. Fitur inti yang sama pada ketiganya adalah breakpoint, yaitu titik jeda yang ditanam pada suatu baris atau fungsi sehingga eksekusi program berhenti di sana sebelum baris itu dijalankan. Pada keadaan jeda itu, nilai setiap variabel dapat diperiksa dan eksekusi dilanjutkan per pernyataan.

Sesi GDB mengikuti pola yang tetap. Pertama, program dibangun dengan flag -g agar berkas simbol debug ikut disertakan, misalnya g++ -g main.cpp -o app. Selanjutnya debugger dijalankan dengan gdb ./app, lalu langkah-langkah berikut dilakukan di dalamnya.

  1. break main menanam breakpoint pada awal fungsi main, atau break main.cpp:19 menanamnya pada baris tertentu.
  2. run memulai eksekusi, dan program berhenti tepat sebelum baris breakpointnya.
  3. print nilai menampilkan nilai variabel pada keadaan jeda, misalnya print batas.
  4. next melangkah ke pernyataan berikutnya tanpa masuk ke dalam fungsi, sedangkan step masuk ke dalam fungsi yang dipanggil.
  5. backtrace menampilkan rantai pemanggilan fungsi yang sedang berlangsung, berguna ketika program berhenti di dalam panggilan yang dalam.
  6. continue melanjutkan eksekusi sampai breakpoint berikutnya, dan quit mengakhiri sesi debugger.

Peringatan compiler sebagai pemeriksa pertama#

Lapisan pertama berjalan tanpa biaya tambahan apa pun. Flag -Wall mengaktifkan kelompok peringatan yang paling berguna, sedangkan -Wextra menambahkan kelompok yang lebih ketat, misalnya peringatan atas parameter yang tidak dipakai. Kompilasi berkas contoh dengan kedua flag tersebut, tanpa menautkan program, menghasilkan tiga peringatan.

bash
# kompilasi dengan peringatan lengkap, tanpa menautkan program
g++ -Wall -Wextra -o /tmp/dbg.o -c main.cpp
text Keluaran
main.cpp: In function ‘double rataRata(int, double)’:
main.cpp:4:21: warning: unused parameter ‘banyak’ [-Wunused-parameter]
    4 | double rataRata(int banyak, double total) {
      |                 ~~~~^~~~~~
main.cpp: In function ‘int main()’:
main.cpp:15:15: warning: comparison of integer expressions of different signedness: ‘int’ and ‘unsigned int’ [-Wsign-compare]
   15 |     if (nilai < batas) {
      |         ~~~~~~^~~~~~~
main.cpp:12:9: warning: unused variable ‘cadangan’ [-Wunused-variable]
   12 |     int cadangan;
      |         ^~~~~~~~

Setiap pesan menyebut baris, kolom, kategori flag, dan menampilkan potongan barisnya sebagai petunjuk visual. Peringatan pertama menandai parameter banyak yang tidak pernah digunakan, pola yang hampir selalu berarti perhitungan yang belum selesai. Peringatan kedua menandai perbandingan int dengan unsigned int. Nilai -5 dikonversi menjadi bilangan tak bertanda yang sangat besar, sehingga kondisi nilai < batas tidak pernah terpenuhi dan cabangnya tidak pernah dieksekusi. Peringatan ketiga menandai variabel yang tidak pernah dipakai. Opsi -Werror dapat ditambahkan untuk menjadikan setiap peringatan sebagai error sehingga build berhenti sampai semuanya dibersihkan.

Sanitizer pada saat program berjalan#

Flag peringatan tidak menjangkau masalah yang baru muncul saat program dieksekusi, misalnya luapan 2000000000 * 2. Sanitizer menyisipkan pemeriksaan runtime ke dalam biner, dan UBSan melalui flag -fsanitize=undefined menangkap kelas kesalahan yang membuat standar C++ berhenti menjamin perilakunya.

bash
# bangun ulang dengan UBSan, lalu jalankan programnya
g++ -fsanitize=undefined main.cpp -o /tmp/dbg && /tmp/dbg
text Keluaran
main.cpp:19:9: runtime error: signed integer overflow: 2000000000 * 2 cannot be represented in type 'int'
poin ganda: -294967296

Pesan runtime menyebut baris, operasi, dan tipe yang dilanggar, kemudian program tetap dilanjutkan. Nilai -294967296 yang tercetak bukan hasil yang dijamin standar, melainkan salah satu kemungkinan dari perilaku tak terdefinisi, dan itulah alasan operasi tersebut harus diperbaiki, bukan sekadar dipercaya hasilnya. ASan melalui flag -fsanitize=address melengkapi UBSan untuk kesalahan memori seperti akses di luar batas dan use-after-free. Laporan ASan mencantumkan alamat memori dan jejak alokasinya, sehingga keluarannya tidak ditampilkan pada topik ini. Binari yang disisipi sanitizer berjalan lebih lambat dan memakai memori lebih banyak, sehingga keduanya lazim dipakai pada build pengujian dan debug, bukan pada build produksi.

Kesalahan umum#

  1. Program diperiksa dalam build yang berbeda dari yang bermasalah. Bug hanya muncul pada build dengan flag tertentu, tetapi pemeriksaan dilakukan pada build lain sehingga tidak dapat direproduksi. Penyebabnya adalah flag compiler yang berbeda antara build yang dijalankan dan build yang diperiksa. Perbaikannya berupa mencatat flag pada Makefile atau CMakeLists.txt, lalu mengulang pemeriksaan dengan konfigurasi yang sama.
  2. Peringatan dibiarkan menumpuk. Ratusan peringatan lama membuat peringatan baru yang penting tenggelam di antaranya. Penyebabnya adalah kebiasaan membiarkan keluaran build penuh peringatan. Perbaikannya berupa membersihkan peringatan sejak sekarang dan menambahkan -Werror agar tidak bertambah lagi.
  3. Kesimpulan diambil dari satu build saja. Program ber-UB dapat tampak benar pada satu compiler atau satu flag optimasi, lalu berubah di tempat lain. Penyebabnya adalah menganggap keberhasilan satu build sebagai bukti kebenaran. Perbaikannya berupa membangun ulang program dengan sanitizer sebelum menyimpulkan kode yang mencurigakan sudah benar.

Ringkasan#

  • Alur kerja debugging berjalan berlapis, yaitu peringatan compiler terlebih dahulu, kemudian sanitizer, dan debugger untuk kasus yang paling sulit.
  • Breakpoint menahan eksekusi pada baris tertentu, dan perintah run, print, next, serta backtrace menelusuri keadaan program pada keadaan jeda tersebut.
  • -Wall -Wextra menandai parameter dan variabel yang tidak terpakai serta perbandingan tipe campuran tanpa biaya runtime apa pun.
  • UBSan menangkap perilaku tak terdefinisi saat program berjalan dan melaporkan baris serta operasinya, sedangkan ASan menutup kelas kesalahan memori.

Referensi#