Introduction to debugging

Author: Richard Darst
Date: 2014-06-24

What is debugging?

Outline

Interactive development

This is a good system, but the things you type are temporary. They can't be used as part of other functions or anything. This makes it harder to use for big programs.

Debuggers

Debuggers for different languages

Debugging is a concept that exists across programming languages. Creating a debugger is a necessary step of creating any programming language, toolchain, or operating system.

  • Debugging is actually an interface, so there can be more friendly front-ends available. For example,
    • The "Data Display Debugger" (ddd) is a more graphical debugger for gcc.
    • pudb is a console (ncurses) based Python debugger.
    • Most IDEs (e.g. emacs, spyder, ...) will integrate debuggers somehow.

Some terminology

More formal definitions:

call stack:
A data structure that stores active subroutines in a computer program. On the stack is the main function, then the first function called, then the second function called, and so on. The python exception tracebacks are a listing of the stack.
execution frame:
All the context within a function. In Python, this is basically the local variables (locals()), global variables of the module (globals()).
scope:
Each variable is defined in a certain scope. For example, a local variable is accessible within that function, but global variables are accessible anywhere in the file.

Basically, whatever you do, you should be able to find a debugger for it. Most of the operations I describe below should work with your environment. The commands within the debuggers seem to be fairly standard.

Debuggers exist not just for "normal" programs like we use here, but for operating system kernels (which have to operate at a very low level, maybe by external network connections since a kernel can't pause to debug itself), embedded devices (which may have to run over dedicated cables attached to the circuit board), as servers to run over network links, and so on.

Prerequisites

Debuggers

Problems with the interactive examples above:

The debugger:

Post-mortem debugging on a program

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
$ pdb ex-raises-exception.py
> /home/richard/scicomp/tut/debugging/ex-raises-exception.py(1)<module>()
-> import numpy
(Pdb) cont
Traceback (most recent call last):
  ...
  File "ex-raises-exception.py", line 1, in <module>
    import numpy
  File "ex-raises-exception.py", line 7, in main
    func(arr)
  File "ex-raises-exception.py", line 3, in func
    x + numpy.array([1, 2])
ValueError: operands could not be broadcast together with shapes (3) (2)
Uncaught exception. Entering post mortem debugging
Running 'cont' or 'step' will restart the program
> /home/richard/scicomp/tut/debugging/ex-raises-exception.py(3)func()
-> x + numpy.array([1, 2])
(Pdb)

Debugger commands

The debugger has many commands:

cont, continue
Run code until there is an exception.
l, list
List lines of code around the exception, or at any other point.
bt, backtrace

Print a bactrace of all stack frames, for example:

1
2
3
4
5
6
/home/richard/scicomp/tut/debugging/ex-raises-exception.py(1)<module>()
-> import numpy
  /home/richard/scicomp/tut/debugging/ex-raises-exception.py(7)main()
-> func(arr)
> /home/richard/scicomp/tut/debugging/ex-raises-exception.py(3)func()
-> x + numpy.array([1, 2])
u, d, up, down
Go up/down one stack frame. This lets you see the variables/code in the calling functions using p and l.
p, print <expression>
Print a variable or an expression evaluation.
h, help
Get help, list of commands or help on command
1
2
3
4
(Pdb) print x
[ 0  1 10]
(Pdb) print x + numpy.array([1, 2])
*** ValueError: operands could not be broadcast together with shapes (3) (2)
These commands are somewhat standard across debuggers

Breakpoints

Procedure:

  1. Start the debugger
  2. Set breakpoints using break
  3. Type cont, program stops at breakpoint.
There are other things you can do, like make conditional breakpoints (only break if a certain condition is true), or breakpoints that just print something but don't stop. A debugger can be an extremely powerful environment, but I generally don't use it that way.

Breakpoints example

Invoke pdb on the file:

pdb filename.py

Add a breakpoint like this:

1
2
3
(pdb) break file:lineno
(pdb) break functionName
(pdb) cont
For gdb: if your program has command line arguments, use gdb --args arg1 arg2 ...)

Debugger commands 2

If you don't type cont, you can step through the program manually.

s, step:
Run the current line and then stop again. Step into any functions called on the next line.
n, next:
Run the next line(s). If there are functions called in the next line, do not debug inside of them.
r, return:
Run until the function returns, then return to the debugger.

Example:

ex-breakpoints.py:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
def A(x):
    print 'begin A'
    a = x + 1
    B(x)
    print 'end A'

def B(y):
    print 'begin B'
    c = y * 2
    print c
    print 'end B'

def main():
    A(5)

if __name__ == '__main__':
    main()

Output:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
$ pdb ex-breakpoints.py
> /home/richard/scicomp/tut/debugging/ex-breakpoints.py(3)<module>()
-> def A(x):
(Pdb) break B
Breakpoint 1 at
/home/richard/scicomp/tut/debugging/ex-breakpoints.py:9
(Pdb) cont
begin A
> /home/richard/scicomp/tut/debugging/ex-breakpoints.py(10)B()
-> print 'begin B'
(Pdb) l
  8
  9 B   def B(y):
 10  ->     print 'begin B'
 11         c = y * 2
 12         print c
 13         print 'end B'

A "normal" way of using this on a program would be to start the debugger, set a breakpoint before the problem, and step through the file, checking each line manually to see what the error is.

With interactive languages like Python that have better error handling facilities, this is not as critical a development strategy, but is useful nonetheless.

Attaching to a running process

Example:

gdb-attaching.c:
1
2
3
4
5
6
7
8
// Compile with `gcc -g` to include debugging symbols!

int main() {
  int a = 0;
  while (1) {
    a += 1;
  }
}

Output:

1
2
3
4
5
6
$ gcc -p PID
...
main () at gdb-attaching.c:7
7         }
(gdb) print a
$1 = 1503027589

Using gdb on a running python process

Example:

gdb-attaching-python.py:
1
2
3
a = 0
while True:
    a += 1

Output:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
 $ gdb -p 17456

 <endless ugly stuff>

 (gdb) py-bt
 #0 Frame 0x12a7870, for file gdb-attaching-python.py, line 6, in
 <module> ()
 (gdb) py-list
 ...
 1
 2    a = 0
 3    while True:
>4        a += 1
 ...
 (gdb) py-print a
 global 'a' = 52638676

Another way to start debugging

Python: code.interact

Python: Begin debugger at a certain place

Actual programs vs running functions

I have observed two main ways people write their code:

IPython debugger - from command line

IPython includes its own debugger (in a separate package, python-ipdb). It is equivalent to the regular debugger in most respects.

IPython shell - post-mortem debugger

IPython debugger - interactive

To invoke ipython debugger at a certain place, do

1
import ipdb ; ipdb.set_trace()

I have noticed that this sometimes, ipdb can't do things that pdb can. If one method does't work, try the other.

This probably relates to subtle implementation differences and the use of enclosing scopes. I do not fully understand it, I figure out problems as I go.

Conclusions

References

Advanced topics

Python: Things to watch out for: lines not in functions

1
2
$ ipython --pdb <filename>.py
$ python -m verkko.misc.pdbtb <filename>.py

Example:

exception-not-in-function.py:
1
2
print 1
raise Exception("The bug is on this line.")
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$ pdb exception-not-in-function.py
(Pdb) cont
1
Traceback (most recent call last):
  ...
  File "exception-not-in-function.py", line 1, in <module>
    print 1
Exception: The bug is on this line.
Uncaught exception. Entering post mortem debugging
Running 'cont' or 'step' will restart the program
> .../exception-not-in-function.py(1)<module>()
-> print 1

Notice that lines 6-7 and 11-12 in the output say that the exception is on line 1 in the code, print 1, not line 2.

Python: Things to watch out for: nested contexts

Example:

1
2
a = [1, 2, 3]
print (x+b for x in a)``

Inside this generator (where the NameError is raised), you can't print a. The scope gets messed up inside the generator and it doesn't know how to find the a variable. If you type up in the debugger one or two times, it will work.

The technical explanation is that when python does the exec of your input in the debugger, it doesn't properly use the enclosing scope.

Easy use of PDB from command line

I wrote a module to invoke pdb automatically:

Core dump