Bug, Debugging, dan Debugger

Baru saja ada seseorang yang bertanya kepada saya mengenai cara mendebug program buatannya. Sebentar saya merasa heran: masak nggak bisa pake debugger? lalu ketika saya mencoba mengingat-ingat, di sebagian besar buku dan kuliah mahasiswa tidak diajarkan mengenai bug yang umum, cara mencari bug, dan cara menggunakan debugger itu sendiri.

Kemampuan mencari bug di program sendiri ini juga menjadi dasar untuk mencari bug security. Kalo kita bisa menemukan kesalahan yang kita buat sendiri, akan lebih mudah untuk mencari kesalahan di program orang lain.

Bug

Bug adalah segala macam cacat dalam program. Bisa saja cacatnya hanya berupa tampilan yang sedikit salah, bisa crash, bisa berupa bug security (harusnya hanya bisa diakses user X, bisa diakses user Y), kadang bug tertentu tidak muncul sampai kasus ekstreem (misalnya jika jumlah user banyak maka akan out of memory karena ada memory leak).

Sumber bug bisa banyak, dari mulai salah design, salah implementasi, salah konfigurasi, dsb. Sebagai programmer, bug yang akan saya bahas adalah dari sisi implementasi (coding). Biasanya seorang programmer akan belajar sedikit demi sedkit jenis bug dari pengalaman. Semakin banyak coding, semakin banyak salah, semakin banyak belajar.

Beberapa jenis bug sangat umum di semua bahasa (kesalahan logika, kesalahan aritmatika, dsb), tapi bug tertentu hanya muncul di bahasa tertentu atau jika menggunakan framework tertentu. Jadi pengalaman coding tetap merupakan guru yang utama.

Contoh bug yang sederhana adalah lupa menginisialisasi atau mereset variabel. Biasanya jika program berjalan benar pertama kali, lalu ketika diulangi tidak benar hasilnya (dan benar lagi ketika aplikasi direstart) maka penyebabnya adalah lupa mereset variabel.

Contoh lain adalah menggunakan try catch secara malas, misalnya dengan membungkus seluruh fungsi dalam satu try except. Dengan cara ini, maka tidak jelas apa sumber error di sesungguhnya.

Contoh bug yang spesifik terhadap bahasa adalah bug manajemen memori di C. Kesalahan mengakses memori di C sangat mudah terjadi dan biasanya akan menyebabkan crash.

 

Source Control

Jangan heran juga kalau kadang bug baru muncul karena hal sepele, misalnya source code berubah  karena keyboard tersenggol. Misalnya sebuah konstanta 1000 terdelete angka nol terakhirnya (atau bertambah satu), atau tanda “lebih besar sama dengan” terhapus  menjadi lebih besar saja. Di sini penggunakan version control (yang populer saat ini: Git) sangat berguna, Anda bisa mereview segala perubahan pada source code.

Source control juga memungkinkan kita melihat sejarah perubahan secara keseluruhan. Bagi security researcher, ini sering menjadi ladang untuk mencari bug baru di software open source. Jika ada yang menambahkan fungsionalitas baru, maka kemungkinan ada bug baru yang muncul.

Unit Testing

Mencari dan membetulkan bug itu susah (baik bug sendiri maupun orang lain), jadi hal pertama yang perlu dicoba adalah: menghindari bug. Hal yang paling dasar adalah: kita perlu memecah program kita menjadi unit yang kecil (unit di sini berupa fungsi/kelas/method) dan menguji tiap bagian kecil tersebut. Istilahnya untuk ini adalah unit testing. Kita bisa menggunakan library tertentu untuk unit testing, atau panggil saja fungsi yang akan ditest.

Saya akan mengasumsikan Anda minimal sudah bisa membuat aplikasi yang bisa dicompile dan bisa dijalankan. Jika Anda masih salah syntax, salah opsi untuk mengcompile, dsb, sebaiknya ada mulai lagi dari nol dan mulai perlahan.

Dengan memecah program menjadi unit kecil dan mengujinya satu persatu, maka kesalahan umumnya bisa dilokalisasi: fungsi X ini masih salah. Pertanyaan berikutnya adalah: apa salahnya?

Asersi

Saat ini kebanyakan bahasa memiliki fitur assert. Fitur ini memungkinkan kita melakukan pengecekan ekstra (yang biasanya tidak akan dilakukan di versi produksi). Assert ini biasanya digunakan untuk menyatakan kondisi yang pasti harus benar, karena jika tidak benar maka titik program berikutnya akan error fatal.

Ini adalah contohnya:

assert(index >= 0);

val = values[index];

Jika index kurang dari 0, maka ada sesuatu kesalahan fatal di program kita (mungkin kesalahan logika, mungkin ada hal yang lupa ditangani). Jika asersi diaktifkan biasanya program akan berhenti dengan “Assertion error”.

Di berbagai bahasa assert ini bisa diaktifkan/nonaktifkan dengan opsi tertentu. Jadi dalam versi rilis kita bisa membuang berbagai pengecekan asersi ini.

Pesan dari Compiler dan Linter

Sebelum melihat jauh ke mana-mana, coba lihat dulu apakah ada warning dari compiler Anda. Contohnya untuk potongan kode C ini:

 int i;
 printf("i = %d\n", i);

Compiler memberitahu:

warning: ā€˜iā€™ is used uninitialized in this function [-Wuninitialized]

Bacalah pesan error dan warning dengan teliti. Di situ dijelaskan bahwa variabel i tidak diinisialisasi. Di C, variabel lokal nilainya tidak bisa diprediksi jika tidak diinisialisasi.

Selain compiler, biasanya ada program yang namanya linter untuk sebuah bahasa yang bisa memberi peringatan jika ada sesuatu yang dianggap tidak standar atau aneh. Anda bisa melihat daftar lengkap software semacam ini di Wikipedia. Sebagian software ini mudah sekali diinstall, dan sebagian lagi terlalu rumit, tapi beberapa IDE sudah memiliki linter built in. Saran saya: jika Anda masih belajar dan tools linter tersedia, pakailah dan perhatikan outputnya, jika terlalu sulit mensetupnya ya sudah lupakan saja untuk saat ini.

Print debugging

Lalu berikutnya cara paling primitif dalam debugging adalah dengan menuliskan nilai saat ini menggunakan: “print” “echo”, “printf” atau sejenisnya.

Contoh sederhana, misalnya kita punya conditional di Python seperti ini (melakukan regular expression matching):

if (re.search(PATTERN, x)): # do something

Lalu bagian aksi tidak pernah dieksekusi, atau cuma dieksekusi beberapa kali. Apakah Anda yakin patternnya benar? Coba print sebelumnya, jangan-jangan regexnya masih salah untuk input tertentu:

print "Input ", x, "Match result ", re.search(PATTERN, x)

Tentunya tidak semua aplikasi bisa punya layar yang menampilkan teks itu (contohnya Game, atau aplikasi grafik lain). Biasanya cara yang lebih baik adalah menggunakan library untuk logging yang saat ini sudah built in di banyak bahasa (misalnya ada modul logger di Python, java.util.logging di Java, dsb). Output logging bisa dikonfigurasi sedetail apa, dan outputnya ke mana (ke file, ke terminal, ke jaringan, dsb).

Logging tidak hanya untuk proses development. Banyak aplikasi menyediakan fungsi logging untuk mencatat berbagai hal yang berhubungan dengan aplikasi untuk mendebug jika ada masalah ketika dijalankan. Contohnya aplikasi bisa menampilkan di lognya: “berusaha melakukan koneksi database ke server XX port YY dengan username ZZ”, yang bisa diikuti dengan “Koneksi berhasil”, atau “Koneksi Gagal, pesan error: invalid username or password”.

Debugger

Debugger adalah tools untuk mencari bug. Di kebanyakan IDE (Integrated Development Environment), tools ini sudah built in dan mudah dipakai. Ada juga debugger stand alone, tapi biasanya ini lebih sulit bagi pemula.

Untuk menggunakan debugger pertama kita perlu mengeset breakpoint, yaitu titik berhenti sementara (break). Ketika kita mengklik “debug” maka debugger akan mulai menjalankan program sampai titik itu. Di beberapa bahasa, jika kita tidak mengeset breakpoint, maka program dihentikan sementara di titik awal.

Debugger paling dasar sekalipun akan memiliki fungsi untuk menampilkan nilai variabel saat ini. Dengan menggunakan ini kita biasanya tidak perlu mencetak dengan printf, tapi untuk mencetak objek yang kompleks, kadang tetap saja keluaran print masih lebih baik.

Fungsi debugger berikutnya adalah melakukan single stepping, artinya kita bisa menelusuri langkah demi langkah program. Contohnya jika kita memiliki source sederhana

diskon = 0.1

harga = 10000

harga = harga - (diskon * harga)

Kita bisa melakukan single step dan melihat nilai diskon dan harga di setiap langkah.

contoh

Jika kita memanggil fungsi lain, maka kita memiliki dua opsi, masuk ke dalam fungsi itu (istilahnya biasanya “step into”) atau memanggil fungsi itu, lalu segera meneruskan ke baris berikutnya (kita tidak peduli atau tidak ingin mendebug fungsi itu, istilah ini “step over”).

Jika fungsi yang akan kita cek sangat jauh atau hanya dipanggil di kondisi tertentu maka kita bisa menggunakan breakpoint. Biasanya di berbagai IDE ini bisa dilakukan dengan mengklik di samping nomor baris, dan akan muncul bulatan merah. Jika kita menekan tombol “continue”, maka program akan dijalankan dan akan dihentikan sementara di titik breakpoint tersebut.

Ada juga yang namanya “conditional breakpoint”. Artinya kita akan berhenti hanya pada kondisi tertentu. Misalnya kita ingin berhenti dalam loop setelah i > 100, karena di titik itu ada data yang aneh, maka kita bisa menambahkan kondiri “i>100”

conditional

Beberapa debugger memiliki fungsi yang menakjubkan, misalnya GDB mendukung reverse execution dan Visual Studio mendukung “Edit and Continue”. Untuk perubahan kecil, kita bisa mengubah program kita tanpa menghentikan debugger.

Di beberapa environment mensetup debugging tidak mudah, tapi ketika sudah terinstall ini akan sangat berguna sekali.

Debugger level Assembly

Untuk program biasa, Anda biasanya tidak akan pernah butuh mendebug sampai level ini, tapi jika Anda membuat eksploit atau berusaha mengeksploit binary, Anda perlu tahu ini (saya pernah membuat pengantar mengenai buffer overflow jika Anda tertarik).

Debugger di level assembly sebenarnya tidak berbeda jauh dari level source code. Jika kita punya source codenya, bahkan kita bisa melihat keduanya berdampingan. Di level assembly kita bisa menelusuri tiap instruksi dan juga melihat nilai register. Dengan melihat nilai register beserta flagsnya Anda bisa belajar banyak mengenai assembly.

Tools lain

Kadang debugger saja tidak cukup, tools analisis runtime seperti strace, ltrace, valgrind dsb akan berguna membantu menemukan bug terutama di kode native. Topik mengenai tools sudah pernah saya bahas sebelumnya.

Logika

Tools apapun tidak akan bisa membantu jika kita tidak punya logika yang baik. Dasar dari debugging adalah mencari kesalahan program, lalu membetulkannya agar berjalan sesuai harapan. Jadi di tahap paling awal, Anda sendiri harus tahu harapannya apa yang terjadi di setiap langkah.

Contohnya jika Anda mendebug algoritma mencari nilai maksimum, Anda perlu mengerti dulu seperti apa seharusnya algoritmanya dan apakah ketika dijalankan sudah sesuai harapan.  Dalam kasus ini, di langkah pertama seharusnya nilai maksimum adalah elemen pertama, di langkah kedua, jika nilainya lebih besar maka elemen ini yang jadi elemen maksimum (dan jika tidak, maka elemen pertama yang tetap menjadi maksimum), dan seterusnya untuk elemen berikutnya.

Penutup

Sebenarnya bisa panjang sekali bahasan mengenai bug software, artikel ini hanya sekedar perkenalan. Anda sebaiknya mendalami berbagai tools supaya lebih produktif dalam bekerja, dari mulai tools source control, editor, debugger, linter, build system (Makefile, CMake, Ant, Maven, atau apapun), maupun tools lainnya.

Untuk Anda yang baru belajar memprogram: jika Anda menggunakan IDE, sesekali tekanlah tombol debug (atau menu debug) dan cobalah bereksperimen menggunakan fungsi dasar seperti step into, step over, dsb. Ini akan sangat membantu Anda dalam belajar pemrograman.

 

One thought on “Bug, Debugging, dan Debugger”

Tinggalkan Balasan

Alamat email Anda tidak akan dipublikasikan. Ruas yang wajib ditandai *