The Visual Debugger (dbv)
Written in 1996
(Needs more images :-)
[ Intro | Needs | Views | SGI's cvd | ups ]
Introduction
Througout my programming career I've used about 10 debuggers. None of them was leading me quickly towards finding my bugs.
This is frustrating. Weren't debuggers invented to help people find bugs?
Better debuggers than those that currently exist are possible. Had they existed, people would have much shorter debugging cycles.
Debugging is a mental process leading from observation of a problem (symptom or bug) to an insight on where the cause of the problem lies. Essentially debugging is a search process. The problem is that debuggers are designed much more like slow-motion movie players than like search engines.
Current debuggers fail miserably in a few areas:
- Program state is shown in small details (e.g. single variable values)
rather than in a more general or hierarchical way that can be zoomed in for details in an efficient (one mouse click) manner. We need to easily tell the forest from the trees.- They place a too big burden on the user:
- To formulate a search strategy based on low-level details
- To give a command for every small step during this search
- They often require a long learning process, have too many options buttons and menus, rather than supporting an intuitive direct-manipulation of the objects in question (see below).
This makes the debugging process more tedious than it should be.
My ideal, visually oriented, (and yet non-existent) debugger, which I call
dbv
, is one that tries to address the above problems. dbv is designed top-down, starting from the problem of quickly getting from symptom to cause, rather than from the feature-list up. dbv focuses on the search paradigm, with usability and visualization as its top focus points. dbv enables high-level, coarse-grained operations which save significant typing It allows you to drill-down into details only when necessary. Lastly, it requires no learning since it presents the user with familiar, rather than new concepts.This document briefly describes the UI of the visual debugger
dbv
. I deliberately don't get into implementation issues or specifics like multithreading or kernel debugging because the principles outlined below are orthogonal to these issues. I hope and expect debugger designers to apply these universal principles to specific debugger implementations.At the time of this writing (1996) some PC world (e.g Borland or Microsoft) debuggers are much more friendly than the Unix traditional ones (like dbx), but they still fall short of my ideal debuuger because of their fundamental design flaw. One of the notable exceptions is Mark Russell's ups which is the closest thing to an ideal debugger I've seen from the GUI/interaction and detail hiding aspects.
This is a personal view. Your mileage may vary.
What do we need?
Step backwards / Undo
One of the worst, and universally accepted misfeatures of any debugger is the fact that you are forced to perform several runs to debug a problem because there's no way back. i.e. you do a "next" and oops, you've stepped too far... While Undo has been an essential part of every editor I know, no debugger I know has a "step backwards" (with full undo) feature. Imagine how many hours you could have saved if debuggers would have saved some state and allowed you to go back as opposed to rerun the program from scratch.
Now before you tell me that this is ``impossible!'' consider that saving intermediate states of processes is something operating systems do all the time, multiple times per second.
Space considerations: saving a full state dump (registers + variables) at the last N breakpoints or so, or at every active function entry/exit should be practical and possible today and assuming Moore's law, definitely in a few more years.
Even saving I/O state should be partially possible if we keep all the file read/write pointers in the state. Regarding non-file I/O (devices, FIFOs, sockets, pipes etc.), I'll gladly accept a compromise and give up on these, if you can give me the rest.
Animated views of the program
While your program is running, it changes in space and time: the heap and the stack grow and shrink, the PC is traversing the code space back and forth. Your data structures change values.
Since a picture is worth a thousand words, there should be a way to present all these objects graphically. Since the views change in time, these views should be animated. The debugger should project the program state into the human mind in a way that is easy to grasp and understand.
Imagine if we could watch our program running as we watch a video: be able to fast forward, go back, turn to slow-motion, stop, and then more: expand a certain view for more details!
If we look at the things that can go wrong (bugs): A variable gets thrashed, a memory leak occurs, a wrong condition takes control to the wrong if-then-else basic block. etc. These can all be easily mapped into visual space. As an example: if an array holds the values of peoples' ages, then any negative or greater than 100 value should be very conspicuously displayed (e.g. a red point in a green line) This way, thrashing a variable would be immediately evident.
Auto-pilot animated views should be supported, rather than having the programmer click on
next
hundreds of timesdbv
should support a "play in slow motion" mode in which the program runs by itself slowly enough (say, a 1 second pause on each function call or source line) thereby enabling the programmer to "click to stop" at the right place. The pause time can be adjustable by a visual acceleration gauge and its resolution (func calls vs. source lines etc.) should be conform to the current view resolutions (see hierarchical views below).Context sensitivity + direct manipulation of objects
Is your debugger window full of cascading menus? does it have more than three buttons per screen? Do you need to read a manual in order to use it? If you answered Yes to any of the above, read on.On top of the animated views, the UI design should be, object oriented. Rather than having a long menu of operations into which you provide the object, You should ask yourself: what are the most logical operations in any screen area of any animated view (source lines, variable names, etc.)
The ideal debugger has very few buttons, since all objects are directly manipulated. Let's look at a few objects that are relevant to the debugging process:
The main idea is that each of these has very few operations that are relevant to it in a debugging process. For example: source code is composed of source lines. What can you do with a source line ? toggle the breakpoint on it (set break point), execute it (next), execute up to hitting it, disassemble it, and that's about it.
- Source code
- Stack trace / call graph
- Variable name
- Variable value
Rather than having a global menu on all screens with these opeartions dbv simply displays source lines with small visual cues: On the left there's a toggle-breakpoint narrow area, when you click on it the breakpoint toggles and you immediately get a visual feedback: a small red stop sign appearing or disappearing. Since all views are hierarchical, they all have the small
expand/collapse
arrow. Successive expansion would go through code level of detail:
- Source file list
- In a file: function list
- Source lines
- Disassembled lines
The main idea is that clicking on the object itself (or the small visual cues near it) does what makes sense for that object alone. In other words, all object displays are context sensitive. In the simple cases one click does what's needed, in the complex case, a pop-up menu should appear in place with only the permitted operations on that object.
Moving the mouse over a variable name in the source window should display a tiny popup with its value, clicking on its value in an assignment statement, should allow an in-place edit dialog where you can change the value. To sum up: every user action should be interpreted as an intention to get information or to change the state of the underlying object. The most intuitive response based on the context should ensue.
To sum up: minimize the use of "global" buttons. Make the views themselves clickable and context sensitive.
Simplicity and consistency
Many menu items are not a goal. Intuitive use is. Basic operations should have cosistent ways of invoking them for example: viewing/inspecting would result from mouse movement and placement, while changing state or values should be a result of mouse clicking. More on that later.Hierarchical views
There should be a quick way to zoom in to the details or to zoom out for a high-level view. A few examples:See the example of the source view below.
- Single-stepping support at the function call/return level (note that this is not the same as
next
orstep over
) as opposed to source-line based stepping.- Ability to expand/collapse a full call frame as opposed to having to type multiple variable names to see their values.
Minimization of manual labor
Dbv should minimize user typing: views should be displayed by merely placing the mouse over the correct object. i.e. a variable value would be displayed in a small popup when you place the mouse (and stop motion) over a variable in the source.A common trap in traditional debuggers is the "stuck inside a loop" syndrome: as you step through your program you get into a loop that makes stepping a tedious process. What is needed are two new operations: "execute to here" in the code view (see below) by clicking on the right mouse button and/or "finish loop" which is similar to the "return from subroutine" traditional operation but refers to the current loop.
Quickly breaking out of multiple levels of nested loops should also be supported. A good way to do this is: if there are several levels of nesting provide a popup menu with a nesting level selection (click mouse, select, release mouse, should do it).
Event driven debugging
Since single stepping is tedious every debugger should support event driven actions such as "stop when some condition is true (stop if)" or "display every change to a certain variable or address (trace)" This is a huge time saver and not all debuggers support it. Modern CPUs with fence and debugging registers and VM systems with memory protection support these features without enough debuggers taking advantage of them.Integration with run-time checkers
There's no real reason why tools like Purify, or debug_malloc would not be integrated into a compiler or debugger.As an example: out-of-bound array references, references to freed heap areas and null-pointer dereferences could be checked during runtime (compiled by default) when using
cc -g
(or maybe better: a special-run-time-checks
compiler option). The implementation is something along the following lines:*pwould be converted to:#include <assert.h> assert(p != NULL) *pAnd:a[i]would be converted to:assert(i >= 0 && i < sizeof(a) - 1) /* or some such */ a[i]And the program would be linked with some form of-ldebug_malloc
Of course the above is a simplification, but you get the idea.
Such feature alone, would eliminate a significant amount of bugs very quickly and without even having to look for them. Think of this as an automatic assertion injection into the code.
While the code size would grow significatly when using such option, countless programmer hours would be saved, and our software quality would improve tremendously.
One reason "4th generation languages" like perl are becoming popular is that they make programmers much more productive by eliminating the burden of memory management, out-of-bound, and null-pointer errors. As you see above, some of these features can be transparently integrated into the tool-set of conventional so called "high-level" languages - like C.
Debugger Views
Linear Stack view
A dynamic (expanding and shrinking) view of the stack.This is similar to what you get when you type
where
in debugger likedbx
except:
- it is displayed continuously (if its window is active) so you don't need to type much.
- As in all views, it is updated and kept synchronized with others with every step (or continuously if running in "slow motion" mode).
- It supports the direct manipulation of its variables
- It provides hierarchical views: clicking on one frame expands/collapses it to see more/less details (in this case, all local variables, and parameters and their values)
- All clicking is context sensitive: i.e if you click on a frame it expands. If you click on a filename, the source is shown, If you click on a variable name (varname=value) that happens to be a pointer it gets dereferenced, clicking on a value allows you to edit it in place (assign to the variable), clicking on pointers to structs cause to print the struct values etc. To put it in another way: a debugger should not have a special "traverse linked list" (or any other big menu) option, clicking on a list name should do this by default, use right and left mouse buttons to go back and forth. direct object manipulation is the principle here. Every object has very few "logical operations" supported, and these should be implemented when clicking on that object.
Example of a linear stack view.Call graph view
The static caller/callee graph with the current active stack view higlighted. This is an extension of the linear stack view.
Example of a call graph view"single stepping" default is at the routine call level. The number of time routine X was called from routine Y is placed as a label on every edge in the graph. Placing the cursor over the argument list of a certain call, should bring a detailed popup of the relevant frame. Clicking on a function name should bring the focus to the source window at the point of call.
Every click on "step call" button will do one of the two:
- Call the next function
- Return from the current function
whichever comes first. A return will restore the default color of the node of the function from which we just returned and reverse the direction of its arrow. A call will highlight the function and the arrow leading to it using the "active" color, and will increment the call count label next to the arrow. This is my favorite stepping view.
Recursive calls can be handled as well: arrows will point back up in the graph and if a function is active on the stack but not at the top of the stack it can have a special color and a broken line arrow.
Breaking out of function-calls within loops should be supported via the right mouse button (see minimizing manual labor above). i.e. it should be possible to quickly execute multiple function calls within a loop with one mouse click.
Collapsible/expandable source view
Current location (PC) arrow pointing at the code and moving along it as you step through the code (or during "slow motion" mode). Currently active file, routine, line are highlighted with inverse video (I use red below).main.c search.c kruskal.c find.c line 5: setup(int elem, int *dad) line 15: find(int elem=5, int dad=2) int i, temp; /* Find root of class (class-representative) */ for (i = elem; dad[i] > 0; i = dad[i]) ; while (dad[elem] > 0) { /* point path members */ temp = elem; /* directly to root */ elem = dad[elem]; dad[temp] = i; } return (i); line 95: cleanup() union.cExample of a collapsible/expandable hierarchical source view Note the real estate saving by omitting routine headers and external opening/closing braces.
Unlike the traditional source view, 4 levels of resolution are supported:
- Source file
- Routine
- Source line
- Assembly level (hopefully you seldom need to go there...)
Zooming in/out the resolution levels should affect the single-step resolution (i.e. in the assembly level: a single step is in machine instructions: possibly several on a multi-scalar machine)
The resolutions are not mutually exclusive but combined into one view using collapsible/expandable regions. i.e. every element (file name, source line) can be zoom-ed into or out of (toggle) by clicking on it. e.g. clicking on a filename with expand to an indented list of function names in that file. Clicking on a function name will give its traditional source-line view. Clicking again on the file name, will collapse the function list back and make the current source line and function invisible.
Breakpoints and the current PC are marked clearly with colors A breakpoint is a small red
traffic sign to the left of the line. A quick "execute till this line" operation should be supported as opposed to the traditional labor-intensive, set breakpoint, continue, delete breakpoint (e.g. could be mapped to a right mouse button click). Heap view
The heap arena colored based on used vs. freed areas.Image is missing here ... SGI's
cvd
supports this. Clicking on an allocated area in the heap should jump to the respectivemalloc
location in the code view. This is the view used to detect memory leaks or main heap hogs. This is assuming we are still dealing with a language that doesn't take care of these issues for us.Static data view
Global + static variables of the program.Image is missing here ... Collapsible/expandable like the code view. i.e. structures/arrays are expanded to show their members (if too big, allow scrolling). Clicking on pointer types will both show their value (address) and the element they point to. Walking along linked lists is trvial and intuitive: just click on the "next" member in the structure.
Appendix A: problems with cvd (SGI's debugger)
SGI's cvd has very nice visualization features, however, it is one of the most unfriendly debuggers I've used. This is not due to lack of functionality but mainly due to UI design. It is possible that a partial redesign of the UI would makecvd
far more usable and friendly.When I first used cvd, I found no obvious direct manipulation of what you see on the screen. e.g. clicking on a variable didn't show its value. Doug Young told me that this has been supported for some time, although for some odd reason the feature is off by default. Right click in the text area, and select "auto evaluation" or "click to evaluate". Then just pointing at a variable shows it's value. I guess this is exactly what I mean by ``non-intuitive''.
It uses a window per variable type rather than having just having one variable viewer (and deducing the type automatically). As a result, variable inspection not only is labor intensive but also takes too much screen space (multiple windows). Other operations are not intuitive: e.g. the "trap" operation gives a free text dialog box and it is not clear what to put in it. As a reference point: while I was able to use the Borland C debugger very effectively the first time I tried it without reading any documentation, I found
cvd
with its tons of functionality, zillions of powerful options, very non-intuitive to use.Appendix B: UPS
ups
is a freely availabe, open source debugger originally written by Mark Russell at the University of Kent. It already supports at least three architectures and has the following features:
- Very intuitive UI: object oriented directly manipulated
- Clicking on a variable in the source window shows its value
- Clicking on a line not over a variable toggles breakpoint
- etc.
- Views are hierachical: collapsible/expandable views is used throughout
- Includes a built-in C interpreter so you can inject C code into your breakpoints: e.g. stop (here) if (condition)... added directly to into source window.
ups
is available (among other places) from: http://sunsite.unc.edu/pub/X11/contrib/devel_tools/
Comments, suggestions, better debuggers are welcome at