Example: mcGDB support from scratch

The easiest way to document mcGDB extension methodology might be with an detailed illustration. So in this document, we'll see, step by step, how to build an mcGDB extension for our own programming environment.

Task-Based Programming Environment and Application

In example/task-model.h you'll find the public API of a simple programming environment based on concurrent tasks (implementation is in example/task-model.c):

struct task_s *
task_new(task_body_f body);

void
task_set_dependency(struct task_s *src, struct task_s *dst);

void
task_run(struct task_s *task);

int
task_get_future(struct task_future_int_s *future);

int
task_get_result(struct task_s *task);

void
task_destroy(struct task_s *task);

And example/appli.c contains an application running on top of this environment:

#include "task-model.h"

int produce(void *not_used) {
  return 4; // chosen by fair dice roll.
            // guaranteed to be random
            /* http://xkcd.com/221/ */
}

int consume(void *data) {
  int result = 0;
  struct task_future_int_s *future_input = (struct task_future_int_s *) data;

  /* prepare computation */
  result -= 6;

  /* consume input */
  result += task_get_future(future_input) * 12;

  return result;;
}

int main(void) {
  struct task_s *prod = task_new(produce);
  struct task_s *cons = task_new(consume);

  task_set_dependency(NULL, prod);
  task_set_dependency(prod, cons);

  task_run(prod);
  task_run(cons);

  printf("Async result: %d\n", task_get_result(cons));

  task_destroy(prod);
  task_destroy(cons);
}

Fairly easy and straightforward, everything is sequential, task_run() does barely nothing and body functions are actually executed inside task_get_result().

mcGDB Debugging Support

mcGDB debugging support is implemented inside example/task_model_debugging.py.

Initialization

To load you Python module, you need to these two commands (or similar) in GDB:

1
2
python sys.path.append(".")
python import task_model_debugging

Line 1 tells Python where to look for your packages, and line 2 actually loads it.

For the example, I put these lines into example/gdbinit, and run GDB with gdb -ex "source ./gdbinit" (for security reasons, GDB may not want to load .gdbinit from an untrusted directory).

Capture Module

Let's look at the function struct task_s * task_new(task_body_f body); capture breakpoint: TaskNewBreakpoint:

# Capture `task_new` function calls
class TaskNewBreakpoint(mcgdb.capture.FunctionBreakpoint):
    def __init__(self):
        mcgdb.capture.FunctionBreakpoint.__init__(self, "task_new")

    def prepare_before (self):
        data = {}
        data["body"] = gdb.parse_and_eval("body")
        TaskBodyExecutionBreakpoint(str(data["body"]).split(" ")[0])

        return False, True, data

    def prepare_after (self, data):
        data["task_handle"] = gdb.parse_and_eval("(struct task_s *) {}".format(RETURN_VALUE_REGISTER))

        debug_capture("New task created: {} (id: {})".format(data["body"], data["task_handle"]))
        Task(data["task_handle"], data["body"])

In prepare_before(), we capture the first parameter, corresponding to the body function of the task. We also set a dynamic breakpoint, at the address of task body function.

In prepare_after(), we capture the return parameter, corresponding to the task handler. The call to debug_capture() remains (on purpose) from the early stage of the development: it was used to ensure that the breakpoint was correctly triggered, and the parameters successfully captured.

Lastly, we pass these parameters to the representation module, by creating a new Task instance.

All the other capture breakpoints are based on the same template:

# Instantiate capture breakpoints
def init_capture():
    TaskNewBreakpoint()
    TaskSetDependencyBreakpoint()
    TaskRunBreakpoint()
    TaskGetResultBreakpoint()
    TaskGetFutureBreakpoint()
    TaskDestroyedBreakpoint()

Representation Module

The representation module consists here of a single class definition, Task:

# Debugger representation for tasks
class Task:
    tasks = {}
    uids = 0

    def __init__(self, handle, body):
        self.body = body
        self.alive = True
        self.result = None
        self.depends_of_task = None
        self.executing = False
        self.running = False
        self.uid = Task.uids
        Task.uids += 1

        handle = str(handle)
        self.handle = handle
        Task.tasks[handle] = self
        info_representation("New task {} #{}".format(handle, self.uid))

    def run(self):
        self.running = True

    ## <Documentation representation start_exec example>    # called when the task starts its execution
    def start_execution(self):
        if mcgdb.toolbox.catchable.is_set("catch_exec", self.uid):
            mcgdb.capture.FunctionBreakpoint.push_stop_request(
                "Execution catchpoint triggered by Task #{}".format(self.uid))
        self.executing = True
## </Documentation representation start_exec example>#

    def finish_execution(self):
        self.executing = False

    def depends_of(self, src):
        self.depends_of_task = src

    def got_result(self, res):
        self.running = False
        self.result = res

    def ask_result_from(self, rmt):
        self.running = False
        pass

    def got_result_from(self, rmt, res):
        self.running = True

    def destroy(self):
        self.alive = False
        info_representation("Task {} #{} destroyed".format(self.handle, self.uid))

There is a more-or-less one-to-one mapping between the capture breakpoints and methods of class Task:

Note

The code in this class is independent of the actual implementation of the supportive environment, another environment based on the same model could reuse this class.

Interaction Module

The last consist in implementing the user interaction module. In this example that's fairly simple and limited.

First, for listing the tasks of the application, we browse the task_model_debugging.Task.tasks list and display the different parameters of the task instances:

# List the tasks of an application
class cmd_task_info(gdb.Command):
    def __init__(self):
        gdb.Command.__init__(self, "task info", gdb.COMMAND_OBSCURE, prefix=True)

    def invoke(self, arg, from_tty):
        for t in reversed(Task.tasks.values()):
            print("#{} Task {} {}".format(t.uid, t.handle, str(t.body).split(" ")[1]))
            if t.depends_of_task is not None:
                print("\tDepends of: #{} Task {}".format(t.depends_of_task.uid, t.depends_of_task.handle))
            print("\t{},\t{}, \tResult: {}".format(
                    "Alive" if t.alive else "Dead",
                    "Running" if t.running else "Not running",
                    t.result))

The second example offers catchpoints on the task body execution. It parses the parameters to select specific tasks, or all of them, and then activates the catchpoints in mcgdb.toolbox.catchable.

# Offer catchpoint on task body execution
class cmd_task_catch_exec(gdb.Command):
    """task catch_exec on|off [task id]*
Sets (on) or remove (off) a catchpoint on one (task id), several or all (empty) the tasks.
`task info` not implemented ...
"""

    def __init__ (self):
        gdb.Command.__init__(self, "task catch_exec", gdb.COMMAND_OBSCURE)
        mcgdb.toolbox.catchable.register("catch_exec")

    def invoke(self, arg, from_tty):
        args = gdb.string_to_argv(arg)
        if not args:
            print("Invalid parameters ...")
            print(cmd_task_catch_exec.__doc__)
            return
        do_activate = args.pop(0) == "on"

        tids = [mcgdb.toolbox.catchable.ALL_ENTITIES] if not args else args

        for tid in args:
            try:
                mcgdb.toolbox.catchable.activateRemove("catch_exec",
                                                       int(tid),
                                                       do_activate)
                print("{} execution catchpoint on task #{}.".format("Set" if do_activate else "Unset",
                                                                    tid))
            except Exception as e:
                print("Didn't work for task {}: {}".format(tid, e))

The second part of this mechanism is implemented in task_model_debugging.Task.start_execution(), when a stop request is registered (mcgdb.capture.FunctionBreakpoint.push_stop_request()) when a task with a catchpoint starts its execution:

    # called when the task starts its execution
    def start_execution(self):
        if mcgdb.toolbox.catchable.is_set("catch_exec", self.uid):
            mcgdb.capture.FunctionBreakpoint.push_stop_request(
                "Execution catchpoint triggered by Task #{}".format(self.uid))
        self.executing = True

Python Code for Debugging Support

Module task_model_debugging

class task_model_debugging.Task(handle, body)[source]

Bases: object

ask_result_from(rmt)[source]
depends_of(src)[source]
destroy()[source]
finish_execution()[source]
got_result(res)[source]
got_result_from(rmt, res)[source]
run()[source]
start_execution()[source]
tasks = {}
uids = 0
class task_model_debugging.TaskBodyExecutionBreakpoint(addr)[source]

Bases: mcgdb_lite.FunctionBreakpoint

prepare_after(data)[source]
prepare_before()[source]
class task_model_debugging.TaskDestroyedBreakpoint[source]

Bases: mcgdb_lite.FunctionBreakpoint

prepare_before()[source]
class task_model_debugging.TaskGetFutureBreakpoint[source]

Bases: mcgdb_lite.FunctionBreakpoint

prepare_after(data)[source]
prepare_before()[source]
class task_model_debugging.TaskGetResultBreakpoint[source]

Bases: mcgdb_lite.FunctionBreakpoint

prepare_after(data)[source]
prepare_before()[source]
class task_model_debugging.TaskNewBreakpoint[source]

Bases: mcgdb_lite.FunctionBreakpoint

prepare_after(data)[source]
prepare_before()[source]
class task_model_debugging.TaskRunBreakpoint[source]

Bases: mcgdb_lite.FunctionBreakpoint

prepare_before()[source]
class task_model_debugging.TaskSetDependencyBreakpoint[source]

Bases: mcgdb_lite.FunctionBreakpoint

prepare_before()[source]
class task_model_debugging.cmd_task[source]

Bases: gdb.Command

class task_model_debugging.cmd_task_catch_exec[source]

Bases: gdb.Command

task catch_exec on|off [task id]* Sets (on) or remove (off) a catchpoint on one (task id), several or all (empty) the tasks. task info not implemented ...

invoke(arg, from_tty)[source]
class task_model_debugging.cmd_task_info[source]

Bases: gdb.Command

invoke(arg, from_tty)[source]
task_model_debugging.debug_capture(*objects, **kwargs)[source]
task_model_debugging.info_representation(*objects, **kwargs)[source]
task_model_debugging.init_capture()[source]
task_model_debugging.init_interaction_exec()[source]
task_model_debugging.init_interaction_struct()[source]
task_model_debugging.initialize()[source]

Module mcgdb_lite

class mcgdb_lite.capture[source]

Bases: object

class FunctionBreakpoint(spec)[source]

Bases: gdb.Breakpoint

breakpointed = {}
prepare_after(data)[source]
prepare_before()[source]
classmethod push_stop_request(clazz, msg)[source]
stop()[source]
stop_requests = []
class capture.FunctionFinishBreakpoint(parent, fct_data)[source]

Bases: gdb.Breakpoint

out_of_scope()[source]
stop()[source]
class mcgdb_lite.toolbox[source]

Bases: object

class Catchable[source]

Bases: object

ALL_ENTITIES = '<all entities>'
static activate(name, entity)[source]
static activateRemove(name, entity, do_add)[source]
catch = {}
static is_set(name, entity=None)[source]
static register(name)[source]
static remove(name, entity)[source]
toolbox.catchable = <mcgdb_lite.toolbox.Catchable object>