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.
// 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.
break mainmenanam breakpoint pada awal fungsimain, ataubreak main.cpp:19menanamnya pada baris tertentu.runmemulai eksekusi, dan program berhenti tepat sebelum baris breakpointnya.print nilaimenampilkan nilai variabel pada keadaan jeda, misalnyaprint batas.nextmelangkah ke pernyataan berikutnya tanpa masuk ke dalam fungsi, sedangkanstepmasuk ke dalam fungsi yang dipanggil.backtracemenampilkan rantai pemanggilan fungsi yang sedang berlangsung, berguna ketika program berhenti di dalam panggilan yang dalam.continuemelanjutkan eksekusi sampai breakpoint berikutnya, danquitmengakhiri 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.
# kompilasi dengan peringatan lengkap, tanpa menautkan program
g++ -Wall -Wextra -o /tmp/dbg.o -c main.cppmain.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.
# bangun ulang dengan UBSan, lalu jalankan programnya
g++ -fsanitize=undefined main.cpp -o /tmp/dbg && /tmp/dbgmain.cpp:19:9: runtime error: signed integer overflow: 2000000000 * 2 cannot be represented in type 'int'
poin ganda: -294967296Pesan 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#
- 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.
- 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
-Werroragar tidak bertambah lagi. - 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, sertabacktracemenelusuri keadaan program pada keadaan jeda tersebut. -Wall -Wextramenandai 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#
- Warning Options — GCC, Using the GNU Compiler Collection
— Daftar flag peringatan GCC, termasuk
-Walldan-Wextra. - Instrumentation Options — GCC, Using the GNU Compiler Collection
— Dokumentasi
-fsanitize=undefineddan-fsanitize=address. - Using an integrated debugger: Running and breakpoints — Learn C++ — Alur kerja breakpoint dan pelangkahan pada debugger IDE.