Metal.GDB: Controlling GDB through Python Scripts with the GDB Python API

Today we have a guest post from Klemens Morgenstern, an embedded C++ consultant. You can learn more about Klemens on his website.


While debugging commonly happens through a JTAG or SWD port, many developers still use a serial port as an additional debugging tool, typically for automated communication with an embedded device. Today we’ll take a look at the python API for gdb. This API allows us to automate communication with our embedded target.

Note that the standard arm-none-eabi build has a separate executable called arm-none-eabi-gdb-py that requires python-2.7.

What is the GDB Python API?

The gdb Python API allows gdb to execute Python scripts that have access to the functionality of gdb. For the simplest example, we can enter a minimal python script directly in gdb:

py
print("hello world") # Press Ctrl+D to exit the python mode
hello world

We can also call a script like this (which you could also load in a command file)

source hello_world.py
hello world

When using the gdb Python API, gdb exposes numerous functions and objects to Python.

py
print(gdb.lookup_type('int').sizeof)
4

You can find the entire API documented here. Check out our Python interface file to see how we use the API.

Some gdb Terminology

Since the Python API is a bit lower level, we’ll need to explain some terminology. Let’s consider this program:

int choice()
{
    return 42;
}

int main() 
{
    int value =42;
    if (choice() == 42) {
        double value  = 3.142;
    } else {
        char value = "foo";
    }
    
    return 0;
}

A "frame" (short for "stack frame") is something that was called, mostly functions. If we stop with gdb inside choice, we are in a function frame, who has a parent frame for the call to main.

A "block" is the same as what is called a scope in C++. choice only has one block, while main has three: the main function block, and one for each condition. If you look in the above code, value can refer to very different things depending on the block. In the gdb CLI, this is implicit, but in the Python API we’ll need to use blocks & frames explicitly.

Getting Data

One difficulty of embedded systems is that they don’t really send a return code back to the host system when they exit. For our purposes, we’ll add a function that can be used during debugging.

void _exit(int);

Now we want to add a small python script that informs us when the _exit function is called and prints the exit code.

# exitcode.py

class ExitBreakpoint(gdb.Breakpoint):
    def __init__(self):
        # Initialize the breakpoint for `_exit`
        gdb.Breakpoint.__init__(self, "_exit")
    def stop(self):
        frame = gdb.selected_frame() # Use the current frame
        # Find all the arguments in the current block
        args = [arg for arg in frame.block() if arg.is_argument]

        exit_code = None
        for arg in args:
            exit_code = str(arg.value(frame))
            break
        print("Program exited with " + exit_code)
    
  exit_code = ExitBreakpoint()

Now we can load our script into gdb:

source exitcode.py
Breakpoint 1 at 0x40053d: file startup.c, line 142.
run
Program exited with 0

As you can see, it is quite simple to get data from the embedded target and have the host print it for you. You can of course do much more with this API, like implement a full unit-test framework

Setting Data

We may also want to send data to the target. Let’s consider this scenario: we have a function choice that has a parameter, and we want to set that parameter before running the executable. First. we use a gdb.Parameter for the setting:

class ChoiceParam(gdb.Parameter):
    def __init__(self):
        super(ChoiceParam, self).__init__("choice",
                                              `gdb`.COMMAND_DATA,
                                              `gdb`.PARAM_INTEGER)
        self.value = 0

    set_doc = '''Some choice for our target.'''
    show_doc = '''This parameter represents a choice for something.'''

choiceParam = ChoiceParam()

Now we can set this parameter in gdb:

set choice 42
Some choice for our target.

Next we need to get the data into the C++ code. Our function looks like this:

int choice() {volatile int the_choice = 0; return the_choice;}

This function uses a volatile value inside so the debugger can set it while also ensuring that the optimizer doesn’t remove it. In order to set the variable, we implement the breakpoint as follows:

# choice.py

class ChoiceBreakpoint(gdb.Breakpoint):
    gdb.Breakpoint.__init__(self, "choice")

    def stop(self, bp):
        gdb.parse_and_eval('the_choice = {}'.format(len(choiceParam.value)))
choiceBreakpoint = ChoiceBreakpoint()

We can now put both together in a program like this:

int choice() {volatile int the_choice = 0; return the_choice;}
int main() 
{
    return choice();
}

And observe the impact of setting this variable by combining it with our exitcode.py script:

source exitcode.py
Breakpoint 1 at 0x40053d: file startup.c, line 142.
source choice.py
Breakpoint 1 at 0x40053d: file main.c, line 1.
set choice 12
Some choice for our target.
run
Program exited with 12

Summary

As you can see, it is quite easy to use gdb in combination with Python to automate communication with a target process. The metal.gdb project provides additional solutions along these lines, such as a unit-test framework and a convenient way to combine several breakpoints into one.

Share Your Thoughts

This site uses Akismet to reduce spam. Learn how your comment data is processed.