printf() debugging in assembly language
I am happy to report that I have figured out a way to do printf()
debugging with assembly language!
I know that many people think of printf()
debugging as a pedestrian
technique, a hack for people who are too lazy or obstinate to use a
real debugger. But I’ve always found it extremely useful, because
of the simple fact that stepping through a program is slow. Debugging
is all about finding the precise moment where the program’s expected
behavior diverged from its actual behavior. Stepping through your
entire program line-by-line and verifying the expected behavior at
every step is slow and thought-intensive, and if you miss the critical
moment you have to start over – debuggers don’t go backwards.
(Ed: actually I take this back; it looks like GDB can go backwards these days! I will most definitely have to try this out. But it’s still the case that the critical moment may be far behind the moment where the debugger is stopped.)
Contrast this with printf()
debugging, which very quickly gives you
a transcript of the program’s entire execution. This lets you see
patterns and hopefully hone in on the crucial moment where things went
awry. If you didn’t get quite the information you need, you can
insert slightly different print statements until the transcript tells
you the story.
In pretty much every language, printf()
debugging is easy, because
you can insert a print statement anywhere and pass it any parameters
you want. But when you’re hand-writing assembly language, it’s a much
bigger pain. Making function calls (like to printf()
) is far more
manual; you have to figure out where to store the string, deal with
varargs calling conventions, and even then making the function call
will clobber half your registers. It’s not very practical.
Looking for a solution to this, I came across the GDB breakpoint
command lists. Basically you can give GDB a list of things to do
when a breakpoint is hit, like print arbitrary registers, variables,
memory, etc! And since the last command can be continue
, the
program can run without any interactive debugger intervention.
I came across some helpful examples on Stack Overflow (like this one) to give me ideas. So far I’ve just been using simple commands like:
This will print the name of this function whenever it is hit. I’m using this to debug my current project. I’ve been working on a bytecode interpreter with a JIT code generator backend. I can already debug the interpreter by making it dump its list of bytecodes as it executes them. With this technique I can debug the JIT in a similar way; I can make GDB trace the execution of my JIT code and dump the bytecode equivalent of what it is doing. That way I can see where my JIT code is diverging from the behavior of my known-good interpreter.