Pmodules

pmodules allow users to augment snapshots by performing custom analysis and data structure pretty printing in response to specific events.

In this tutorial, we'll write a basic pmodule using the Lua API. The C API is similar; for that, consult the documentation provided in /opt/backtrace/include/ptrace/pmodule.h from the backtrace-ptrace-modules package. For a full reference of the Lua API, see the Pmodule API page on this site. The example module written here is reproduced in full at the top of the Pmodule API page.

From a pmodule's perspective, a snapshot is generated during two phases: attach and postattach. We are in the attach phase when the ptrace tracer is attached to the target process, during which threads, frames and variables are extracted. We are in the postattach phase after the ptrace tracer detaches from the process, at which point we have a fully populated backtrace object.

A pmodule may react to any of these events by registering a callback. For reference, the possible events are:

Each callback will receive as arguments the objects relevant to the event.

As an example of when you'd want a specific callback, you might want to pretty print a specific data structure if it appears in your snapshot, so you'd register a variable event callback with a filter specifying the type of variable you're interested in (we'll cover filters in more detail below).

These event callbacks typically perform some analysis or additional data extraction/formatting, and augment the snapshot via additional classifiers, annotations, and the like.

Now that we have a better understanding of a pmodule's event-driven design, let's start defining our module. At the global (chunk) scope, call pmodule.define like so:

-- The following functions are stubs; we'll define a real load function later.
pmodule.define{
    id = "pmodule_lua",
    load = function () pmodule.log(pmodule.log_level.warning, "pm_load") end,
    unload = function () pmodule.log(pmodule.log_level.warning, "pm_unload") end
}

id and load are required fields; unload is optional. They are used as such:

pmodule.define will register the module with the pmodule subsystem. After this, the registered load function will be called to establish event handlers as specified. Let's look at an example load:

function pm_load()
    pmodule.register(pmodule.event.postattach, postattach_cb)

    local m = pmodule.match()

    m:add_object("crash")
    m:add_file("crash.c")
    m:add_frame_symbol("recurse", pmodule.match_type.substr)
    m:add_variable_base_type("crash_", pmodule.match_type.substr)
    m:add_variable_ptrace_type(pmodule.variable_type.tuple)

    pmodule.register(pmodule.event.variable,
        function(var)
            var:annotate(pmodule.annotation.critical, "lua: struct var")
        end, m)

    m:reset()
    m:set_fault()
    pmodule.register(pmodule.event.frame, frame_cb, m)
end

Here, we're interested in three types of events: the postattach event, variable extraction events, and frame events. We'll get into some examples of what we can do in each of them, but for now, let's focus on the match filters, starting with the one used for the variable event callback.

All of the object event callbacks (variable, frame, and thread) may be filtered by a match object. Here, we're saying we're interested in a variable extracted from the object file crash, the compilation unit crash.c, whose frame's symbol contains recurse, whose base type contains crash_, and whose variable type is a tuple (i.e. a struct). Since what we want to do with this variable is pretty simple (annotate it with a useless message), we'll just register the event with an anonymous function and this match filter.

    local m = pmodule.match()

    m:add_object("crash")
    m:add_file("crash.c")
    m:add_frame_symbol("recurse", pmodule.match_type.substr)
    m:add_variable_base_type("crash_", pmodule.match_type.substr)
    m:add_variable_ptrace_type(pmodule.variable_type.tuple)

    pmodule.register(pmodule.event.variable,
        function(var)
            var:annotate(pmodule.annotation.critical, "lua: struct var")
        end, m)

Next, let's look at the frame event registration. Here, we're interested in faulting frames, so we reset the match object (you can also just use a different one), set it to match faulted objects, and then register the event using that.

    m:reset()
    m:set_fault()
    pmodule.register(pmodule.event.frame, frame_cb, m)

The postattach event registration call is pretty vanilla, but we'll go deeper into that callback later.

Now that we've registered all our callbacks, our module is ready to start handling events. ptrace will execute its registered callbacks, assuming any specified match filters pass. Let's look at what we want to do when a faulted frame is extracted:

local function frame_cb(fr)
    for v, i in fr:fprm() do 
        v:annotate(pmodule.annotation.critical, "lua: fprm %d", i)
    end

    local signal = fr:siginfo()
    if signal then
        fr:backtrace():annotate(
            pmodule.annotation.json,
            '{"json": {"context": "Signal", \z
            "Reason": "%s", \z
            "Populated": "%s", \z
            "Address": "%x", \z
            "Num": "%d", \z
            "Code": "%d", \z
            "String": "%s"}}',
            signal:reason(),
            tostring(signal:address_populated()),
            signal:address(),
            signal:num(),
            signal:code(),
            tostring(signal))
    end
end

Pretty simple. First, we want to annotate all parameters of the faulting frame (this is for illustrative purposes only; hydra will indicate all frame parameters by surrounding them with ( )). For many objects (we'll discuss more later), pmodules support Lua's generic for interface. Here, we can simply iterate over the frame's parameters, which will give us a variable object and a parameter index per iteration.

Note: all four main objects (variables, frames, threads, and the overarching backtrace) may be annotated.

Next, we want to pretty print signal information (again, this is just an example; hydra already has this pretty-printed under the faulting frame). We'll annotate the backtrace object here, so the pretty printed data will be displayed in hydra's Process pane. We'll use the json annotation type for this -- refer to the Pmodule API documentation for more details on the expected format.

Let's look at the postattach callback next:

local function postattach_cb(bt)
    local m = pmodule.match()
    m:set_fault()

    -- Example iterators.
    for thr in bt, m do
        for fr in thr do
            for var in fr do
                if var:type() == pmodule.variable_type.reference then
                    local addr = var:value()

                    var:annotate(pmodule.annotation.critical,
                        "[%x] %s - example annotation",
                        var:value(), var:name())
                end
            end
        end
    end

    -- Example global variable iteration.
    pmodule.log(pmodule.log_level.warning, "Global variables:");
    for var, object, cu in bt:variables(), {object = "crash", cu = "invalid_write.c"} do
        pmodule.log(pmodule.log_level.warning,
            "name: %s, value: %s, object: %s, cu: %s",
            var:name(), tostring(var:value()), object, cu)
    end

    -- Example TLS variable iteration.
    for thr in bt do
        pmodule.log(pmodule.log_level.warning, "TLS variables:");

        for var, object, cu in thr:variables(), {cu = "hang"} do
            pmodule.log(pmodule.log_level.warning,
                "name: %s, value: %s, object: %s, \z
                cu: %s",
                var:name(), tostring(var:value()), object, cu)
        end
    end

    -- Example global variable lookup by name.
    for var in bt:variables(), {name = "global_version"} do
        local str = pmodule.address_read_string(var:value(), 256)

        pmodule.log(pmodule.log_level.warning,
            "[%x] string: %s", var:value(), str)
    end

    pmodule.log(pmodule.log_level.warning, "process state: %d",
        bt:process_state())

    bt:add_kv_int("lua_key1", 42)
    bt:add_kv_string("lua_key2", "lua_value")
    bt:add_classifier("lua")
end

Here, we start with iteration. The backtrace object, threads, and frames may be iterated over (again using Lua's generic for interface). Each of these iterators accept an optional match object via the invariant state (the second expression in the for's expression list). In the first iterator, we're iterating over all faulting threads.

    local m = pmodule.match()
    m:set_fault()

    -- Example iterators.
    for thr in bt, m do
        for fr in thr do
            for var in fr do
                if var:type() == pmodule.variable_type.reference then
                    local addr = var:value()

                    var:annotate(pmodule.annotation.critical,
                        "[%x] %s - example annotation",
                        var:value(), var:name())
                end
            end
        end
    end

Note: the full backtrace will be populated only within the postattach callback (which is why we do the full iteration example in there). In the thread callback, you're guaranteed the full thread will be populated (with its frames and variables). In the frame callback, you're guaranteed the full frame will be populated (with its variables).

Next, we'll cover global and TLS variable iteration.

    -- Example global variable iteration.
    pmodule.log(pmodule.log_level.warning, "Global variables:");
    for var, object, cu in bt:variables(), {object = "crash", cu = "invalid_write.c"} do
        pmodule.log(pmodule.log_level.warning,
            "name: %s, value: %s, object: %s, cu: %s",
            var:name(), tostring(var:value()), object, cu)
    end

    -- Example TLS variable iteration.
    for thr in bt do
        pmodule.log(pmodule.log_level.warning, "TLS variables:");

        for var, object, cu in thr:variables(), {cu = "hang"} do
            pmodule.log(pmodule.log_level.warning,
                "name: %s, value: %s, object: %s, \z
                cu: %s",
                var:name(), tostring(var:value()), object, cu)
        end
    end

These iterations use the same generic for interface the previously mentioned thread, frame, etc. iterators use, with one difference - the filter (invariant state) here is a table instead of a match object. The supported fields are:

All of these fields (and the filter table itself) are optional. Each iteration returns a variable object, the object file name, and the cu name.

Let's say we're interested in a particular global variable - some variable containing the version of our program:

    -- Example global variable lookup by name.
    for var in bt:variables(), {name = "global_version"} do
        local str = pmodule.address_read_string(var:value(), 256)

        pmodule.log(pmodule.log_level.warning,
            "[%x] string: %s", var:value(), str)
    end

To find it, we use the global variable iterator with a name filter. This is a C-string variable (i.e. a pointer), so we'll need to use the global read API -- pmodule.address_read_string -- to actually read the string.

Suppose we've noticed something interesting (perhaps application-specific) about the data in our snapshot, and we want to classify the snapshot according to this. Your pmodule may use the following API for that:

    -- bt is a backtrace object.
    bt:add_kv_int("lua_key1", 42)
    bt:add_kv_string("lua_key2", "lua_value")
    bt:add_classifier("lua")

Previously, we mentioned bt_variables. Pmodules can use these to access specific fields in structures and iterate over arrays, linked lists, and other data structures. A full example is reproduced below. Note: bt_variables themselves only allow access to member fields, which return new bt_variables; to extract actual data from them, they must first be synthesized into pmodule variables.

--[[

-- Assume we have these definitions:

struct nested {
    int c;
};

struct linkedstruct {
    STAILQ_ENTRY(linkedstruct) linkage;
    int v;
};

struct somestruct {
    int a;
    double b;
    struct nested n;
    struct nested *np;
    struct nested **npp;
    struct nested ***nppp;
    STAILQ_HEAD(, linkedstruct) list;
    const char *s;
};

struct array_struct {
    struct somestruct *kids;
    int len;
};

-- And these global variables:

static struct somestruct *some_struct_g;
static struct array_struct papa;

-- And the following population code:

papa.kids = calloc(10, sizeof(struct somestruct));
if (papa.kids == NULL) {
    fprintf(stderr, "allocation failure");
    exit(EXIT_FAILURE);
}
papa.len = 10;

some_struct_g = calloc(1, sizeof *some_struct_g);
if (some_struct_g == NULL) {
    fprintf(stderr, "allocation failure");
    exit(EXIT_FAILURE);
}

STAILQ_INIT(&some_struct_g->list);
for (z = 0; z < 10; ++z) {
    struct linkedstruct *l;

    l = calloc(1, sizeof *l);
    if (l == NULL) {
        fprintf(stderr, "allocation failure");
        exit(EXIT_FAILURE);
    }

    l->v = z;

    STAILQ_INSERT_TAIL(&some_struct_g->list, l, linkage);
}

some_struct_g->a = 4;
some_struct_g->b = 5;
some_struct_g->n.c = 6;
some_struct_g->np = &some_struct_g->n;
some_struct_g->npp = &some_struct_g->np;
some_struct_g->nppp = &some_struct_g->npp;
some_struct_g->s = "kids";
for (z = 0; z < 10; ++z) {
    papa.kids[z].a = 9 + z;
    papa.kids[z].b = 9 + z;
    papa.kids[z].n.c = 9 + z;
    papa.kids[z].np = &papa.kids[z].n;
    papa.kids[z].npp = &papa.kids[z].np;
    papa.kids[z].nppp = &papa.kids[z].npp;
    papa.kids[z].s = "papa";
}

]]--

local function attach_cb(bt)
    -- Note: this is a global (Lua) variable.
    -- cached_ref_var will be nil if this fails. variable_cb will
    -- attempt to use this, resulting in an exception if nil.
    cached_ref_var = pmodule.bt_query("some_struct_g", "crash", "crash.c")

    local n = cached_ref_var.list.stqh_first
    local v = pmodule.variable_from_bt(n)

    while v:value() ~= 0 do
        local val = pmodule.variable_from_bt(n.v)
        print(string.format("[%x] %s: %d",
            val:address(), val:name(), val:value()))
        n = n.linkage.stqe_next
        v = pmodule.variable_from_bt(n)
    end
end

local function variable_cb(var, raw)
    -- Variables synthesized this way will be finalized/freed. They cannot
    -- be added to the snapshot. All synthesized variables must go through
    -- the synthesis API.
    -- This is enforced through the API.
    local n = pmodule.variable_from_bt(raw.len):value()
    local addr = pmodule.variable_from_bt(raw.kids):value()

    -- We'll use size to iterate over the array.
    local size = pmodule.sizeof(pmodule.deref(cached_ref_var))

    print(string.format("addr: %x, len: %d, size: %d", addr, n, size))

    for i = 0, n - 1 do
        local na = addr + i * size

        print(string.format("--- iter %d (%x)---", i, na))

        elem = pmodule.deref(cached_ref_var, addr + i * size)

        print(string.format("a: %d",
            pmodule.variable_from_bt(elem.a):value()))
        print(string.format("b: %f",
            pmodule.variable_from_bt(elem.b):value()))

        -- Note: when accessing member fields, pointers are
        -- automatically dereferenced until we reach the underlying
        -- struct.
        print(string.format("nppp.c: %d",
            pmodule.variable_from_bt(elem.nppp.c):value()))
    end
end

local function pm_load()
    local m = pmodule.match()

    m:add_object("crash")
    m:add_file("crash.c")
    m:add_variable_base_type("array_struct", pmodule.match_type.substr)
    m:add_variable_ptrace_type(pmodule.variable_type.tuple)
    m:add_variable_name("papa", pmodule.match_type.substr)

    pmodule.register(pmodule.event.variable, variable_cb, m)
    pmodule.register(pmodule.event.attach, attach_cb)
end

pmodule.define{
    id = "array_iter",
    load = pm_load
}

There are a few CLI options to keep in mind when using pmodules:

For more details, consult ptrace -h output.

That concludes this tutorial. We've reviewed how to register your pmodule with ptrace, how to register callbacks for events that interest you, how to iterate over various snapshot objects, and some of what you can do with individual objects. Of course, there's more one can do in a pmodule -- for a full reference, consult the pmodule API documentation.