Author: | Richard Darst |
---|---|
Date: | 2014-06-24 |
This tends to be the first way of debugging. It works, but is usually slow since you have to re-run the program each time it goes.
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.
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.
- Different C compilers may have different debuggers. You may have to search some to find the right debugger for your language, compiler, and architecture.
- Matlab: - http://se.mathworks.com/help/matlab/debugging-code.html - Tutorial: http://se.mathworks.com/help/matlab/matlab_prog/debugging-process-and-features.html#brqxeeu-177
- Bash: http://sourceforge.net/projects/bashdb/
- R: http://www.stats.uwo.ca/faculty/murdoch/software/debuggingR/
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.
Python, being interpreted, always has the source code available, no nothing special is needed.
In C, you must compile with debugging symbols.
Since C programs are basically raw machine code, the program doesn't include the source code for each machine instruction, variable names, or anything human-understandable.
Compile using the -g option:
1 | $ gcc -g filename.c
|
Other languages or compiler options may vary.
Problems with the interactive examples above:
The debugger:
Post-mortem debugging is starting the debugger after some fatal exception or error.
Example:
ex-raises-exception.py:1 2 3 4 5 6 7 8 9 10 | import numpy
def func(x):
x + numpy.array([1, 2])
def main():
arr = numpy.array([0, 1, 10])
func(arr)
if __name__ == '__main__':
main()
|
We run pdb filename.py on our file
We type cont to begin execution.
When an exception happens, you can inspect the problem.
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)
|
The debugger has many commands:
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])
|
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
Procedure:
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.
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 ...)
If you don't type cont, you can step through the program manually.
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.
In everything we have done so far, we have to decide we want to debug before we start the program. What happens if it's already running?
gdb (the GNU debugger) can attach to already running processes:
gdb -p PID
Then, you use bt to figure out where you are in the call stack, list to list the code, and print to show contents of variables, etc.
You could even set future breakpoints and then cont, and it will run until you get there. Or just use step and next to continue through the program.
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
|
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
|
You can use code.interact on a single line to examine an execution frame, but this doesn't give you the debugger up or down commands.
Add this to a line
1 | from code import interact ; interact.interact(local=locals())
|
See ex1.py
You can start a full debugger instead by using:
1 | import pdb ; pdb.set_trace()
|
pdb will start exactly from that point.
Type cont to quit debugger and resume execution.
This allows you to go up and down the call stack, unlike code.interact.
I have observed two main ways people write their code:
IPython includes its own debugger (in a separate package, python-ipdb). It is equivalent to the regular debugger in most respects.
Can be automatically invoked with
1 | ipython --pdb filename.py
|
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.
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.
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.
I wrote a module to invoke pdb automatically:
You normally run your program with
1 | $ python filename.py
|
Change to run your program with
1 | $ python -m verkko.misc.pdbtb filename.py
|
This uses the standard python -m MODNAME ... mechanism. It is the same as running python /path/to/MODNAME.py ... .
Python will run normally and with no overhead. You don't have to type cont to make it start or quit/restart the debugger.
If (and only if) there is an exception, it will drop to pdb at that point. Otherwise, the program terminates normally.
The term core dump refers to a dump (to disk) of core (memory of a process)
This core can be used to debug the crash, after the program has already terminated.
This could be useful, for example, on jobs submitted to a cluster
Must be enabled using ulimit:
ulimit -c unlimited