Take the 2-minute tour ×
Programmers Stack Exchange is a question and answer site for professional programmers interested in conceptual questions about software development. It's 100% free, no registration required.

To debug my programs I mainly use the following methods:

  1. Use printf (or equivalent in other languages) to check the value of a variable after a particular statement or to check whether the program enters a conditional statement or a loop.

  2. Use watch/breakpoints when using IDEs.

I am able to solve the problems using the above methods. But I have noticed that there are two kinds of builds - debug and release. From the name I can understand that debug build would be helpful in debugging. Also I have read that debug build stores something called symbol table information and stuff.

How can I use the debug build for debugging? Are there any other debugging techniques which I should learn?

share|improve this question
    
The difference depends on the platform. For Java the difference is small. For C it can be vastly differen.t –  user1249 Feb 14 '12 at 1:04
1  
what languages/platforms are you coding on? –  DXM Feb 14 '12 at 5:16
add comment

7 Answers

A debug build is built with debugging symbols in the executable. Basically what that means is that every line of source code is linked to the machine code that was generated from it, and all the names of variables and functions are kept. In GCC this is done with the -g flag when you compile source files. Another thing that is often done is to turn off optimisation, this is because the compiler does some neat tricks that makes your program go faster, but makes debugging impossible. In GCC this is done with -O0.

The single most useful tool that I have ever used for debugging is gdb. It is a text version of the IDE breakpoints that you mentioned. You can do a lot more with gdb than you can with an IDE though. In fact, some IDEs are just a wrapper around gdb, but some features are lost. You can watch memory locations, print assembly, print stack frames, change signal handling, and a whole lot more. If you are serious about debugging, I would learn gdb or some equivalent text-based debugging program.

Another thing that I often find useful is valgrind. It finds memory leaks and keeps track of whether memory is initialised or not. You use it with your debug build, because then you get line numbers where interesting things happen.

share|improve this answer
1  
Really? Every time I've had to use GDB I find it a significant step backwards from the power and ease of use I have at my fingertips with a good IDE-based debugger. Particularly since GDB is a highly general tool, whereas an integrated debugger can take advantage of knowing the rules of the language it was designed for to give you more specific and more relevant feedback. –  Mason Wheeler Feb 14 '12 at 0:30
1  
GDB is very much designed for C and C++. I used IDE debuggers for quite a while and they frustrated me no end. I find that I have so much more control over what I'm doing when I use GDB. –  Jarryd Feb 14 '12 at 2:51
2  
@MasonWheeler, gdb is scriptable, IDEs are normally not. I don't need any "power" at my fingertips when a computer can do all the work for me while I'm sipping my tea. –  SK-logic Feb 14 '12 at 9:30
3  
@MasonWheeler, no, you're doing something wrong when hitting "step-inspect-step-inspect" like crazy. It is so much easier to write a tiny script for verifying your current hypothesis about a cause of a problem, run it (pretty much the same way you're running the functional tests) and analyse the results, repeat if necessary. All the forms of an interactive debugging are almost always nearly useless, even if it is a post-mortem of a core dump. A typical debugging script would set up several break points, run, inspect and format nicely the values at the breakpoints. –  SK-logic Feb 14 '12 at 13:29
1  
@Mason Wheeler, a simple stack trace (bt in gdb) in most cases would provide you with more than enough information for a 0-hypothesis (if this is not the case, your code is, well, far below the sub-standard quality threshold). I've been using debuggers since VMS, tried all possible flavours, including some extreme exotic beasts like time unwinding debuggers, but so far could not find any really useful one. You're testing your assumptions interactively - then, you're wasting your time. I'm testing my assumptions in batch (could be many in parallel), quickly and with very little effort. –  SK-logic Feb 14 '12 at 17:35
show 7 more comments

Learn your Debugger

It's really helpful to get to grips with the debugger, whether it be text-based, full IDE or some blend thereof. You don't give much details so I'll describe the general case:

1) Breakpoints

In addition to just stopping at a line of code, many debuggers let you specify to break when a condition arises (e.g. "x > 5"), after a number of passes through the code or when some memory changes value. This is very useful for understanding how your code gets into a bad state, e.g. watching for when a pointer becomes null rather than catching the crash when it is dereferenced.

2) Stepping through code

You can step into functions, line-by-line along code, jump over lines ('set next statement') and then 'up' out of functions. It's a really powerful way of following your code execution to check that it does do what you think it does :-)

3) Evaluating expressions

So you can put variables into a Watch list/window and see their value change when you hit a breakpoint or step through code, but you can also usually do complex expression evaluations too, e.g. "x + y / 5" will be evaluated. Some debuggers let you put function calls into the watch lists too. You can do things like "time()", "MyFunction(...)", "time()" and get clock timing of how long your function took.

4) Exceptions and Signal handling

So if your language supports exceptions and/or signals, you can usually configure the debugger for how to react to this. Some debuggers let you break into the code at the point where the exception is about to happen, rather than after it has failed to be caught. This is useful for tracking down weird problems like "File Not Found" errors because the program is running as a different user account.

5) Attaching to a process/core

So sometimes you have to use the debugger to jump on an existing process which is going awry. If you've got source code nearby and the debug symbols are intact, you can dive in as if you'd started in the debugger in the first place. This is similar for core dumps, too, except you usually can't continue debugging in those (the process has already died).

Build Configuration

There are a number of build variations you can make through turning on or off features like debug symbols, optimisations and other compiler flags:

1) Debug

Traditionally, this is a plain build with no special characteristics, which makes it both easy to debug and predictable. It varies a little by platform, but there may be some extra head-room on, e.g. allocations and buffer sizes in order to ensure reliability. Usually a compiler symbol like DEBUG or Conditional("Debug") will be present so debug-specific code is pulled in. This is often the build that is shipped, with function-level symbols intact, especially if reliability and/or repeatability are a concern.

2) Release/Optimised build

Enabling compiler optimisations enables some low-level code generation facilities in the compiler to make faster or smaller code based on assumptions about your code. The speed increases possible are irrelevant if your algorithm choice is poor, but for intensive computations this can make enough of a difference through Common Subexpression Elimination and Loop Unrolling, etc. Sometimes the assumptions made by the optimiser are incompatible with your code and so the optimiser has to be cranked down a notch. Compiler bugs in optimised code have also been a problem in the past.

3) Instrumented/Profiled build

Your code is built with specific instrumentation code to measure the number of times a function is called and how long is spent in that function. This compiler-generated code is written out at the end of the process for analysis. Sometimes it's easier to use a specialised software tool for this - see below. This type of build is never shipped.

4) Safe/Checked build

All the 'safety valves' are enabled via preprocessor symbols or compiler settings. For example, ASSERT macros check function parameters, iterators check for non-modified collections, canaries are put into the stack to detect corruption, heap allocations are filled with sentinel values (0xdeadbeef being a memorable one) to detect heap corruption. For customers who have persistent problems that can only be reproduced on their site, this is a handy thing to have.

5) Feature build

If you have different customers that have differing requirements of your software product, it's common to make a build for each customer that exercises the different parts when testing. For example, one customer wants Offline functionality and another wants Online-only. It's important to test both ways if the code is built differently.

Logging and Tracing

So there's writing some helpful statements to printf() and then there's writing comprehensive, structured trace information to data files as you go. This information can then be mined to understand the runtime behaviour/characteristics of your software. If your code doesn't crash, or it takes some time to repro, it is helpful to have a picture of e.g. list of threads, their state transitions, memory allocations, pool sizes, free memory, number of file handles, etc. this really depends on the size, complexity and performance requirements of your application, but as an example, game developers want to ensure that there are no 'spikes' in CPU or memory use while a game is in progress as that will likely affect frame rate. Some of this info is maintained by the system, some by libraries and the rest by the code.

Other Tools

It's not always the case that you have to make a different build to cover these scenarios: some aspects can be chosen at runtime through process configuration (Windows Registry tricks), making alternate libraries available with higher priority to the standard libraries, e.g. efence on your loader path or using a Software ICE or specialised debugger to probe your software for runtime characteristics (e.g. Intel v-Tune). Some of these cost big money, some are free - dtrace, Xcode tools.

share|improve this answer
add comment

There is one extremely powerful debugging technique, not mentioned yet in the other answers. It is available for free for some development environments, or can be added with a relatively little effor to nearly anything else.

This thing is an embedded REPL, preferably allowing to connect any time via a socket, running in a dedicated thread and able to use all the possible forms of reflection for the running code, modifying or completely replacing the running code, adding new things, running functions, etc.

You'll have it out of the box if you code in, say, Common Lisp, Smalltalk, Python, Ruby, etc. It is possible to embed a lightweight interpreter (say, Lua, Guile, JavaScript or Python) into a native application. For JVM or .NET-based environments there are many embeddable compilers and interpreters available, and quite a powerful reflection is there for free.

This approach is much more efficient than the interactive/iterative debuggers (such as gdb, visual studio debugger, etc.), and for the best results should be used in conjunction with a proper logging facility and properly placed assertions.

share|improve this answer
    
but not used in conjunction with a hacker accessing your network. Make sure you disable this in Release builds :) –  gbjbaanb Feb 14 '12 at 22:48
    
@gbjbaanb, that's what SSL is used for. Read the Viaweb story - they've benefited a great deal from such an approach, having an ability to debug the running production system. –  SK-logic Feb 15 '12 at 8:11
add comment

The most important debug tool I've ever used is the post-mortem crash dump (core dump in Linux, user dmp in Windows).

Its a really complex topic, so here's a link:

basically (on Windows, the platform I have most experience post-mortem debugging with), you build your apps with symbols that are saved in a separate file (a .pdb file). You keep these safe (so no-one can easily reverse-engineer your code) and wait for a crash. When it does (and you have DrWatson or similar running to capture the crash and generate the dump file), you load the .dmp file into WinDbg (windows debugger) along with the symbols (and optionally, a path to the source code) and it will show you loads of information like call stack, registers, variables, memory values etc. Its beautiful when it works.

For a debug build, all this is set up to happen automatically. You have to enable symbols when building release. Debug builds also add other stuff like memory guards (that trigger exceptions is you try to write into them, this shows buffer overflows or memory corruption real easily; or asserts for things that are not-quite-right). Generally though Debug builds are for developers to run and test their code before passing it on.

Now this is for native code debugging, .NET code is a PiTA for post-mortem stuff like this, but you can sometimes get .NET exception stuff out by loading sos.

All complex stuff, but it's not so bad. Don't expect nice pointy-clicky GUI though, this is command line power loveliness.

share|improve this answer
add comment

Use printf (or equivalent in other languages) to check the value of a variable after a particular statement or to check whether the program enters a conditional statement or a loop.

You should also use any kind of assert statement that's offered.

You should also write unit tests.

I am able to solve the problems using the above methods.

Perfect. You know everything you need to know.

Are there any other debugging techniques which I should learn?

No. Not that you should learn. You may learn more if you think it will help. But you don't need anything more than what you have.

For the last 30+ years, I've used a debugger only a few times (perhaps three or four). And then, I've only used it to read post-mortem crash dumps to find the function call which failed.

Use of the debugger isn't an essential skill. The print statement is sufficient.

share|improve this answer
add comment

Here's a quick list of techniques:

  • Static call graphs
  • Dynamic call graphs
  • Hotspot profiling
  • Thread messaging watching
  • Replay debugging/reverse execution
  • Watchpoints
  • Tracepoints

You can also implement custom behaviors with AOP-style tools, as well as going a long way with a good static analysis tool.

share|improve this answer
add comment

Learn everything about your debugging tool.

They often hide really powerful features that might help you understand better what's happening. (in particular the C++ debugger of Visual Studio)

share|improve this answer
add comment

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.