The Art of Debugging

Writing unorganised code for something to work is an easy task, just put anything anywhere and see if the result is coming or not. If the result comes out in first attempt then you are really lucky but normal people like me are not. Then what we should do to check where the problem is occuring, we put some print statements (or console.log) to check the result in various places and observe where the output is deviating from its path. Well, it itself is a good practise of debugging the code but it does not work always especially not with large projects. So what should we do then ? According to me, we should give debugger a chance to find the bugs in our program. Debugging is an important part of writing programs because it teaches you to write unit tests for the parts of the program where it can fail.

To perform debugging, we need a debugger and thankfully we have one, gdb (GNU debugger). In this article we will start by seeing some basic debugging techniques which involves logging the output and assertions. Then in the next article, we'll use gdb which will do the heavy lifting for us and will tell us what is going in our program internally. So the topics we'll be covering in this article will be :-

Method of debugging and what to avoid

The basic approach to debugging our program follows the sequence of following steps :

  1. Know what your program is supposed to do.
  2. Detect when it doesn’t.
  3. Fix it.
This seems pretty simple, right ? In reality it is not that much simple. Detection the bugging point is a tedious job itself. Also there are some tempting mistakes which we do when we see wrong output. First mistake is, we don't follow the first step and just randomly tweak the code until it works. Don't do this because it will eventually mess up your code and even if it works(which i doubt), you will end up with a hard-to-maintain code. The second mistake is to attempt to intuit where things are going wrong by staring at the code or the program’s output. Like if see a segmentation fault and we know that we have used pointers in our program then we keep staring at the pointers code and try to see where the reference is going wrong. Stop doing this and let the computer find itself the mistake using debugging tools.

Basic debugging

Usually there are programs which consist only 50-100 lines of code and 2-3 functions then there basic debugging techniques can be applied to find the bug easily. If basic debugging doesn't help you then move straight to use gdb instead of applying these techniques too much. The 2 basic methods of basic debugging are:

  • Logging output to console
  • Assertions
Lets see them one by one

Output Logging

Output logging basically means printing the output our program generates bewteen various parts. In C, we use printf function to print out the variables and we can see their state in between our program lifecycle. But priting the variables using printf has some disadvantages. Some of these disadvantages are:

  • We can't change the output without editing the code.
  • A lot of print statements can create mess and we'll get confused which part is generating which output.
  • The output can be misleading: in particular, printf output is usually buffered, which means that if your program dies suddenly there may be output still in the buffer that is never flushed to stdout. This can be very confusing, and can lead you to believe that your program fails earlier than it actually does
There are some rules of thumb to follow when using output logging technique.
  • Use fprintf: Instead of printf statement, use fprintf(stderr, ...). It will keep your regular output seperate from debugging output.
  • Flush the output: If you still want to use printf then call fflush(stdout) to prevent buffering problem.
  • Use #ifdef: Wrap your code inside #ifdef It will allow you to turn your debugging code on/off according to your needs.
Lets see an example of it in a quicksort program.
#include<stdio.h>
#define DEBUG
void qsort(int *arr, int l, int r)
{
 int i ;
    if(r>l)
    {
        int index = partition(arr, l, r ) ;
        #ifdef DEBUG
        for(i=0 ; i<5 ; i++){
            printf("%d ", arr[i]) ;
        }
        printf("\n") ;
        fflush(stdout) ;
        #endif
        qsort(arr, l, index) ;
        qsort(arr, index, r) ;
    }
}
int partition(int *arr, int l, int r)
    {
    int i=l, j=l ;
    int pivot = arr[r-1] ;
    while(j<r)
        {
        if(arr[j]<pivot)
            {
            int temp = arr[i] ;
            arr[i++] = arr[j] ;
            arr[j] = temp ;
        }
        j++ ;
    }
    int temp = arr[r-1] ;
    arr[r-1] = arr[i] ;
    arr[i] = temp ;
    return i ;
}
int main(){
    int i, arr[5] ;
    for(i=0 ; i<5 ; i++){
        scanf("%d", &arr[i]) ;
    }
    qsort(arr, 0, 5) ;
    for(i=0 ; i<5 ; i++){
        printf("%d ", arr[i]) ;
    }
    return 0 ;
}
This is a simple quicksort function but there is some error in it. To debug it, i have added a loop which prints my array after each partition in the qsort function. Also i have wrapped that piece of code inside a #ifdef block. I have switched on the debugging by defining DEBUG using #define DEBUG. You can comment that line if you want to turn off debugging(which means the code inside #ifdef block won't execute). Now if you try to run it then you will infinite array prints. Also those arrays are all sorted. So it suggests that our code is working fine but it is not stopping after sorting the array. What might be the reason for endless recursion. One can think may be our recursion condition is not right. And that is the bug in our program. If you change the recursion condition if(r>l> to if(r>l+1), you program will start behaving correctly. Now you can turn the debugging off by commenting #define debug.

Assertions

We just saw the ouput logging technique and how to use it smartly and carefully. Now lets see another technique of basic debugging. In this technique we'll use C assert library. Every non-trivial C program should include <assert.h>, which gives you the assert macro. The assert macro tests if a condition is true and halts your program with an error message if it isn’t. A basic example could be:

#include<stdio.h>
#include<assert.h>
int main(){
 assert(2+2 == 5) ;
 return 0 ;
}
When you will run the above program, the assertion will fail and you will see an error message. Assertions are useful because you won't have to change your code to get more debugging output. Assertions can be used to verify assumptions made by the program and print a diagnostic message if this assumption is false.

So far we have seen how can we debug our sweet simple programs but when our programs grow in size, they become more nasty. And to handle those, we'll be needing a more powerful tool to help us. In the next article, we'll see that mighty tool (gdb) and will see how to use it, when to use it and other tools to use along with gdb. If you have any query then drop a comment below and i'll be willing to help you out.

Comments

Popular posts from this blog

Search box for blog or website

Image Search Engine Using Python

Cordova viewport problem solved