Debugging C like it's Python

I use the Python interpreter interactively and pdb (as well as ipdb) a lot and they let me understand my programs' state and test new things out quickly.

When writing Flpc in C, I found it a difficult to transition. In this post, I describe how to use gdb to get a similar workflow.

In the course of that project, I also gained a much greater appreciation for C (compared to writing everything in assembly). It is really convenient and macros when used in moderation lets me customize the language a bit to get around some quirks.

What I use in Python

pdb.stack_trace() sets breakpoints in code. I know its possible to set breakpoint from the debugger but I find the uniform syntax (for my code and the debugging commands) is easier to remember and use. [1]

from pdb import set_trace as bp

[...]
def some_problematic_function():
    if some_condition:
        bp()

Once bp() is called, I get a REPL in which I can examine values

(pdb) p some_var
3
(pdb) p f(some_var) + 4
9

I can move forward by single steps (examining values in between) and eventually continue if all is well.

(pdb) s
(pdb) s
(pdb) n
(pdb) c

If bp is called rarely enough, this lets me get to the crux of the problem quickly without setting up explicit conditionals.

I also tend to throw bp around when I add new functions (by breaking at the beginning of the function) or new functionality to an already defined one. This lets me test with real data instead of artificial data.

Using the ! prefix, I can run commands with the function's input values and then paste commands that worked back into my text editor. I've even started adding blank functions with just bp() in the body! Again, this lets me write the function with a sample input in hand.

pdb.pm() (and pdb.post_mortem) starts post-mortem debugging. I tend to run Python with -i for interactive mode. And when the program crashes, I'll start the debugger.

>>> import pdb; pdb.pm()
(pdb)

I'll move around the stack and examine values. This removed almost all the print statement (and logging) debugging I used to do. It lets me examine value I wouldn't have thought of printing beforehand when I started the program.

Its unfortunately not possible to continue (with c) in this case.

Using gdb

Compile the binary to include more debugging information.

$ gcc -gdwarf-2 -g3 flpc_all.c -o flpc
$ gdb ./flpc
[...]
(gdb) r <command-line-parameters>

With that, gdb can expand macros

(gdb) p some_var
3
(gdb) p some_macro(some_var) + 4
9

and make calls

(gdb) call ps()

To get the same effect as pdb.set_trace, I create an empty function bp

void bp() {}

and set a breakpoint there before running the program.

(gdb) b bp

Similarly, when an error occurs, I route all of them to a function _error defined as

void _error(char *s){
  printf("%s\n", s);
  exit(1);}

and use it like so.

if (pop(local_stack) == Sep){
  _error("Popping empty data stack!\n");}

Like bp, I set a breakpoint on _error before running the program.

(gdb) b _error

If only the input to my program changes but the source doesn't, I don't need to exit gdb and can just rerun

(gdb) r <command-line-parameters>

each time.

Footnotes

[1] Version control help me find and remove these before commiting.

Posted on Feb 17, 2017

Blog index