Building Voltron Command Plugins

In a previous post I described how to build views for Voltron. In this short post I’ll cover building command plugins for Voltron.

The debugger hosts supported by Voltron each provide a method of adding user-defined commands to the debugger CLI. Voltron’s command plugin API provides a way to implement user-defined commands that are debugger-agnostic - so the commands will work in any supported debugger host that implements the full Voltron debugger host adaptor API, and supports command plugins (LLDB and GDB at this stage). This is the case provided that the command implementation only uses functions provided by the debugger host adaptor API, otherwise the command plugin would have to individually support each debugger host.

Hello world

A basic example command plugin:

import voltron
from voltron.plugin import CommandPlugin
from voltron.command import VoltronCommand


class HelloCommand(VoltronCommand):
    def invoke(self, *args):
        print('ohai ^_^')


class HelloCommandPlugin(CommandPlugin):
    name = 'hello'
    command_class = HelloCommand

This example is fairly straight-forward. First we have the HelloCommand class, which subclasses VoltronCommand - this is the implementation of the actual command. The invoke method is called when the command is invoked in the debugger. Then we have the HelloCommandPlugin class, which subclasses CommandPlugin. This is the plugin class that is registered in Voltron’s plugin registry. It specifies the command to register (‘hello’) and the class that contains the command’s implementation.

Installing it:

$ ln -s /path/to/hello.py ~/.voltron/plugins/

Executing it:

$ lldb
Voltron loaded.
Run `voltron init` after you load a target.
(lldb) hello
ohai ^_^
(lldb)

Easy.

A simple register list command

Now for an example that uses the debugger host adaptor API. This interface isn’t documented yet, but the main methods map one-to-one with the methods defined in the JSON API reference. Have a look at voltron/plugins/debugger/dbg_lldb.py.

import blessed
import voltron
from voltron.plugin import CommandPlugin
from voltron.command import VoltronCommand


class ExampleCommand(VoltronCommand):
    def invoke(self, *args):
        regs = voltron.debugger.registers()
        reg_list =  ['rax','rbx','rcx','rdx','rbp','rsp','rdi','rsi','rip',
                     'r8','r9','r10','r11','r12','r13','r14','r15']
        for name in reg_list:
            print("{t.bold}{:3} {t.normal}{:0=16X}".format(name, regs[name], t=blessed.Terminal()))


class ExampleCommandPlugin(CommandPlugin):
    name = 'example'
    command_class = ExampleCommand

You can see the invoke method there calls voltron.debugger.registers() to get the current values for the registers in the current inferior. voltron.debugger is the package-wide reference to the current debugger host adaptor object. In LLDB, this will be an instance of LLDBAdaptor which is defined in voltron/plugins/debugger/dbg_lldb.py. Similarly, in GDB this will be an instance of GDBAdaptor from voltron/plugins/debugger/dbg_gdb.py. Both classes implement most of the same methods (including registers).

Install it as per the previous example:

$ ln -s /path/to/example.py ~/.voltron/plugins/

Executing it:

(lldb) example
rax 0000000100000CF0
rbx 0000000000000000
rcx 00007FFF5FBFFAA0
rdx 00007FFF5FBFF9B0
rbp 00007FFF5FBFF990
rsp 00007FFF5FBFF988
rdi 0000000000000001
rsi 00007FFF5FBFF9A0
rip 0000000100000CF0
r8  0000000000000000
r9  00007FFF5FBFEA48
r10 0000000000000032
r11 0000000000000246
r12 0000000000000000
r13 0000000000000000
r14 0000000000000000
r15 0000000000000000

An LLDB-specific command plugin

If the adaptor API doesn’t cover what you want to do in command plugins, you can access the host debugger instance itself and perform debugger API-specific actions. Here’s an example of an API plugin that calls LLDB’s SBDebugger.GetVersionString():

import voltron
from voltron.plugin import CommandPlugin
from voltron.command import VoltronCommand


class LLDBHelloCommand(VoltronCommand):
    def invoke(self, *args):
        print("Debugger host: {}".format(voltron.debugger.host))
        print("Do an LLDB thing: {}".format(voltron.debugger.host.GetVersionString()))


class LLDBHelloCommandPlugin(CommandPlugin):
    name = 'lldbhello'
    command_class = LLDBHelloCommand

It’s installed the same way as the previous examples. Executing it:

$ lldb
Voltron loaded.
Run `voltron init` after you load a target.
(lldb) lldbhello
Debugger host: Debugger (instance: "debugger_1", id: 1)
Do an LLDB thing: lldb-320.3.103

Of course, if you want LLDB-specific features you can always just use the normal LLDB command script API to implement your plugin, rather than the Voltron API, but it’s more fun this way.

So that’s about it. Have a look at the debugger adaptor plugins for the methods you can use in a command plugin in order to be supported cross-debugger host.