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.

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.

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.

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).

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():

In 2013 I released a small debugger UI tool called Voltron. In the last few months I completely rewrote the guts of it to be much more extensible, and gave a short talk at Kiwicon 8 on it, so I figured it was about time for an update. This post will take you through building new Voltron views for your own purposes.

Some of the information in this blog post is covered in the documentation on the wiki.

Note: I’m using LLDB in this post, but you can use GDB and it will be much the same.

Poking around in the REPL

Whenever I write Python I spend a lot of time poking around in classes in a REPL, often in lieu of actually consulting the documentation, so that seems like a good place to start.

First off let’s fire up LLDB with an inferior, initialise Voltron, set a breakpoint and run the executable so we have some useful debugger state to poke at. I’m using the test inferior from the Voltron package, but use whatever.

Now load up a Python REPL in another terminal (I use bpython) and import the Voltron package. We can then instantiate a Voltron client object, connect to the Voltron server running in the debugger, and issue API requests.

The perform_request() function creates an API request of the specified type with the given parameters (none in this case), sends it to the server, and returns an instance of a subclass of APIResponse containing the response. In this example we’ve queried the state of the debugger and can see that the request was successful, and that the debugger is stopped.

Now let’s try getting the contents of a register, say RSP, with the registers API method:

One more important API method worth noting at this point is the wait method. The server will only return a response once the specified debugger state change has occurred. Currently the only state change supported is ‘stopped’ - signaling that the debugger has stopped for some reason (for example a breakpoint was hit, the user stepped over an instruction, etc). This is the default state change, so we don’t need to specify it.

See the API reference for information on the supported core API methods.

Building a simple standalone client

Now that we have a basic understanding of how the API works, let’s build a simple client.

#!/usr/bin/env pythonimportvoltronfromvoltron.coreimportClientdefmain():# Create a client and connect to the serverclient=Client()client.connect()# Main event loopwhileTrue:# Wait for the debugger to stop againres=client.perform_request('wait')ifres.is_success:# If nothing went wrong, get the instruction pointer and print itres=client.perform_request('registers',registers=['rip'])ifres.is_success:print("Instruction pointer is: 0x{:X}".format(res.registers['rip']))else:print("Failed to get registers: {}".format(res))else:print("Error waiting for the debugger to stop: {}".format(res))breakif__name__=="__main__":main()

Pretty self-explanatory - we’ve basically just put together all the bits discussed in the previous section. We use the wait method to construct a main event loop that executes some stuff every time the debugger stops. The stuff that we execute just grabs the contents of the RIP register and prints it out. Hooray!

If we run the client and then stepi a few times in the debugger, the output from the client looks something like this:

Building a simple view plugin

OK, so we get the gist of how the API works and we’ve used it to build a simple standalone client. Now let’s turn our standalone example into a simple view plugin. Each of the core views included with Voltron that are accessible via the voltron view command is implemented as a view plugin like this.

First, we need to subclass ViewPlugin from the voltron.view module. This is the main entry point for the plugin and contains the name of the plugin and a reference to the main view class for the plugin.

Since this contains a reference to the view class, the view class will need to be defined before the plugin class in the file. So above the plugin class we’ll define the view class, which subclasses TerminalView from the voltron.view module.

The important attribute is the render() method. The TerminalView class (well, actually its parent, the VoltronView class) implements something similar to the main event loop in our example standalone client; each time the debugger stops, the view’s render() method is called.

The parent classes define a few useful attributes. Firstly the clear() method which we call at the beginning of the render() method to clear the screen. Secondly, a Client instance called client.

After that we do pretty much the same thing as in our standalone example, only using the view’s included client object.

There’s also some more functionality that the parent classes provide. Here’s a more typical view example:

fromvoltron.viewimportTerminalViewfromvoltron.pluginimportViewPluginclassExampleView(TerminalView):defrender(self,*args,**kwargs):# Perform the request and set the body to the resultres=self.client.perform_request('registers',registers=['rip'])ifres.is_success:self.body="Instruction pointer is: 0x{:X}".format(res.registers['rip'])else:self.body="Failed to get registers: {}".format(res)# Set the title and infoself.title='[example]'self.info='some infoz'# Let the parent do the renderingsuper(ExampleView,self).render()classExampleViewPlugin(ViewPlugin):name='example'view_class=ExampleView

In this example, rather than printing the view contents out directly, we set self.body to the output we want in the main body of the view, and set self.title and self.info to what we want in the title and info fields respectively.

We then let the parent classes render it, which should look something like this:

Finally, with some small modifications we can turn this into an x86_64 register view:

fromvoltron.viewimportTerminalViewfromvoltron.pluginimportViewPluginclassExampleView(TerminalView):defrender(self,*args,**kwargs):# Perform the requestres=self.client.perform_request('registers')ifres.is_success:# Process the registers and set the body to the formatted listreg_list=['rax','rbx','rcx','rdx','rbp','rsp','rdi','rsi','rip','r8','r9','r10','r11','r12','r13','r14','r15']lines=map(lambdax:'{:3}: {:016X}'.format(x,res.registers[x]),reg_list)self.body='\n'.join(lines)else:self.body="Failed to get registers: {}".format(res)# Set the title and infoself.title='[example]'self.info='some infoz'# Let the parent do the renderingsuper(ExampleView,self).render()classExampleViewPlugin(ViewPlugin):name='example'view_class=ExampleView

Looking something like this:

We can now modify the way the view is displayed, say to show the footer, with the automatically-added command line flags: