Codementor Events

Basic Debugging

Published May 22, 2019Last updated Nov 17, 2019

Debugging is a skill you're going to need.  Programs rarely work the first time, and we need to be able to figure out what is going wrong.

Debugging can be accomplished a number of ways: two of them are with by logging or by running a debugger (sometimes both).  Either way, the goals is to observe the program's operation, and determine where things are going awry.

Fundamentally, we are interested in two things:

  1. validating that variables are being updated (set) as expected, and,
  2. validating control flow is working as expected.

Once these two things are working, generally speaking, most programs will work, or at least produce more expected results.

In order to debug programs well, it helps a lot to write "simple code"! .  "Simple code" uses more of shorter lines instead of fewer of longer lines.  The shorter lines are connected to each other through local variables.

In most languages today, especially C, C++, Java, and C#, using local variables is virtually free as they are allocated either on the stack (very inexpensive) or directy to machine registers (completely free) — further if the local variable is used just once — as often happens when programming in this simple style — then the compiler (when optimizing) can fully eliminate the local variable.

Further, let's also note that this approach of "simple code" is applicable to compile-time error fixing as well as runtime debugging.  If the compiler is complaining about something, in many languages the error message will be much more informing when using simple code — where there is only one major operation happening on each line of code.  If program is crashing at runtime (e.g. segmentation fault, or Null Pointer Exception), using simple code will help to zero in on the exact operation that caused the fault — since with simple code we reduce the number of operations per line.

It is possible to do to much simple code — but really, the only downside is verbosity.  So, when you're done debugging, you can reassemble some things into larger statements if you like.  However, in the other direction, we can apply simple code in the extreme as an approach to debugging things in an area of code where we don't understand what is wrong: in the most extreme, we would do only one operation (e.g. adding, calling, dereferencing) per line using as many variables as needed to connect them.

Let's look at some examples of how to write simple code:

if ( ButtonEvents () & BUTTON_1_DOWN ) {
    ...
} else {
    ...
}

Since the condition being tested involves a function call, we instead capture its return value in a local variable as follows:

int be = ButtonCheckEvents ();
if ( be & BUTTON_1_DOWN ) {
    ...
} else {
    ...
}

This simplification separates a larger more complex if condition into two lines that are connected by a variable (be).  Here, the function call result is captured in a variable, and by doing this we can inspect its return value much more easily, whether using a debugger or logging with printf.

Next, we want to verify variables and control flow.  As we have separated the primary expression from the original if statement, it is appropriate to first see the value of the new variable.

int be = ButtonEvents ();
printf ("be = %d\n", be );	// new printf added for debugging (check variable value)
if ( be & BUTTON_1_DOWN ) {
    ...
} else {
    ...
}

*We could go even further, even capturing * be & BUTTON_1_DOWN into a local variable, reducing the if condition to testing just one variable.

Also, we need to verify the flow of control:

int be = ButtonEvents ();
printf ("be = %d\n", be );
if ( be & BUTTON_1_DOWN ) {
    printf ( "Button 1 down...\n" );	// new debugging printf, flow of control
    ...
} else {
    printf ( "Button 1 not down...\n" );	// new debugging printf, flow
    ...
}

With flow of control, we're looking to see that the overall flow works, whether we are missing an else part, testing the wrong condition, missing a break; statement in a switch statement, off-by-one error in loop control, etc..

You can see that some printf statements are added just to indicate the flow of control that is happening in the program.  Others are used to show the values of variables after their assignment.

When using a debugger, the idea is to obtain the same information: variable assignment, to see if reasonable values are being computed, and, flow of control, whether it is working or not.

As mentioned before, once the flow of control is working properly and variable assignments are working, the most programs will work!

Sometimes, the printfs become to noisy, and to see what the problem is, we need to reduce the chatter.  When we're doing printf debugging, we can use simple if-statements to restrict the number of printf's coming out:

int be = ButtonEvents ();
if ( count == 100 )						// if condition guard to reduce printfs
    printf ("be = %d\n", be );
if ( be & BUTTON_1_DOWN ) {
    printf ( "Button 1 down...\n" );
    ...
} else {
    printf ( "Button 1 not down...\n" );
    ...
}

As if-conditions that guard/reduce debugging printf's we can either test existing program state (variables) or introduce new state as needed (it goes wrong on the 100th count: do we already have a count for that or should we create a counter?).

When we're using a debugger, we can sometime introduce a conditional breakpoint, if the debugger supports that concept.  However, these may run slower than a regular breakpoint, whereas putting an if-statement testing state may be better:

int be = ButtonEvents ();
if ( count == 100 ) {
    int a = 1;	// good line to put break point in debugger
}
if ( be & BUTTON_1_DOWN ) {
    printf ( "Button 1 down...\n" );
    ...
} else {
    printf ( "Button 1 not down...\n" );
    ...
}

This construct has two effects: it provide a good place to put an unconditional break point (while still only breaking on the condition of interest), while also (in most languages) providing a compiler warning that "a" is unused: this is good to help us remember to remove these kind of additions after the code is working.

Assignment statements can also be simplified, in the sense that if too much work is going on, we can break the logic into two statements, also connected by local variables.

In summary, write "simple" code.

Discover and read more posts from Erik Eidt
get started
post commentsBe the first to share your opinion
Mike Bell
5 years ago

I don’t see a lot of actual debugging discussed in this post so much as rewriting/refactoring code in order to be easier to debug. Simple code can still have bugs in it such that you’re unable to simplify the code further. At this point, real debugging can begin.

deanm5
5 years ago

How about this as an alternative to (lots of) debugging?
(1) Write a specification for what you intend to develop. A clear software requirements specification (“SRS”) helps immensely – it’s not just “paperwork”.
(2) Break large programs into smaller modules – and write requirements specs. for the modules.
(3) Now that you have the requirements fully established, write a testbench for the item in question. Do that before writing a line of code!
(4) As you develop each piece of code, run the testbench for that code. M ake sure that the code passes the tests before moving on.
(5) If you need to refactor the code, re-run the testbench – make sure that you didn’t break something.
We have found that this (test driven development or “TDD”) has dramatically reduced the amount of debugging needed, and has led to much higher quality (fewer residual bugs). We have also found (perhaps surprisingly, perhaps not) that the total time to market (and development cost) is reduced when using TDD.

Luis O. Freire
5 years ago

Outside of using debugger, the variable assignment strategy is something I used often in the early years of software design. That quickly faded away as third party libraries and modules became part of the problem. An outdated library (in C for instance), could bring the execution to an abrupt end without any indication of the source of the problem.

Show more replies