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:

set width 0
set height 0
set verbose off

b X.0x7ffff5221b34.OP_CHECKDELIM_RET
commands
  silent
  printf "X.0x7ffff5221b34.OP_CHECKDELIM_RET\n"
  continue
end

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.