7

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?

Cracker
  • 3,183
  • The difference depends on the platform. For Java the difference is small. For C it can be vastly differen.t –  Feb 14 '12 at 01:04
  • 2
    what languages/platforms are you coding on? – DXM Feb 14 '12 at 05:16

7 Answers7

6

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.

JBRWilkinson
  • 6,759
5

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.

Jalayn
  • 9,807
Jarryd
  • 151
  • 4
  • 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 00: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 02: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 09:30
  • @SK-logic: ...what? Why in the world would you want to "script" your debugger? Scripts are for performing repetitive tasks. Debugging should not be a repetitive task; you track each bug down once to its unique source and fix it. If you need to script your debugger, you're doing something wrong. – Mason Wheeler Feb 14 '12 at 13:20
  • 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
  • @SK-logic: You're making several false assumptions here. First, that interactive debugging involves "hitting stop-inspect-stop-inspect like crazy." (seriously, have you ever actually done it? That description is so bad it falls under the "not even wrong" category.) And second, that I can be reasonably expected to have enough data about the state of the program to formulate a decent hypothesis without having looked at it first. Your script will only ever be as good as the assumptions you have while writing it. – Mason Wheeler Feb 14 '12 at 17:24
  • But if I have a debugger that will show me all local variables automatically, with proper visualization support and an understanding of my language's object model, and an expression evaluator/immediate mode invoker (which requires compiler integration) to test my hypothesis once I've got enough data to formulate one, I don't need a possibly-flawed hypothesis up front and I don't waste time chasing wild gees when the hypothesis is wrong. I get to see what's actually going on. Your description of the details of how debugger scripting actually works just makes it sound even more ridiculous. – Mason Wheeler Feb 14 '12 at 17: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
  • @Sk-logic: You're starting the game in the 8th inning; by the time you get to the point where a simple stack trace can help you identify the problem, you're usually almost done anyway! Now at that point, you can write a script and run everything again to test it, or you can use the stack viewer to walk up and inspect the execution point and local state for each stack frame until you see what's causing the problem. You're trying to argue, in essence, that indirect observation is the better tool when direct observation is both possible and easy, and that's a logical absurdity. Not buying it. – Mason Wheeler Feb 14 '12 at 18:44
  • @Mason Wheeler, I suspect your view on debugging is too strongly affected by your Delphi incline. I ran out of arguments - you really have to try the more advanced debugging techniques yourself before you can understand the benefits. – SK-logic Feb 14 '12 at 22:15
  • @SK-logic: That's the same thing you said about closures. "Oh, this is more advanced, and you wouldn't understand unless you actually drink the kool-aid." And I didn't buy it there either. An answer like that can be applied to any objection, valid or not, which makes it more useful to deflect criticism than to answer it. Scripts are for automating something you already understand, and debugging, by its very nature, is an investigative process. If you know enough to automate it, you already know enough to fix the bug. That's not a "Delphi-incline" thing, that's a programming thing in general. – Mason Wheeler Feb 14 '12 at 22:46
  • @Mason Wheeler, it was your own argument that you can't see the value in scripting, and that your primitive interactive debugging approach is "enough" for you. Of course it is a full stop for a discussion - I can't give you any more insight since you're not willing to learn. To be honest, I find it very typical for the Delphi programmers, they're a stubborn lot. Scripts are not for automating repetitive tasks (I now, Delphi sucks in scripting, that's why such a misunderstanding), scripts are, basically, for making one's life easier. I doubt you know much about "programming in general", sorry. – SK-logic Feb 15 '12 at 08:08
2

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.

gbjbaanb
  • 48,585
  • 6
  • 103
  • 173
2

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.

SK-logic
  • 8,507
  • 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 08:11
2

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.

S.Lott
  • 45,390
0

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.

Paul Nathan
  • 8,570
0

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)

Klaim
  • 14,862
  • 4
  • 50
  • 62