Monday, March 03, 2008

Debugging With Lazy Breakpoints

Sometimes you're hunting down a problem that occurs very rarely. When it does, an exception is triggered. If you're debugging, Delphi should pop up (it does) and allow you to look at the stack and inspect local variables (it doesn't - happens a lot to me). What can you do?

You could set a conditional breakpoint just before the problem occurs and test for the problem causing condition here. That would work on a short run, but if you're hunting the problem for many weeks, the breakpoint may move around or even get disabled. Delphi's breakpoint storage mechanism is somewhat unstable if you edit the code around the breakpoint a lot (and even more if you edit the code on multiple computers).

More stable solution is to do everything in code, from testing if debugger is running to pausing the program and breaking into the debugger. Following fragment illustrates the approach:

if DebugHook <> 0 then // test if debugger is running
if problem = true then //test the condition
asm int 3 end; //break into debugger

Not very obvious and full of arcane knowledge, but it works.

18 comments:

  1. Anonymous12:14

    You can also use the WinAPI DebugBreak().

    ReplyDelete
  2. Bah, Win32 API is for sissies.

    Just kidding. Thanks for pointing it out.

    I guess I'm just too caught in old school programming habbits. If I recall correctly, INT 3 is a legacy from DOS times.

    ReplyDelete
  3. how about just improve your logging instead?

    ReplyDelete
  4. In a related question: how can I programmatically disable the exception-handling of the debugger?

    In some situations, an exception is rightly expected (for example in unit-tests) - But I wish I could make de debugger ignore those specific occurrances (I know I can ignore the whole exception-class, but that's a bit too much for my intent).

    Any ideas?

    ReplyDelete
  5. Sorry, no idea. But maybe some other reader knows?

    ReplyDelete
  6. Anonymous01:11

    Why you do not use asserts?

    ReplyDelete
  7. Anonymous01:13

    why not asserts?

    ReplyDelete
  8. We have some tests that throw expected exceptions, but we don't generally have problems with the debugger.

    First of all, we only run under the debugger when we absolutely have to. We usually use "Run without debugging" (Ctrl+Shift+F9). The tests load and run faster if you're not debugging. And besides, we got in the habit of avoiding the debugger back when we were on Delphi 2005 and 2006, when the debugger crashed all the time. (2007's debugger seems pretty stable, though.)

    Second, when we do need to debug, we'll just go into the DUnit GUI and uncheck all the tests except the one we're trying to troubleshoot. As a general thing, tests should be fine-grained enough that any one test won't throw-and-catch more than one exception (each exception should usually be its own test), so again, having the debugger stop on exceptions isn't a big deal.

    ReplyDelete
  9. Why not assert? Because assert raises an exception and something in this project interfers with Delphi debugger and exception handling in such a way that I can't examine variables on the stack when exception occurs.

    ReplyDelete
  10. My current practice in a huge, old and unstable project (tends to break Delphi/Debugger) is:
    Raising a "silent" exception (descending from EAbort), immediately catching it and using JCL's handler to log the call stack (plus some additional info, like tempvars and/or instvars).
    This works really great when a bug occurs only in customers environment and w can't reproduce it.

    ReplyDelete
  11. Anonymous13:43

    you may just write
    asm nop; end;
    instead of
    asm int 3; end;
    and set breakpoint there

    ReplyDelete
  12. Do read what I wrote again.

    Breakpoints will stay in place during the day, may stay in place for days, but edit the unit in question for many days on many machines and you can be sure that they won't be placed at the original position after a week.

    ReplyDelete
  13. Anonymous17:15

    The empty post. Just "asm int 3 end;" and that's all.

    ReplyDelete
  14. sm2, what are you trying to say?

    ReplyDelete
  15. Anonymous11:30

    This is were MadExcept scores. You can get your application to email you when an exception occurs along with the stack and line number of where the exception occurs.

    We use MadExcept to log these errors in an event log in our applications.

    Dave Craggs

    ReplyDelete
  16. We're using our own stack tracer, based on the Hallvard Vassbotn's work, and sometimes madExcept, which is simply great so I'm no stranger to stack traces.

    But in this case stack trace was not enough - the problem depended on a state of various internal structures and you can't see that from a stack trace.

    BTW, the trick I mentioned already helped. While working on another issue I caught the problem in the debugger and solved it in ten minutes. It was simple once I understood the cause.

    ReplyDelete
  17. Guy Gordon15:09

    RE "Delphi should ... allow you to look at the stack and inspect local variables"
    Press Ctrl-Alt-C to bring up the CPU window, F8 to single step, then close the CPU window. Debugger is now at the line that caught the exception. If the offending line is inside a try..except block you can now see the local variables.

    But I like your idea. Gonna try it out.

    ReplyDelete
  18. Anonymous15:15

    RE "Delphi should ... allow you to look at the stack and inspect local variables"
    Press Ctrl-Alt-C to bring up the CPU window, F8 to single step, then close the CPU window. Debugger is now at the line that caught the exception. If the offending line is inside a try..except block you can now see the local variables.

    But I like your idea. Gonna try it out.

    ReplyDelete