Introduction to debugging

"If debugging is the process of removing bugs, then programming must be the process of putting them in."                                                                                                                                                                                                                                                                                       - Edsger W. Dijkstra

Here's the situation: your program doesn't work, and you have no idea why. To fix this mysterious issue in your code, you have added several print statements and enabled trace logging, too. Still no luck. Worry not, for you are not alone! Every programmer has been there and has spent countless hours finding that one nasty bug that brought havoc into production.

Errors and deviations in software are referred to as bugs, and the act of removing them is termed debugging. Debugging is a controlled and systematic approach to examining the cause and effect of a fault in software. It's an essential skill to learn for anybody that's interested in gaining more insight into how their program behaves and runs. However, debugging is often not an easy task without the right tools, and the developer can lose track of what the actual bug is or might even be looking for bugs in the wrong place. The approaches we use to identify bugs in software can greatly affect the time taken to squash them and continue on a happy path. Depending on how complex a bug is, debugging is usually approached in one of the following ways:

  • Print-line debugging: In this method, we sprinkle print statements in the required places in the code, where we suspect the bug may possibly modify application state, and monitor the output when the program is run. This is simple, crude, and often effective, but it's not possible in every situation. This technique requires no extra tools, and everybody knows how to do it. It's actually the starting point of debugging for most bugs. To aid with print-line debugging, Rust provides the Debug trait, which we have already used many times before, and the dbg!, println!, and eprintln! family of macros.
  • Read-Evaluate-Print-Loop-based debugging: Languages that are interpreted, such as Python, often come with their own interpreter. An interpreter provides you with a Read-Evaluate-Print-Loop (REPL) interface, where you can load your program in an interactive session and examine the state of variables step by step. It is very useful in debugging, especially if you've managed to properly modularize your code so that it can be invoked independently as functions. Unfortunately, Rust does not have an official REPL, and its overall design doesn't really support one. However, there have been some efforts on this with the miri project, which can be found at https://github.com/solson/miri.
  • Debuggers: With this approach, we compile our program with special debugging symbols in the resulting binary and use an external program to monitor its execution. These external programs are called debuggers and the most popular ones are gdb and lldb. They are the most powerful and efficient methods of debugging, allowing you to inspect a lot of details about your program at runtime. Debuggers give you the ability to pause a running program and examine its state in memory to find out the specific line of code that introduced the bug.

The first two approaches are quite obvious, and so we don't need to go through them here. This leaves us with the third approach: debuggers. Debuggers as a tool are very simple to use, but they are not easy to understand and are often not introduced properly to programmers early in their careers. In the next section, we'll go through a step-by-step process of debugging a program written in Rust with gdb. But before that, let's get to know a little about debuggers.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset