Intro To Debugging - Joe Linhoff - 10/27/2014 - page 1

Introduction To Debugging

Content and Concepts

●Workflow

●Breakpoints

●Memory Model

●Bugs!

Debugging is an important skill that you need to learn and practice.

1.Workflow

Adopt a workflow that allows you to break tasks into small chunks. Keeping in mind that the size of the chunk should be no bigger than what you can fit into your head, code each chunk and test it before moving on to the next. Testing here involves stepping through every line of code and inspecting every variable. If you get distracted or detoured and can not remember all the steps you've taken to complete this chunk, start over.

Configuration and Customization, MSV Studio

Remove as much clutter as possible from the interface. Disable toolbars that you are not using. The dialog shown in the screenshot (Tools > Customize.. > Toolbars) shows the active toolbars. I only enable the Debug toolbar and drag other tools I need into this one toolbar.

The toolbar looks like this:

/ (tools shown in the left toolbar)
●Start Debugging
●Build Solution
●Delete All Breakpoints
●Stop Debugging
●Show Next Statement
●Step Over
●Step Into
●Step Out
●Run To Cursor
●Hexadecimal Display

Expert Settings

For some of the debugging features discussed, you must sure expert settings are enabled. (Tools > Settings > Expert Settings)

Create Hello World

Create a new, general, empty project for this worksheet.

Create a new file, main.cc, and type in the hello world code:

#include <stdio.h>

int main(int argc,char *argv[])

{

printf("Hello World\n");

return 0;

} // main()

// EOF

2.Breakpoints

Double click to the left of a line of source code to set a breakpoint at that line. This area, to the left of the code, is called the gutter. Double click to the left of the return statement, click the green arrow to build and run.

The breakpoint is hit. The yellow arrow signals the program counter (PC).

●What is the value of argc?

●What type is argv?

●How do these two variables work together and what are they used for?

Inspecting Variables

Hold the cursor over argc and wait for the debugger to show you the value. You can change the value if you like. Cursor over argv to inspect its value.

●What is the value of argv[0]?

●Why doesn’t the debugger give you more than 1 value for argv?

●What is the value of argv[1]?

Watch Windows

Watch windows allow you to view / change the values of variables as you step through the program. (Debug > Windows > Watch > Watch 1) Setup the watch window to view argv[0]. Type the expression in the Name field.

●Click the magnifying glass to see what that does. What does M$ call this?

Console Window

Windows brings up a console window to display the printf() calls (which uses stdout).

Continuing

Double click the breakpoint to clear it, or else click the Delete All Breakpoints tool (. Breakpoints can be set or cleared while the code is running. Press the Start Debugging tool (green arrow) to continue.

Advanced Questions

●What is the difference between the types: char **argv and char *argv[]?

●How can you change the value of argv[0]?

●With the PC stopped at the return-breakpoint, why does pressing the close box in the console window not close the program immediately?

●Break the program at the printf(), press the close box on the console window. Describe and explain the action when you single step.

Dragging The Instruction Pointer

A time saving technique is to drag the stopped instruction pointer to another line before continuing execution. Grab the instruction pointer, signified by a yellow arrow, with the mouse cursor and drag it carefully to another line. This allows you to change code and re-run the section without restarting the program.

Go To Definition

It is often useful to find a definition. Click and F12 or right click on the item, and Go To Definition. MSVC uses a database to find definitions.

Coding Breakpoints

Often while coding, you would like validate parameters, or flag a case that should never happen. This is especially beneficial during development.

Create the BRK() macro to break into the debugger when it is hit.

#define BRK() __debugbreak()

Asserts are meant to catch logic errors: things that can never logically happen, and are impossible to recover from. If your program hits an assert, the assert usually kills the program. Don’t code this way!

Code To Continue

Use BRK()s to help you find the problems without killing the program. Do your best to recover or abort the operation non-fatally.

You only want the BRK() to break when you are actually debugging. When you build a release version, you do not want the BRK() macro to expand to break the debugger -- this will crash your program.

The first step to make this happen is to define the BRK() macro so it only has an effect when you are debugging.

#define DEBUG 1

#if DEBUG

#define BRK() __debugbreak()

#else // !DEBUG

#define BRK() /* nothing */

#endif // !DEBUG

Setup The DEBUG Define

Move the DEBUG define into the configuration.

(Project > __ Properties..) (Configuration Properties > C/C++ > Preprocessor > Preprocessor Definitions)

DEBUG=1;

The DEBUG define is set in the Debug configuration, not in the Release configuration.

Don’t Change The Flow

When surrounding code with #if DEBUG .. #endif // DEBUG statements, be very careful to not change the program flow. If the execution path of your program changes when you release it, i.e. turn off DEBUG, then you are in for a world of hurt.

Using BRK() statements, do not change the flow.

Similarly, do not do things in the DEBUG version differently than in the non-DEBUG version.

#if DEBUG

if(rec == 0) // test for null pointer

{

BRK();

printf("* NULL pointer found\n");

return -1; // this is WRONG -- it will only save your DEBUG version

}

#endif // DEBUG

if(rec == 0) // test for null pointer

{

#if DEBUG

BRK();

printf("* NULL pointer found\n");

#endif // DEBUG

return -1; // this is OK -- it works the same in DEBUG and non-DEBUG

}

Testing The Release Version

Test everything. Test the release version. Place a BRK() in the program and build both a debug and release version. (Build > Configuration Manager..)

Use Windows Explorer to navigate to the Debug folder, launch the .exe. Do the same for the Release .exe. If you run the Debug version, it should crash.

●Why does the program crash when it hits the BRK() in the debug version?

●What code is generated for BRK() in the release version?

Break At Memory Access

The debugger can also break when a memory location is modified. This can be extremely helpful in tracking down difficult bugs. (Debug > New Breakpoint > New Data Breakpoint)

Give the address (i.e. &var) to break when var is changed.

Design To Debug

If you can see it, you can debug it.

Design and create code that is easy to debug.

●objects have unique string names

●debug-flag objects at one point and look for them in another

Advanced Questions

●How can you modify the BRK() macro to not-crash when the debug version is run outside the debugger?

●Describe the code that calls our main() in hello world.

●What is the third parameter to main() and how is it used?

  1. The Memory Model & Call Stack

When you launch your program, the loader loads your packed code and data into memory, unpacks, patches, sets up a stack and starts execution.

Simplified Runtime Environment

physical & mapped memory address space: 0..64 bits (lots of memory)

[data,code] -- most of the .exe is loaded & unpacked into memory

[stack] -- allocated from the heap when loaded, fixed size

[heap] -- everything else, may have limits

The Call Stack

Another useful debugging window is the call stack window. It shows the stack at each call. Place a breakpoint at the printf in the hello world program again, and look at the call stack at this point.

●How many entries are shown in the call stack window?

●Click on the entry that calls our main()

When you select a call stack entry, that frame and all its locals are visible and viewable.

Allocations, New

Every variable and object lives in memory and is usually in stack, heap memory.

struct Record {

int ival;

float fval;

}; // struct Record

int main()

{

int stackvalue;

Record stackrecord;

Record *rec;

// stack (local) storage

stackvalue=123;

printf("value=%d\n",stackvalue); // print as decimal

// what is the address of the memory stackvalue uses?

// stack storage

rec = &stackrecord; // point to stackrecord

rec->ival=999;

printf("rec ivalue=%d\n",rec->ival); // print as hexadecimal

// when is the constructor called?

// how is the stackrecord freed?

// what is the address of rec?

// heap storage

rec = new Record(); // create w/initializer

rec->ival=999;

printf("rec ivalue=%d\n",rec->ival); // print as hexadecimal

// what is the address of rec?

delete rec; // destroy

return 0;

} // main()

Use The Watch Window To Inspect

To find the address of any variable, use the & operator. To find the address of a variable (i.e. where the var is stored in memory): &stackvalue

  1. Bring On The Bugs!

Bugs are easy to find and fix when they are isolated. This makes writing short questions difficult. This also is your goal when bug hunting -- find and isolate the bug, play with it, learn about it and learn from it. You usually must find and fix bugs inside the framework of a complex program.

There are a lot of different kind of bugs, some of the types:

●syntax errors, misspellings

●divide-by-zero errors

●floating point overflows

●floating point corruptions

●memory leaks

●dangling pointers

●using uninitialized vars

●stack overflow

●logic bugs

●swapped parameters

●shadowed or wrong variable use

●race conditions

●multi-thread-related bugs

●lots more!

Setup a single file to test all kinds of bugs.

// file: main.cc

#include <stdio.h>

#define BUG_SYNTAX 0

#define BUG_MISSPELLING 0

#define BUG_UNINITIALIZED 1

#if BUG_SYNTAX

int main(int argc,char *argv[])

{

printf("Hello World\n")

return 0;

} // main()

#endif // BUG_SYNTAX

#if BUG_MISSPELLING

typedef struct {

int value;

} Record1;

int main()

{

Record1 rec;

rec1.value=111;

printf("value=%d\n",rec1.value);

return 0;

} // main()

#endif // BUG_MISSPELLING

#if BUG_UNINITIALIZED

int main()

{

int value;

printf("value=%d\n",value); // print as decimal

printf("value=%x\n",value); // print as hexadecimal

return 0;

} // main()

#endif // BUG_UNINITIALIZED

// END OF FILE

  1. Extra Credit Questions

Here are a few questions that you should be able to answer with a little extra work in this lab.

FLOW

  1. In the debugger, what is the difference between stepping over and stepping into a function?
  2. What happens when a program hits, and fails, an assert()?
  3. What happens when a program hits a BRK()?
  4. Why is it bad to change the program flow with DEBUG or other defines that change at production?

MEMORY

  1. What is the hex value of uninitialized stack memory when debugging in MSVC? View values as hexadecimal. This will show up in integer variables, the same values are stored in floats and all variable types.
  2. What is the hex value of uninitialized heap memory?
  3. What is the hex value of freed heap memory?
  4. How does MSVC set the values for uninitialized memory on the stack? On the heap?

STACK & HEAP ADDRESSES

int main()

{

int stackvalue;

Record stackrecord;

Record *rec;

// more code

} // main()

  1. How do you get a pointer to a fundamental type, such as an integer?
  2. How many bytes are in a 32 bit integer?
  3. The address of the variable stackvalue is 0x0038fbe8, the address of the variable stackrecord is 0x0038fbd8, the address of the variable rec is 0x0038fbcc. If the vars are packed tightly, how many bytes are each of the variables? (OK to indicate which addresses would you subtract if you had a hexadecimal calculator)
  4. After rec = new Record(); the value of the variable rec is 0x00171290. Why is this address so different from the 0x0038fbd8 addresses?
  5. (trick question) After rec = new Record(); what is the address of the rec variable?

FLOATING POINT

  1. How can you get a variable to register as infinity?
  2. How can you test for infinity?
  3. How do you get a variable to register as NaN / indeterminate?
  4. How can you test for indeterminate?
  5. Will a divide by zero always crash your program?