diff --git a/tests/gdb_cli_tests.py b/tests/gdb_cli_tests.py index c10fed9f..ce244ad7 100644 --- a/tests/gdb_cli_tests.py +++ b/tests/gdb_cli_tests.py @@ -42,7 +42,7 @@ def setup(): voltron.setup_env() # compile test inferior - pexpect.run("cc -o tests/inferior tests/inferior.c") + pexpect.run("cc -g -o tests/inferior tests/inferior.c") # start debugger start_debugger() @@ -124,6 +124,13 @@ def test_memory(): assert res.status == 'success' assert len(res.memory) > 0 +def test_source_location(): + res = client.perform_request('source_location', address=registers['rip']) + assert res.status == 'success' + assert not res.output is None + file, line = res.output + assert os.path.basename(file) == 'inferior.c' + assert line == 12 def test_state_stopped(): res = client.perform_request('state') diff --git a/tests/lldb_cli_tests.py b/tests/lldb_cli_tests.py index a198fc19..bdf422b2 100644 --- a/tests/lldb_cli_tests.py +++ b/tests/lldb_cli_tests.py @@ -196,6 +196,12 @@ def test_disassemble(): assert len(res.disassembly) > 0 assert 'push' in res.disassembly +def test_source_location(): + restart() + time.sleep(1) + res = client.perform_request('source_location', address=registers['rip']) + assert res.status == 'success' + assert res.output is None def test_dereference(): restart() diff --git a/tests/lldb_cli_tests_with_symbols.py b/tests/lldb_cli_tests_with_symbols.py new file mode 100644 index 00000000..a3410f40 --- /dev/null +++ b/tests/lldb_cli_tests_with_symbols.py @@ -0,0 +1,140 @@ +""" +Tests that test voltron in the lldb cli driver + +Tests: +Client -> Server -> LLDBAdaptor + +Inside an LLDB CLI driver instance +""" +from __future__ import print_function + +import tempfile +import sys +import json +import time +import logging +import pexpect +import os +import tempfile +import six + +from mock import Mock +from nose.tools import * + +import voltron +from voltron.core import * +from voltron.api import * +from voltron.plugin import PluginManager, DebuggerAdaptorPlugin + +from .common import * + +log = logging.getLogger('tests') + +p = None +client = None + + +def setup(): + global p, client, pm + + log.info("setting up LLDB CLI tests") + + voltron.setup_env() + + # compile test inferior + pexpect.run("cc -g -o tests/inferior tests/inferior.c") + + # start debugger + start_debugger() + time.sleep(10) + + +def teardown(): + read_data() + p.terminate(True) + time.sleep(2) + + +def start_debugger(do_break=True): + global p, client + + if sys.platform == 'darwin': + p = pexpect.spawn('lldb') + else: + p = pexpect.spawn('lldb-3.4') + + # for travis + (f, tmpname) = tempfile.mkstemp('.py') + os.write(f, six.b('\n'.join([ + "import sys", + "sys.path.append('/home/travis/virtualenv/python3.5.0/lib/python3.5/site-packages')", + "sys.path.append('/home/travis/virtualenv/python3.4.3/lib/python3.4/site-packages')", + "sys.path.append('/home/travis/virtualenv/python3.3.6/lib/python3.3/site-packages')", + "sys.path.append('/home/travis/virtualenv/python2.7.10/lib/python2.7/site-packages')"]))) + p.sendline("command script import {}".format(tmpname)) + + print("pid == {}".format(p.pid)) + p.sendline("settings set target.x86-disassembly-flavor intel") + p.sendline("command script import voltron/entry.py") + time.sleep(2) + p.sendline("file tests/inferior") + time.sleep(2) + p.sendline("voltron init") + time.sleep(1) + p.sendline("process kill") + p.sendline("break delete 1") + if do_break: + p.sendline("b main") + p.sendline("run loop") + read_data() + client = Client() + + +def stop_debugger(): + p.terminate(True) + + +def read_data(): + try: + while True: + data = p.read_nonblocking(size=64, timeout=1) + print(data.decode('UTF-8'), end='') + except: + pass + + +def restart(do_break=True): + # stop_debugger() + # start_debugger(do_break) + p.sendline("process kill") + p.sendline("break delete -f") + if do_break: + p.sendline("b main") + p.sendline("run loop") + + +def test_registers(): + global registers + restart() + time.sleep(1) + read_data() + res = client.perform_request('registers') + registers = res.registers + assert res.status == 'success' + assert len(registers) > 0 + if 'rip' in registers: + assert registers['rip'] != 0 + else: + assert registers['eip'] != 0 + + +def test_source_location(): + restart() + time.sleep(1) + res = client.perform_request('source_location', address=registers['rip']) + assert res.status == 'success' + assert not res.output is None + file, line = res.output + assert os.path.basename(file) == 'inferior.c' + assert line == 12 + diff --git a/voltron/core.py b/voltron/core.py index f22f375c..da799a23 100644 --- a/voltron/core.py +++ b/voltron/core.py @@ -412,7 +412,7 @@ def __init__(self, host='127.0.0.1', port=5555, sockfile=None, url=None, self.server_version = None self.block = False self.supports_blocking = supports_blocking - + self.sw = None def send_request(self, request): """ Send a request to the server. @@ -570,17 +570,28 @@ def start(self, build_requests=None, callback=None): """ Run the client using a background thread. """ + if not self.done and self.sw != None: + self.callback(error='Error: Client is already running') + return + + if self.done and self.sw != None: + if self.sw.is_alive(): + self.sw.join() + self.sw = None + if callback: self.callback = callback if build_requests: self.build_requests = build_requests # spin off requester thread + self.done = False self.sw = threading.Thread(target=self.run) self.sw.start() - def stop(self): """ Stop the background thread. """ self.done = True + self.sw.join() + self.sw = None diff --git a/voltron/plugins/api/source_location.py b/voltron/plugins/api/source_location.py new file mode 100644 index 00000000..f8daee7d --- /dev/null +++ b/voltron/plugins/api/source_location.py @@ -0,0 +1,66 @@ +import logging + +import voltron +from voltron.api import * + +from scruffy.plugin import Plugin + +log = logging.getLogger('api') + +class APISourceLocationRequest(APIRequest): + """ + API source location from address request. + + { + "type": "request", + "request": "source_location" + "data": { + "target_id": 0, + "address": 0xffffff8012341234 + } + } + `target_id` is optional. + `address` is the address to lookup source information for. Defaults to + instruction pointer if not specified. + """ + _fields = {'target_id': False, 'address': False} + + @server_side + def dispatch(self): + try: + output = voltron.debugger.source_location(target_id=self.target_id, address=self.address) + log.debug('output: {}'.format(str(output))) + res = APISourceLocationResponse() + res.output = output + except NoSuchTargetException: + res = APINoSuchTargetErrorResponse() + except Exception as e: + msg = "Exception finding source location from address: {}".format(repr(e)) + log.exception(msg) + res = APIGenericErrorResponse(msg) + + return res + + +class APISourceLocationResponse(APISuccessResponse): + """ + API source location from address response. Output may be None if no + sorce information is available for the address + + { + "type": "response", + "status": "success", + "data": { + "output": ["main.c", 8] + } + } + """ + _fields = {'output': True} + + output = None + + +class APISourceLocationPlugin(APIPlugin): + request = "source_location" + request_class = APISourceLocationRequest + response_class = APISourceLocationResponse diff --git a/voltron/plugins/debugger/dbg_gdb.py b/voltron/plugins/debugger/dbg_gdb.py index 5e9c5367..6bbaec3c 100644 --- a/voltron/plugins/debugger/dbg_gdb.py +++ b/voltron/plugins/debugger/dbg_gdb.py @@ -306,6 +306,23 @@ def disassemble(self, target_id=0, address=None, count=16): return output + @post_event + def source_location(self, target_id=0, address=None): + """ + Get the source file name and line number of the given address. + This requires symbols to be loaded for the target. + + `address` is the code address to resolve filename and + line number from. If None, the current program counter is used + """ + if address == None: + pc_name, address = self._program_counter(target_id=target_id) + m = re.match('Line\\s+(\\d+)\\s+of\\s+"([^"]+)"', gdb.execute('info line *0x{:x}'.format(address), to_string=True)) + if m != None: + file, line = m.group(2,1) + return (file, int(line)) + return None + @validate_busy @validate_target @post_event @@ -544,7 +561,11 @@ def get_registers_x86_64(self): return vals def get_register_x86_64(self, reg): - return int(gdb.parse_and_eval('(long long)$'+reg)) & 0xFFFFFFFFFFFFFFFF + # Encountered some errors debugging Rust code with symbols + # Output was 0xdeadbeef <_some_func> + # With this hack, everything works, and nothing breaks + val = re.match('(^(?:0x[0-9a-fA-F]+)|(?:-?[0-9]+))', str(gdb.parse_and_eval('$'+reg))).group(1) + return int(val, 0) & 0xFFFFFFFFFFFFFFFF def get_registers_x86(self): # Get regular registers diff --git a/voltron/plugins/debugger/dbg_lldb.py b/voltron/plugins/debugger/dbg_lldb.py index 4dd8ec8e..364e7107 100644 --- a/voltron/plugins/debugger/dbg_lldb.py +++ b/voltron/plugins/debugger/dbg_lldb.py @@ -330,6 +330,24 @@ def disassemble(self, target_id=0, address=None, count=None): return output + @validate_busy + @validate_target + @lock_host + def source_location(self, target_id=0, address=None): + if target_id is None: + target_id=0 + if address == None: + pc_name, address = self.program_counter(target_id=target_id) + t = self.host.GetTargetAtIndex(target_id or 0) + sbaddr = lldb.SBAddress(address, t) + ctx = t.ResolveSymbolContextForAddress(sbaddr, lldb.eSymbolContextEverything) + if ctx.IsValid() and ctx.GetSymbol().IsValid(): + line_entry = ctx.GetLineEntry() + if line_entry.IsValid(): + file_spec = line_entry.GetFileSpec() + return (file_spec.__get_fullpath__(), line_entry.GetLine()) + return None + @validate_busy @validate_target @lock_host diff --git a/voltron/plugins/debugger/dbg_vdb.py b/voltron/plugins/debugger/dbg_vdb.py index 1567954a..5732daec 100644 --- a/voltron/plugins/debugger/dbg_vdb.py +++ b/voltron/plugins/debugger/dbg_vdb.py @@ -245,6 +245,12 @@ def _get_n_opcodes_length(self, address, count): length += op.size return length + @validate_busy + @validate_target + @lock_host + def source_location(self, target_id=0, address=None): + return None + @validate_busy @validate_target @lock_host diff --git a/voltron/plugins/debugger/dbg_windbg.py b/voltron/plugins/debugger/dbg_windbg.py index fa91d24e..c795bc0f 100644 --- a/voltron/plugins/debugger/dbg_windbg.py +++ b/voltron/plugins/debugger/dbg_windbg.py @@ -259,6 +259,18 @@ def disassemble(self, target_id=0, address=None, count=16): return output + @validate_busy + @validate_target + @lock_host + def source_location(self, target_id=0, address=None): + if address is None: + pc_name, address = self.program_counter(target_id=target_id) + try: + file, line, displacement = pykd.getSourceLine(address) + return (file, line) + except: + return None + @validate_busy @validate_target @lock_host