Building Voltron Views
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.
$ lldb tests/inferior
Voltron loaded.
Run `voltron init` after you load a target.
Current executable set to 'tests/inferior' (x86_64).
(lldb) voltron init
Registered stop-hook
(lldb) b main
Breakpoint 1: where = inferior`main, address = 0x0000000100000cf0
(lldb) run
Process 16669 launched: '~/code/voltron/repo/tests/inferior' (x86_64)
Process 16669 stopped
* thread #1: tid = 0x1a2238, 0x0000000100000cf0 inferior`main, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
frame #0: 0x0000000100000cf0 inferior`main
inferior`main:
-> 0x100000cf0: push rbp
0x100000cf1: mov rbp, rsp
0x100000cf4: sub rsp, 0x50
0x100000cf8: mov dword ptr [rbp - 0x4], 0x0
(lldb)
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.
>>> import voltron
>>> from voltron.core import Client
>>> client = Client()
>>> client.connect()
>>> client.perform_request('state')
<<class 'state.APIStateResponse'>: success = True, error = False, body: {'status': u'success', 'state': u'stopped', 'type': u'response'}>
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:
>>> res = client.perform_request('registers', registers=['rsp'])
>>> res.is_success
True
>>> res.registers
{u'rsp': 140734799804616}
Now we’ll read some memory from the inferior’s stack using the value of the stack pointer register we just got:
>>> rsp = res.registers['rsp']
>>> res = client.perform_request('memory', address=rsp, length=0x40)
>>> res.memory
'\xfd\x05\xf1\x92\xff\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00X\xfa\xbf_\xff\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x93\xfa\xbf_\xff\x7f\x00\
x00\xaa\xfa\xbf_\xff\x7f\x00\x00\xbe\xfa\xbf_\xff\x7f\x00\x00'
>>> res.bytes
64
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.
>>> res = client.perform_request('wait')
Now in the debugger:
(lldb) stepi
Process 16669 stopped
* thread #1: tid = 0x1a2238, 0x0000000100000cf1 inferior`main + 1, queue = 'com.apple.main-thread', stop reason = instruction step into
frame #0: 0x0000000100000cf1 inferior`main + 1
inferior`main + 1:
-> 0x100000cf1: mov rbp, rsp
0x100000cf4: sub rsp, 0x50
0x100000cf8: mov dword ptr [rbp - 0x4], 0x0
0x100000cff: mov dword ptr [rbp - 0x8], edi
(lldb)
And the perform_request()
function should have returned a successful response:
>>> res
<<class 'wait.APIWaitResponse'>: success = True, error = False, body: {'status': u'success', 'state': u'stopped', 'type': u'response'}>
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 python
import voltron
from voltron.core import Client
def main():
# Create a client and connect to the server
client = Client()
client.connect()
# Main event loop
while True:
# Wait for the debugger to stop again
res = client.perform_request('wait')
if res.is_success:
# If nothing went wrong, get the instruction pointer and print it
res = client.perform_request('registers', registers=['rip'])
if res.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))
break
if __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:
$ python client.py
Instruction pointer is: 0x100000CF8
Instruction pointer is: 0x100000CFF
Instruction pointer is: 0x100000D02
Instruction pointer is: 0x100000D06
Instruction pointer is: 0x100000D0D
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.
from voltron.plugin import ViewPlugin
class ExampleViewPlugin(ViewPlugin):
name = 'example'
view_class = ExampleView
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.
from voltron.view import TerminalView
class ExampleView(TerminalView):
def render(self, *args, **kwargs):
self.clear()
res = self.client.perform_request('registers', registers=['rip'])
if res.is_success:
print("Instruction pointer is: 0x{:X}".format(res.registers['rip']))
else:
print("Failed to get registers: {}".format(res))
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.
The complete example:
from voltron.view import TerminalView
from voltron.plugin import ViewPlugin
class ExampleView(TerminalView):
def render(self, *args, **kwargs):
self.clear()
res = self.client.perform_request('registers', registers=['rip'])
if res.is_success:
print("Instruction pointer is: 0x{:X}".format(res.registers['rip']))
else:
print("Failed to get registers: {}".format(res))
class ExampleViewPlugin(ViewPlugin):
name = 'example'
view_class = ExampleView
If you put this code into a file and symlink it into your Voltron plugins directory, like so:
$ ln -s /path/to/example.py ~/.voltron/plugins/
You should now see the plugin listed in the view types at the command line:
usage: voltron view [-h]
{backtrace,register,example,command,memory,disassembly,stack}
...
voltron view: error: too few arguments
Run the view:
$ voltron view example
Each time you stepi
in the debugger, the text should update:
Instruction pointer is: 0x100000E75
There’s also some more functionality that the parent classes provide. Here’s a more typical view example:
from voltron.view import TerminalView
from voltron.plugin import ViewPlugin
class ExampleView(TerminalView):
def render(self, *args, **kwargs):
# Perform the request and set the body to the result
res = self.client.perform_request('registers', registers=['rip'])
if res.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 info
self.title = '[example]'
self.info = 'some infoz'
# Let the parent do the rendering
super(ExampleView, self).render()
class ExampleViewPlugin(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:
from voltron.view import TerminalView
from voltron.plugin import ViewPlugin
class ExampleView(TerminalView):
def render(self, *args, **kwargs):
# Perform the request
res = self.client.perform_request('registers')
if res.is_success:
# Process the registers and set the body to the formatted list
reg_list = ['rax','rbx','rcx','rdx','rbp','rsp','rdi','rsi','rip',
'r8','r9','r10','r11','r12','r13','r14','r15']
lines = map(lambda x: '{: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 info
self.title = '[example]'
self.info = 'some infoz'
# Let the parent do the rendering
super(ExampleView, self).render()
class ExampleViewPlugin(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:
$ voltron view example -h
usage: voltron view example [-h] [--show-header] [--hide-header]
[--show-footer] [--hide-footer] [--name NAME]
optional arguments:
-h, --help show this help message and exit
--show-header, -e show header
--hide-header, -E hide header
--show-footer, -f show footer
--hide-footer, -F hide footer
--name NAME, -n NAME named configuration to use
$ voltron view example -f
Which should look like this:
We can also configure the header and footer contents in the config file (~/.voltron/config
) like this:
{
"view": {
"example_view": {
"footer": {
"show": true,
"label_left": {
"name": "title",
"colour": "red",
"bg_colour": "grey",
"attrs": ["bold"]
},
"label_right": {
"name": "info",
"colour": "white",
"bg_colour": "grey",
"attrs": []
}
},
"header": {
"show": false
}
}
}
}
This configuration should be fairly self-explanatory. It will hide the header, and the footer will contain the title in red on the left, and the info field in white on the right, like this:
Until next time…
This should give you a good starting point for building Voltron views. See the source for the views included with the Voltron package for more clues.
The next post will describe how to build Voltron API plugins to add server-side functionality that you can access from your views.