| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,163 @@ | ||
| # DExTer : Debugging Experience Tester | ||
| # ~~~~~~ ~ ~~ ~ ~~ | ||
| # | ||
| # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | ||
| # See https://llvm.org/LICENSE.txt for license information. | ||
| # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | ||
|
|
||
| import sys | ||
| import os | ||
| import platform | ||
|
|
||
| from dex.debugger.DebuggerBase import DebuggerBase | ||
| from dex.dextIR import FrameIR, LocIR, StepIR, StopReason, ValueIR | ||
| from dex.dextIR import ProgramState, StackFrame, SourceLocation | ||
| from dex.utils.Exceptions import DebuggerException, LoadDebuggerException | ||
| from dex.utils.ReturnCode import ReturnCode | ||
|
|
||
| if platform.system() == "Windows": | ||
| # Don't load on linux; _load_interface will croak before any names are used. | ||
| from . import setup | ||
| from . import probe_process | ||
| from . import breakpoint | ||
|
|
||
| class DbgEng(DebuggerBase): | ||
| def __init__(self, context, *args): | ||
| self.breakpoints = [] | ||
| self.running = False | ||
| self.finished = False | ||
| self.step_info = None | ||
| super(DbgEng, self).__init__(context, *args) | ||
|
|
||
| def _custom_init(self): | ||
| try: | ||
| res = setup.setup_everything(self.context.options.executable) | ||
| self.client, self.hProcess = res | ||
| self.running = True | ||
| except Exception as e: | ||
| raise Exception('Failed to start debuggee: {}'.format(e)) | ||
|
|
||
| def _custom_exit(self): | ||
| setup.cleanup(self.client, self.hProcess) | ||
|
|
||
| def _load_interface(self): | ||
| arch = platform.architecture()[0] | ||
| machine = platform.machine() | ||
| if arch == '32bit' and machine == 'AMD64': | ||
| # This python process is 32 bits, but is sitting on a 64 bit machine. | ||
| # Bad things may happen, don't support it. | ||
| raise LoadDebuggerException('Can\'t run Dexter dbgeng on 32 bit python in a 64 bit environment') | ||
|
|
||
| if platform.system() != 'Windows': | ||
| raise LoadDebuggerException('DbgEng supports Windows only') | ||
|
|
||
| # Otherwise, everything was imported earlier | ||
|
|
||
| @classmethod | ||
| def get_name(cls): | ||
| return 'dbgeng' | ||
|
|
||
| @classmethod | ||
| def get_option_name(cls): | ||
| return 'dbgeng' | ||
|
|
||
| @property | ||
| def frames_below_main(self): | ||
| return [] | ||
|
|
||
| @property | ||
| def version(self): | ||
| # I don't believe there's a well defined DbgEng version, outside of the | ||
| # version of Windows being used. | ||
| return "1" | ||
|
|
||
| def clear_breakpoints(self): | ||
| for x in self.breakpoints: | ||
| x.RemoveFlags(breakpoint.BreakpointFlags.DEBUG_BREAKPOINT_ENABLED) | ||
| self.client.Control.RemoveBreakpoint(x) | ||
|
|
||
| def add_breakpoint(self, file_, line): | ||
| # This is something to implement in the future -- as it stands, Dexter | ||
| # doesn't test for such things as "I can set a breakpoint on this line". | ||
| # This is only called AFAICT right now to ensure we break on every step. | ||
| pass | ||
|
|
||
| def launch(self): | ||
| # We are, by this point, already launched. | ||
| self.step_info = probe_process.probe_state(self.client) | ||
|
|
||
| def step(self): | ||
| res = setup.step_once(self.client) | ||
| if not res: | ||
| self.finished = True | ||
| self.step_info = res | ||
|
|
||
| def go(self): | ||
| # We never go -- we always single step. | ||
| pass | ||
|
|
||
| def get_step_info(self): | ||
| frames = self.step_info | ||
| state_frames = [] | ||
|
|
||
| # For now assume the base function is the... function, ignoring | ||
| # inlining. | ||
| dex_frames = [] | ||
| for i, x in enumerate(frames): | ||
| # XXX Might be able to get columns out through | ||
| # GetSourceEntriesByOffset, not a priority now | ||
| loc = LocIR(path=x.source_file, lineno=x.line_no, column=0) | ||
| new_frame = FrameIR(function=x.function_name, is_inlined=False, loc=loc) | ||
| dex_frames.append(new_frame) | ||
|
|
||
| state_frame = StackFrame(function=new_frame.function, | ||
| is_inlined=new_frame.is_inlined, | ||
| location=SourceLocation(path=x.source_file, | ||
| lineno=x.line_no, | ||
| column=0), | ||
| watches={}) | ||
| for expr in map( | ||
| lambda watch, idx=i: self.evaluate_expression(watch, idx), | ||
| self.watches): | ||
| state_frame.watches[expr.expression] = expr | ||
| state_frames.append(state_frame) | ||
|
|
||
| return StepIR( | ||
| step_index=self.step_index, frames=dex_frames, | ||
| stop_reason=StopReason.STEP, | ||
| program_state=ProgramState(state_frames)) | ||
|
|
||
| @property | ||
| def is_running(self): | ||
| return False # We're never free-running | ||
|
|
||
| @property | ||
| def is_finished(self): | ||
| return self.finished | ||
|
|
||
| def evaluate_expression(self, expression, frame_idx=0): | ||
| # XXX: cdb insists on using '->' to examine fields of structures, | ||
| # as it appears to reserve '.' for other purposes. | ||
| fixed_expr = expression.replace('.', '->') | ||
|
|
||
| orig_scope_idx = self.client.Symbols.GetCurrentScopeFrameIndex() | ||
| self.client.Symbols.SetScopeFrameByIndex(frame_idx) | ||
|
|
||
| res = self.client.Control.Evaluate(fixed_expr) | ||
| if res is not None: | ||
| result, typename = self.client.Control.Evaluate(fixed_expr) | ||
| could_eval = True | ||
| else: | ||
| result, typename = (None, None) | ||
| could_eval = False | ||
|
|
||
| self.client.Symbols.SetScopeFrameByIndex(orig_scope_idx) | ||
|
|
||
| return ValueIR( | ||
| expression=expression, | ||
| value=str(result), | ||
| type_name=typename, | ||
| error_string="", | ||
| could_evaluate=could_eval, | ||
| is_optimized_away=False, | ||
| is_irretrievable=not could_eval) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| # DExTer : Debugging Experience Tester | ||
| # ~~~~~~ ~ ~~ ~ ~~ | ||
| # | ||
| # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | ||
| # See https://llvm.org/LICENSE.txt for license information. | ||
| # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | ||
|
|
||
| import os | ||
|
|
||
| from .utils import * | ||
|
|
||
| class Frame(object): | ||
| def __init__(self, frame, idx, Symbols): | ||
| # Store some base information about the frame | ||
| self.ip = frame.InstructionOffset | ||
| self.scope_idx = idx | ||
| self.virtual = frame.Virtual | ||
| self.inline_frame_context = frame.InlineFrameContext | ||
| self.func_tbl_entry = frame.FuncTableEntry | ||
|
|
||
| # Fetch the module/symbol we're in, with displacement. Useful for debugging. | ||
| self.descr = Symbols.GetNearNameByOffset(self.ip) | ||
| split = self.descr.split('!')[0] | ||
| self.module = split[0] | ||
| self.symbol = split[1] | ||
|
|
||
| # Fetch symbol group for this scope. | ||
| prevscope = Symbols.GetCurrentScopeFrameIndex() | ||
| if Symbols.SetScopeFrameByIndex(idx): | ||
| symgroup = Symbols.GetScopeSymbolGroup2() | ||
| Symbols.SetScopeFrameByIndex(prevscope) | ||
| self.symgroup = symgroup | ||
| else: | ||
| self.symgroup = None | ||
|
|
||
| # Fetch the name according to the line-table, using inlining context. | ||
| name = Symbols.GetNameByInlineContext(self.ip, self.inline_frame_context) | ||
| self.function_name = name.split('!')[-1] | ||
|
|
||
| try: | ||
| tup = Symbols.GetLineByInlineContext(self.ip, self.inline_frame_context) | ||
| self.source_file, self.line_no = tup | ||
| except WinError as e: | ||
| # Fall back to trying to use a non-inlining-aware line number | ||
| # XXX - this is not inlining aware | ||
| sym = Symbols.GetLineByOffset(self.ip) | ||
| if sym is not None: | ||
| self.source_file, self.line_no = sym | ||
| else: | ||
| self.source_file = None | ||
| self.line_no = None | ||
| self.basename = None | ||
|
|
||
| if self.source_file is not None: | ||
| self.basename = os.path.basename(self.source_file) | ||
| else: | ||
| self.basename = None | ||
|
|
||
|
|
||
|
|
||
| def __str__(self): | ||
| return '{}:{}({}) {}'.format(self.basename, self.line, self.descr, self.function_name) | ||
|
|
||
| def main_on_stack(Symbols, frames): | ||
| module_name = Symbols.get_exefile_module_name() | ||
| main_name = "{}!main".format(module_name) | ||
| for x in frames: | ||
| if main_name in x.descr: # Could be less hard coded... | ||
| return True | ||
| return False | ||
|
|
||
| def probe_state(Client): | ||
| # Fetch the state of the program -- represented by the stack frames. | ||
| frames, numframes = Client.Control.GetStackTraceEx() | ||
|
|
||
| the_frames = [Frame(frames[x], x, Client.Symbols) for x in range(numframes)] | ||
| if not main_on_stack(Client.Symbols, the_frames): | ||
| return None | ||
|
|
||
| return the_frames |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,185 @@ | ||
| # DExTer : Debugging Experience Tester | ||
| # ~~~~~~ ~ ~~ ~ ~~ | ||
| # | ||
| # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | ||
| # See https://llvm.org/LICENSE.txt for license information. | ||
| # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | ||
|
|
||
| from ctypes import * | ||
|
|
||
| from . import client | ||
| from . import control | ||
| from . import symbols | ||
| from .probe_process import probe_state | ||
| from .utils import * | ||
|
|
||
| class STARTUPINFOA(Structure): | ||
| _fields_ = [ | ||
| ('cb', c_ulong), | ||
| ('lpReserved', c_char_p), | ||
| ('lpDesktop', c_char_p), | ||
| ('lpTitle', c_char_p), | ||
| ('dwX', c_ulong), | ||
| ('dwY', c_ulong), | ||
| ('dwXSize', c_ulong), | ||
| ('dwYSize', c_ulong), | ||
| ('dwXCountChars', c_ulong), | ||
| ('dwYCountChars', c_ulong), | ||
| ('dwFillAttribute', c_ulong), | ||
| ('wShowWindow', c_ushort), | ||
| ('cbReserved2', c_ushort), | ||
| ('lpReserved2', c_char_p), | ||
| ('hStdInput', c_void_p), | ||
| ('hStdOutput', c_void_p), | ||
| ('hStdError', c_void_p) | ||
| ] | ||
|
|
||
| class PROCESS_INFORMATION(Structure): | ||
| _fields_ = [ | ||
| ('hProcess', c_void_p), | ||
| ('hThread', c_void_p), | ||
| ('dwProcessId', c_ulong), | ||
| ('dwThreadId', c_ulong) | ||
| ] | ||
|
|
||
| def fetch_local_function_syms(Symbols, prefix): | ||
| syms = Symbols.get_all_functions() | ||
|
|
||
| def is_sym_in_src_dir(sym): | ||
| name, data = sym | ||
| symdata = Symbols.GetLineByOffset(data.Offset) | ||
| if symdata is not None: | ||
| srcfile, line = symdata | ||
| if prefix in srcfile: | ||
| return True | ||
| return False | ||
|
|
||
| syms = [x for x in syms if is_sym_in_src_dir(x)] | ||
| return syms | ||
|
|
||
| def break_on_all_but_main(Control, Symbols, main_offset): | ||
| mainfile, _ = Symbols.GetLineByOffset(main_offset) | ||
| prefix = '\\'.join(mainfile.split('\\')[:-1]) | ||
|
|
||
| for name, rec in fetch_local_function_syms(Symbols, prefix): | ||
| if name == "main": | ||
| continue | ||
| bp = Control.AddBreakpoint2(offset=rec.Offset, enabled=True) | ||
|
|
||
| # All breakpoints are currently discarded: we just sys.exit for cleanup | ||
| return | ||
|
|
||
| def process_creator(binfile): | ||
| Kernel32 = WinDLL("Kernel32") | ||
|
|
||
| # Another flavour of process creation | ||
| startupinfoa = STARTUPINFOA() | ||
| startupinfoa.cb = sizeof(STARTUPINFOA) | ||
| startupinfoa.lpReserved = None | ||
| startupinfoa.lpDesktop = None | ||
| startupinfoa.lpTitle = None | ||
| startupinfoa.dwX = 0 | ||
| startupinfoa.dwY = 0 | ||
| startupinfoa.dwXSize = 0 | ||
| startupinfoa.dwYSize = 0 | ||
| startupinfoa.dwXCountChars = 0 | ||
| startupinfoa.dwYCountChars = 0 | ||
| startupinfoa.dwFillAttribute = 0 | ||
| startupinfoa.dwFlags = 0 | ||
| startupinfoa.wShowWindow = 0 | ||
| startupinfoa.cbReserved2 = 0 | ||
| startupinfoa.lpReserved2 = None | ||
| startupinfoa.hStdInput = None | ||
| startupinfoa.hStdOutput = None | ||
| startupinfoa.hStdError = None | ||
| processinformation = PROCESS_INFORMATION() | ||
|
|
||
| # 0x4 below specifies CREATE_SUSPENDED. | ||
| ret = Kernel32.CreateProcessA(binfile.encode("ascii"), None, None, None, False, 0x4, None, None, byref(startupinfoa), byref(processinformation)) | ||
| if ret == 0: | ||
| raise Exception('CreateProcess running {}'.format(binfile)) | ||
|
|
||
| return processinformation.dwProcessId, processinformation.dwThreadId, processinformation.hProcess, processinformation.hThread | ||
|
|
||
| def thread_resumer(hProcess, hThread): | ||
| Kernel32 = WinDLL("Kernel32") | ||
|
|
||
| # For reasons unclear to me, other suspend-references seem to be opened on | ||
| # the opened thread. Clear them all. | ||
| while True: | ||
| ret = Kernel32.ResumeThread(hThread) | ||
| if ret <= 0: | ||
| break | ||
| if ret < 0: | ||
| Kernel32.TerminateProcess(hProcess, 1) | ||
| raise Exception("Couldn't resume process after startup") | ||
|
|
||
| return | ||
|
|
||
| def setup_everything(binfile): | ||
| from . import client | ||
| from . import symbols | ||
| Client = client.Client() | ||
|
|
||
| created_pid, created_tid, hProcess, hThread = process_creator(binfile) | ||
|
|
||
| # Load lines as well as general symbols | ||
| sym_opts = Client.Symbols.GetSymbolOptions() | ||
| sym_opts |= symbols.SymbolOptionFlags.SYMOPT_LOAD_LINES | ||
| Client.Symbols.SetSymbolOptions(sym_opts) | ||
|
|
||
| Client.AttachProcess(created_pid) | ||
|
|
||
| # Need to enter the debugger engine to let it attach properly | ||
| Client.Control.WaitForEvent(timeout=1) | ||
| Client.SysObjects.set_current_thread(created_pid, created_tid) | ||
| Client.Control.Execute("l+t") | ||
| Client.Control.SetExpressionSyntax(cpp=True) | ||
|
|
||
| module_name = Client.Symbols.get_exefile_module_name() | ||
| offset = Client.Symbols.GetOffsetByName("{}!main".format(module_name)) | ||
| breakpoint = Client.Control.AddBreakpoint2(offset=offset, enabled=True) | ||
| thread_resumer(hProcess, hThread) | ||
| Client.Control.SetExecutionStatus(control.DebugStatus.DEBUG_STATUS_GO) | ||
|
|
||
| # Problem: there is no guarantee that the client will ever reach main, | ||
| # something else exciting could happen in that time, the host system may | ||
| # be very loaded, and similar. Wait for some period, say, five seconds, and | ||
| # abort afterwards: this is a trade-off between spurious timeouts and | ||
| # completely hanging in the case of a environmental/programming error. | ||
| res = Client.Control.WaitForEvent(timeout=5000) | ||
| if res == S_FALSE: | ||
| Kernel32.TerminateProcess(hProcess, 1) | ||
| raise Exception("Debuggee did not reach main function in a timely manner") | ||
|
|
||
| break_on_all_but_main(Client.Control, Client.Symbols, offset) | ||
|
|
||
| # Set the default action on all exceptions to be "quit and detach". If we | ||
| # don't, dbgeng will merrily spin at the exception site forever. | ||
| filts = Client.Control.GetNumberEventFilters() | ||
| for x in range(filts[0], filts[0] + filts[1]): | ||
| Client.Control.SetExceptionFilterSecondCommand(x, "qd") | ||
|
|
||
| return Client, hProcess | ||
|
|
||
| def step_once(client): | ||
| client.Control.Execute("p") | ||
| try: | ||
| client.Control.WaitForEvent() | ||
| except Exception as e: | ||
| if client.Control.GetExecutionStatus() == control.DebugStatus.DEBUG_STATUS_NO_DEBUGGEE: | ||
| return None # Debuggee has gone away, likely due to an exception. | ||
| raise e | ||
| # Could assert here that we're in the "break" state | ||
| client.Control.GetExecutionStatus() | ||
| return probe_state(client) | ||
|
|
||
| def main_loop(client): | ||
| res = True | ||
| while res is not None: | ||
| res = step_once(client) | ||
|
|
||
| def cleanup(client, hProcess): | ||
| res = client.DetachProcesses() | ||
| Kernel32 = WinDLL("Kernel32") | ||
| Kernel32.TerminateProcess(hProcess, 1) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| # DExTer : Debugging Experience Tester | ||
| # ~~~~~~ ~ ~~ ~ ~~ | ||
| # | ||
| # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | ||
| # See https://llvm.org/LICENSE.txt for license information. | ||
| # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | ||
|
|
||
| from collections import namedtuple | ||
| from ctypes import * | ||
| from functools import partial | ||
|
|
||
| from .utils import * | ||
|
|
||
| Symbol = namedtuple("Symbol", ["num", "name", "type", "value"]) | ||
|
|
||
| class IDebugSymbolGroup2(Structure): | ||
| pass | ||
|
|
||
| class IDebugSymbolGroup2Vtbl(Structure): | ||
| wrp = partial(WINFUNCTYPE, c_long, POINTER(IDebugSymbolGroup2)) | ||
| ids_getnumbersymbols = wrp(c_ulong_p) | ||
| ids_getsymbolname = wrp(c_ulong, c_char_p, c_ulong, c_ulong_p) | ||
| ids_getsymboltypename = wrp(c_ulong, c_char_p, c_ulong, c_ulong_p) | ||
| ids_getsymbolvaluetext = wrp(c_ulong, c_char_p, c_ulong, c_ulong_p) | ||
| _fields_ = [ | ||
| ("QueryInterface", c_void_p), | ||
| ("AddRef", c_void_p), | ||
| ("Release", c_void_p), | ||
| ("GetNumberSymbols", ids_getnumbersymbols), | ||
| ("AddSymbol", c_void_p), | ||
| ("RemoveSymbolByName", c_void_p), | ||
| ("RemoveSymbolByIndex", c_void_p), | ||
| ("GetSymbolName", ids_getsymbolname), | ||
| ("GetSymbolParameters", c_void_p), | ||
| ("ExpandSymbol", c_void_p), | ||
| ("OutputSymbols", c_void_p), | ||
| ("WriteSymbol", c_void_p), | ||
| ("OutputAsType", c_void_p), | ||
| ("AddSymbolWide", c_void_p), | ||
| ("RemoveSymbolByNameWide", c_void_p), | ||
| ("GetSymbolNameWide", c_void_p), | ||
| ("WritesymbolWide", c_void_p), | ||
| ("OutputAsTypeWide", c_void_p), | ||
| ("GetSymbolTypeName", ids_getsymboltypename), | ||
| ("GetSymbolTypeNameWide", c_void_p), | ||
| ("GetSymbolSize", c_void_p), | ||
| ("GetSymbolOffset", c_void_p), | ||
| ("GetSymbolRegister", c_void_p), | ||
| ("GetSymbolValueText", ids_getsymbolvaluetext), | ||
| ("GetSymbolValueTextWide", c_void_p), | ||
| ("GetSymbolEntryInformation", c_void_p) | ||
| ] | ||
|
|
||
| IDebugSymbolGroup2._fields_ = [("lpVtbl", POINTER(IDebugSymbolGroup2Vtbl))] | ||
|
|
||
| class SymbolGroup(object): | ||
| def __init__(self, symgroup): | ||
| self.symgroup = symgroup.contents | ||
| self.vt = self.symgroup.lpVtbl.contents | ||
| self.ulong = c_ulong() | ||
|
|
||
| def GetNumberSymbols(self): | ||
| res = self.vt.GetNumberSymbols(self.symgroup, byref(self.ulong)) | ||
| aborter(res, "GetNumberSymbols") | ||
| return self.ulong.value | ||
|
|
||
| def GetSymbolName(self, idx): | ||
| buf = create_string_buffer(256) | ||
| res = self.vt.GetSymbolName(self.symgroup, idx, buf, 255, byref(self.ulong)) | ||
| aborter(res, "GetSymbolName") | ||
| thelen = self.ulong.value | ||
| return string_at(buf).decode("ascii") | ||
|
|
||
| def GetSymbolTypeName(self, idx): | ||
| buf = create_string_buffer(256) | ||
| res = self.vt.GetSymbolTypeName(self.symgroup, idx, buf, 255, byref(self.ulong)) | ||
| aborter(res, "GetSymbolTypeName") | ||
| thelen = self.ulong.value | ||
| return string_at(buf).decode("ascii") | ||
|
|
||
| def GetSymbolValueText(self, idx, handleserror=False): | ||
| buf = create_string_buffer(256) | ||
| res = self.vt.GetSymbolValueText(self.symgroup, idx, buf, 255, byref(self.ulong)) | ||
| if res != 0 and handleserror: | ||
| return None | ||
| aborter(res, "GetSymbolTypeName") | ||
| thelen = self.ulong.value | ||
| return string_at(buf).decode("ascii") | ||
|
|
||
| def get_symbol(self, idx): | ||
| name = self.GetSymbolName(idx) | ||
| thetype = self.GetSymbolTypeName(idx) | ||
| value = self.GetSymbolValueText(idx) | ||
| return Symbol(idx, name, thetype, value) | ||
|
|
||
| def get_all_symbols(self): | ||
| num_syms = self.GetNumberSymbols() | ||
| return list(map(self.get_symbol, list(range(num_syms)))) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,200 @@ | ||
| # DExTer : Debugging Experience Tester | ||
| # ~~~~~~ ~ ~~ ~ ~~ | ||
| # | ||
| # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | ||
| # See https://llvm.org/LICENSE.txt for license information. | ||
| # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | ||
|
|
||
| from ctypes import * | ||
| from functools import partial | ||
|
|
||
| from .utils import * | ||
|
|
||
| # UUID For SystemObjects4 interface. | ||
| DebugSystemObjects4IID = IID(0x489468e6, 0x7d0f, 0x4af5, IID_Data4_Type(0x87, 0xab, 0x25, 0x20, 0x74, 0x54, 0xd5, 0x53)) | ||
|
|
||
| class IDebugSystemObjects4(Structure): | ||
| pass | ||
|
|
||
| class IDebugSystemObjects4Vtbl(Structure): | ||
| wrp = partial(WINFUNCTYPE, c_long, POINTER(IDebugSystemObjects4)) | ||
| ids_getnumberprocesses = wrp(POINTER(c_ulong)) | ||
| ids_getprocessidsbyindex = wrp(c_ulong, c_ulong, c_ulong_p, c_ulong_p) | ||
| ids_setcurrentprocessid = wrp(c_ulong) | ||
| ids_getnumberthreads = wrp(c_ulong_p) | ||
| ids_getthreadidsbyindex = wrp(c_ulong, c_ulong, c_ulong_p, c_ulong_p) | ||
| ids_setcurrentthreadid = wrp(c_ulong) | ||
| _fields_ = [ | ||
| ("QueryInterface", c_void_p), | ||
| ("AddRef", c_void_p), | ||
| ("Release", c_void_p), | ||
| ("GetEventThread", c_void_p), | ||
| ("GetEventProcess", c_void_p), | ||
| ("GetCurrentThreadId", c_void_p), | ||
| ("SetCurrentThreadId", ids_setcurrentthreadid), | ||
| ("GetCurrentProcessId", c_void_p), | ||
| ("SetCurrentProcessId", ids_setcurrentprocessid), | ||
| ("GetNumberThreads", ids_getnumberthreads), | ||
| ("GetTotalNumberThreads", c_void_p), | ||
| ("GetThreadIdsByIndex", ids_getthreadidsbyindex), | ||
| ("GetThreadIdByProcessor", c_void_p), | ||
| ("GetCurrentThreadDataOffset", c_void_p), | ||
| ("GetThreadIdByDataOffset", c_void_p), | ||
| ("GetCurrentThreadTeb", c_void_p), | ||
| ("GetThreadIdByTeb", c_void_p), | ||
| ("GetCurrentThreadSystemId", c_void_p), | ||
| ("GetThreadIdBySystemId", c_void_p), | ||
| ("GetCurrentThreadHandle", c_void_p), | ||
| ("GetThreadIdByHandle", c_void_p), | ||
| ("GetNumberProcesses", ids_getnumberprocesses), | ||
| ("GetProcessIdsByIndex", ids_getprocessidsbyindex), | ||
| ("GetCurrentProcessDataOffset", c_void_p), | ||
| ("GetProcessIdByDataOffset", c_void_p), | ||
| ("GetCurrentProcessPeb", c_void_p), | ||
| ("GetProcessIdByPeb", c_void_p), | ||
| ("GetCurrentProcessSystemId", c_void_p), | ||
| ("GetProcessIdBySystemId", c_void_p), | ||
| ("GetCurrentProcessHandle", c_void_p), | ||
| ("GetProcessIdByHandle", c_void_p), | ||
| ("GetCurrentProcessExecutableName", c_void_p), | ||
| ("GetCurrentProcessUpTime", c_void_p), | ||
| ("GetImplicitThreadDataOffset", c_void_p), | ||
| ("SetImplicitThreadDataOffset", c_void_p), | ||
| ("GetImplicitProcessDataOffset", c_void_p), | ||
| ("SetImplicitProcessDataOffset", c_void_p), | ||
| ("GetEventSystem", c_void_p), | ||
| ("GetCurrentSystemId", c_void_p), | ||
| ("SetCurrentSystemId", c_void_p), | ||
| ("GetNumberSystems", c_void_p), | ||
| ("GetSystemIdsByIndex", c_void_p), | ||
| ("GetTotalNumberThreadsAndProcesses", c_void_p), | ||
| ("GetCurrentSystemServer", c_void_p), | ||
| ("GetSystemByServer", c_void_p), | ||
| ("GetCurrentSystemServerName", c_void_p), | ||
| ("GetCurrentProcessExecutableNameWide", c_void_p), | ||
| ("GetCurrentSystemServerNameWide", c_void_p) | ||
| ] | ||
|
|
||
| IDebugSystemObjects4._fields_ = [("lpVtbl", POINTER(IDebugSystemObjects4Vtbl))] | ||
|
|
||
| class SysObjects(object): | ||
| def __init__(self, sysobjects): | ||
| self.ptr = sysobjects | ||
| self.sysobjects = sysobjects.contents | ||
| self.vt = self.sysobjects.lpVtbl.contents | ||
| # Keep a handy ulong for passing into C methods. | ||
| self.ulong = c_ulong() | ||
|
|
||
| def GetNumberSystems(self): | ||
| res = self.vt.GetNumberSystems(self.sysobjects, byref(self.ulong)) | ||
| aborter(res, "GetNumberSystems") | ||
| return self.ulong.value | ||
|
|
||
| def GetNumberProcesses(self): | ||
| res = self.vt.GetNumberProcesses(self.sysobjects, byref(self.ulong)) | ||
| aborter(res, "GetNumberProcesses") | ||
| return self.ulong.value | ||
|
|
||
| def GetNumberThreads(self): | ||
| res = self.vt.GetNumberThreads(self.sysobjects, byref(self.ulong)) | ||
| aborter(res, "GetNumberThreads") | ||
| return self.ulong.value | ||
|
|
||
| def GetTotalNumberThreadsAndProcesses(self): | ||
| tthreads = c_ulong() | ||
| tprocs = c_ulong() | ||
| pulong3 = c_ulong() | ||
| res = self.vt.GetTotalNumberThreadsAndProcesses(self.sysobjects, byref(tthreads), byref(tprocs), byref(pulong3), byref(pulong3), byref(pulong3)) | ||
| aborter(res, "GettotalNumberThreadsAndProcesses") | ||
| return tthreads.value, tprocs.value | ||
|
|
||
| def GetCurrentProcessId(self): | ||
| res = self.vt.GetCurrentProcessId(self.sysobjects, byref(self.ulong)) | ||
| aborter(res, "GetCurrentProcessId") | ||
| return self.ulong.value | ||
|
|
||
| def SetCurrentProcessId(self, sysid): | ||
| res = self.vt.SetCurrentProcessId(self.sysobjects, sysid) | ||
| aborter(res, "SetCurrentProcessId") | ||
| return | ||
|
|
||
| def GetCurrentThreadId(self): | ||
| res = self.vt.GetCurrentThreadId(self.sysobjects, byref(self.ulong)) | ||
| aborter(res, "GetCurrentThreadId") | ||
| return self.ulong.value | ||
|
|
||
| def SetCurrentThreadId(self, sysid): | ||
| res = self.vt.SetCurrentThreadId(self.sysobjects, sysid) | ||
| aborter(res, "SetCurrentThreadId") | ||
| return | ||
|
|
||
| def GetProcessIdsByIndex(self): | ||
| num_processes = self.GetNumberProcesses() | ||
| if num_processes == 0: | ||
| return [] | ||
| engineids = (c_ulong * num_processes)() | ||
| pids = (c_ulong * num_processes)() | ||
| for x in range(num_processes): | ||
| engineids[x] = DEBUG_ANY_ID | ||
| pids[x] = DEBUG_ANY_ID | ||
| res = self.vt.GetProcessIdsByIndex(self.sysobjects, 0, num_processes, engineids, pids) | ||
| aborter(res, "GetProcessIdsByIndex") | ||
| return list(zip(engineids, pids)) | ||
|
|
||
| def GetThreadIdsByIndex(self): | ||
| num_threads = self.GetNumberThreads() | ||
| if num_threads == 0: | ||
| return [] | ||
| engineids = (c_ulong * num_threads)() | ||
| tids = (c_ulong * num_threads)() | ||
| for x in range(num_threads): | ||
| engineids[x] = DEBUG_ANY_ID | ||
| tids[x] = DEBUG_ANY_ID | ||
| # Zero -> start index | ||
| res = self.vt.GetThreadIdsByIndex(self.sysobjects, 0, num_threads, engineids, tids) | ||
| aborter(res, "GetThreadIdsByIndex") | ||
| return list(zip(engineids, tids)) | ||
|
|
||
| def GetCurThreadHandle(self): | ||
| pulong64 = c_ulonglong() | ||
| res = self.vt.GetCurrentThreadHandle(self.sysobjects, byref(pulong64)) | ||
| aborter(res, "GetCurrentThreadHandle") | ||
| return pulong64.value | ||
|
|
||
| def set_current_thread(self, pid, tid): | ||
| proc_sys_id = -1 | ||
| for x in self.GetProcessIdsByIndex(): | ||
| sysid, procid = x | ||
| if procid == pid: | ||
| proc_sys_id = sysid | ||
|
|
||
| if proc_sys_id == -1: | ||
| raise Exception("Couldn't find designated PID {}".format(pid)) | ||
|
|
||
| self.SetCurrentProcessId(proc_sys_id) | ||
|
|
||
| thread_sys_id = -1 | ||
| for x in self.GetThreadIdsByIndex(): | ||
| sysid, threadid = x | ||
| if threadid == tid: | ||
| thread_sys_id = sysid | ||
|
|
||
| if thread_sys_id == -1: | ||
| raise Exception("Couldn't find designated TID {}".format(tid)) | ||
|
|
||
| self.SetCurrentThreadId(thread_sys_id) | ||
| return | ||
|
|
||
| def print_current_procs_threads(self): | ||
| procs = [] | ||
| for x in self.GetProcessIdsByIndex(): | ||
| sysid, procid = x | ||
| procs.append(procid) | ||
|
|
||
| threads = [] | ||
| for x in self.GetThreadIdsByIndex(): | ||
| sysid, threadid = x | ||
| threads.append(threadid) | ||
|
|
||
| print("Current processes: {}".format(procs)) | ||
| print("Current threads: {}".format(threads)) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| # DExTer : Debugging Experience Tester | ||
| # ~~~~~~ ~ ~~ ~ ~~ | ||
| # | ||
| # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | ||
| # See https://llvm.org/LICENSE.txt for license information. | ||
| # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | ||
|
|
||
| from ctypes import * | ||
|
|
||
| # Error codes are negative when received by python, but are typically | ||
| # represented by unsigned hex elsewhere. Subtract 2^32 from the unsigned | ||
| # hex to produce negative error codes. | ||
| E_NOINTERFACE = 0x80004002 - 0x100000000 | ||
| E_FAIL = 0x80004005 - 0x100000000 | ||
| E_EINVAL = 0x80070057 - 0x100000000 | ||
| E_INTERNALEXCEPTION = 0x80040205 - 0x100000000 | ||
| S_FALSE = 1 | ||
|
|
||
| # This doesn't fit into any convenient category | ||
| DEBUG_ANY_ID = 0xFFFFFFFF | ||
|
|
||
| class WinError(Exception): | ||
| def __init__(self, msg, hstatus): | ||
| self.hstatus = hstatus | ||
| super(WinError, self).__init__(msg) | ||
|
|
||
| def aborter(res, msg, ignore=[]): | ||
| if res != 0 and res not in ignore: | ||
| # Convert a negative error code to a positive unsigned one, which is | ||
| # now NTSTATUSes appear in documentation. | ||
| if res < 0: | ||
| res += 0x100000000 | ||
| msg = '{:08X} : {}'.format(res, msg) | ||
| raise WinError(msg, res) | ||
|
|
||
| IID_Data4_Type = c_ubyte * 8 | ||
|
|
||
| class IID(Structure): | ||
| _fields_ = [ | ||
| ("Data1", c_uint), | ||
| ("Data2", c_ushort), | ||
| ("Data3", c_ushort), | ||
| ("Data4", IID_Data4_Type) | ||
| ] | ||
|
|
||
| c_ulong_p = POINTER(c_ulong) | ||
| c_ulong64_p = POINTER(c_ulonglong) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,244 @@ | ||
| # DExTer : Debugging Experience Tester | ||
| # ~~~~~~ ~ ~~ ~ ~~ | ||
| # | ||
| # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | ||
| # See https://llvm.org/LICENSE.txt for license information. | ||
| # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | ||
| """Interface for communicating with the LLDB debugger via its python interface. | ||
| """ | ||
|
|
||
| import imp | ||
| import os | ||
| from subprocess import CalledProcessError, check_output, STDOUT | ||
| import sys | ||
|
|
||
| from dex.debugger.DebuggerBase import DebuggerBase | ||
| from dex.dextIR import FrameIR, LocIR, StepIR, StopReason, ValueIR | ||
| from dex.dextIR import StackFrame, SourceLocation, ProgramState | ||
| from dex.utils.Exceptions import DebuggerException, LoadDebuggerException | ||
| from dex.utils.ReturnCode import ReturnCode | ||
|
|
||
|
|
||
| class LLDB(DebuggerBase): | ||
| def __init__(self, context, *args): | ||
| self.lldb_executable = context.options.lldb_executable | ||
| self._debugger = None | ||
| self._target = None | ||
| self._process = None | ||
| self._thread = None | ||
| super(LLDB, self).__init__(context, *args) | ||
|
|
||
| def _custom_init(self): | ||
| self._debugger = self._interface.SBDebugger.Create() | ||
| self._debugger.SetAsync(False) | ||
| self._target = self._debugger.CreateTargetWithFileAndArch( | ||
| self.context.options.executable, self.context.options.arch) | ||
| if not self._target: | ||
| raise LoadDebuggerException( | ||
| 'could not create target for executable "{}" with arch:{}'. | ||
| format(self.context.options.executable, | ||
| self.context.options.arch)) | ||
|
|
||
| def _custom_exit(self): | ||
| if getattr(self, '_process', None): | ||
| self._process.Kill() | ||
| if getattr(self, '_debugger', None) and getattr(self, '_target', None): | ||
| self._debugger.DeleteTarget(self._target) | ||
|
|
||
| def _translate_stop_reason(self, reason): | ||
| if reason == self._interface.eStopReasonNone: | ||
| return None | ||
| if reason == self._interface.eStopReasonBreakpoint: | ||
| return StopReason.BREAKPOINT | ||
| if reason == self._interface.eStopReasonPlanComplete: | ||
| return StopReason.STEP | ||
| if reason == self._interface.eStopReasonThreadExiting: | ||
| return StopReason.PROGRAM_EXIT | ||
| if reason == self._interface.eStopReasonException: | ||
| return StopReason.ERROR | ||
| return StopReason.OTHER | ||
|
|
||
| def _load_interface(self): | ||
| try: | ||
| args = [self.lldb_executable, '-P'] | ||
| pythonpath = check_output( | ||
| args, stderr=STDOUT).rstrip().decode('utf-8') | ||
| except CalledProcessError as e: | ||
| raise LoadDebuggerException(str(e), sys.exc_info()) | ||
| except OSError as e: | ||
| raise LoadDebuggerException( | ||
| '{} ["{}"]'.format(e.strerror, self.lldb_executable), | ||
| sys.exc_info()) | ||
|
|
||
| if not os.path.isdir(pythonpath): | ||
| raise LoadDebuggerException( | ||
| 'path "{}" does not exist [result of {}]'.format( | ||
| pythonpath, args), sys.exc_info()) | ||
|
|
||
| try: | ||
| module_info = imp.find_module('lldb', [pythonpath]) | ||
| return imp.load_module('lldb', *module_info) | ||
| except ImportError as e: | ||
| msg = str(e) | ||
| if msg.endswith('not a valid Win32 application.'): | ||
| msg = '{} [Are you mixing 32-bit and 64-bit binaries?]'.format( | ||
| msg) | ||
| raise LoadDebuggerException(msg, sys.exc_info()) | ||
|
|
||
| @classmethod | ||
| def get_name(cls): | ||
| return 'lldb' | ||
|
|
||
| @classmethod | ||
| def get_option_name(cls): | ||
| return 'lldb' | ||
|
|
||
| @property | ||
| def version(self): | ||
| try: | ||
| return self._interface.SBDebugger_GetVersionString() | ||
| except AttributeError: | ||
| return None | ||
|
|
||
| def clear_breakpoints(self): | ||
| self._target.DeleteAllBreakpoints() | ||
|
|
||
| def add_breakpoint(self, file_, line): | ||
| if not self._target.BreakpointCreateByLocation(file_, line): | ||
| raise LoadDebuggerException( | ||
| 'could not add breakpoint [{}:{}]'.format(file_, line)) | ||
|
|
||
| def launch(self): | ||
| self._process = self._target.LaunchSimple(None, None, os.getcwd()) | ||
| if not self._process or self._process.GetNumThreads() == 0: | ||
| raise DebuggerException('could not launch process') | ||
| if self._process.GetNumThreads() != 1: | ||
| raise DebuggerException('multiple threads not supported') | ||
| self._thread = self._process.GetThreadAtIndex(0) | ||
| assert self._thread, (self._process, self._thread) | ||
|
|
||
| def step(self): | ||
| self._thread.StepInto() | ||
|
|
||
| def go(self) -> ReturnCode: | ||
| self._process.Continue() | ||
| return ReturnCode.OK | ||
|
|
||
| def get_step_info(self): | ||
| frames = [] | ||
| state_frames = [] | ||
|
|
||
| for i in range(0, self._thread.GetNumFrames()): | ||
| sb_frame = self._thread.GetFrameAtIndex(i) | ||
| sb_line = sb_frame.GetLineEntry() | ||
| sb_filespec = sb_line.GetFileSpec() | ||
|
|
||
| try: | ||
| path = os.path.join(sb_filespec.GetDirectory(), | ||
| sb_filespec.GetFilename()) | ||
| except (AttributeError, TypeError): | ||
| path = None | ||
|
|
||
| function = self._sanitize_function_name(sb_frame.GetFunctionName()) | ||
|
|
||
| loc_dict = { | ||
| 'path': path, | ||
| 'lineno': sb_line.GetLine(), | ||
| 'column': sb_line.GetColumn() | ||
| } | ||
| loc = LocIR(**loc_dict) | ||
|
|
||
| frame = FrameIR( | ||
| function=function, is_inlined=sb_frame.IsInlined(), loc=loc) | ||
|
|
||
| if any( | ||
| name in (frame.function or '') # pylint: disable=no-member | ||
| for name in self.frames_below_main): | ||
| break | ||
|
|
||
| frames.append(frame) | ||
|
|
||
| state_frame = StackFrame(function=frame.function, | ||
| is_inlined=frame.is_inlined, | ||
| location=SourceLocation(**loc_dict), | ||
| watches={}) | ||
| for expr in map( | ||
| lambda watch, idx=i: self.evaluate_expression(watch, idx), | ||
| self.watches): | ||
| state_frame.watches[expr.expression] = expr | ||
| state_frames.append(state_frame) | ||
|
|
||
| if len(frames) == 1 and frames[0].function is None: | ||
| frames = [] | ||
| state_frames = [] | ||
|
|
||
| reason = self._translate_stop_reason(self._thread.GetStopReason()) | ||
|
|
||
| return StepIR( | ||
| step_index=self.step_index, frames=frames, stop_reason=reason, | ||
| program_state=ProgramState(state_frames)) | ||
|
|
||
| @property | ||
| def is_running(self): | ||
| # We're not running in async mode so this is always False. | ||
| return False | ||
|
|
||
| @property | ||
| def is_finished(self): | ||
| return not self._thread.GetFrameAtIndex(0) | ||
|
|
||
| @property | ||
| def frames_below_main(self): | ||
| return ['__scrt_common_main_seh', '__libc_start_main'] | ||
|
|
||
| def evaluate_expression(self, expression, frame_idx=0) -> ValueIR: | ||
| result = self._thread.GetFrameAtIndex(frame_idx | ||
| ).EvaluateExpression(expression) | ||
| error_string = str(result.error) | ||
|
|
||
| value = result.value | ||
| could_evaluate = not any(s in error_string for s in [ | ||
| "Can't run the expression locally", | ||
| "use of undeclared identifier", | ||
| "no member named", | ||
| "Couldn't lookup symbols", | ||
| "reference to local variable", | ||
| "invalid use of 'this' outside of a non-static member function", | ||
| ]) | ||
|
|
||
| is_optimized_away = any(s in error_string for s in [ | ||
| 'value may have been optimized out', | ||
| ]) | ||
|
|
||
| is_irretrievable = any(s in error_string for s in [ | ||
| "couldn't get the value of variable", | ||
| "couldn't read its memory", | ||
| "couldn't read from memory", | ||
| "Cannot access memory at address", | ||
| "invalid address (fault address:", | ||
| ]) | ||
|
|
||
| if could_evaluate and not is_irretrievable and not is_optimized_away: | ||
| assert error_string == 'success', (error_string, expression, value) | ||
| # assert result.value is not None, (result.value, expression) | ||
|
|
||
| if error_string == 'success': | ||
| error_string = None | ||
|
|
||
| # attempt to find expression as a variable, if found, take the variable | ||
| # obj's type information as it's 'usually' more accurate. | ||
| var_result = self._thread.GetFrameAtIndex(frame_idx).FindVariable(expression) | ||
| if str(var_result.error) == 'success': | ||
| type_name = var_result.type.GetDisplayTypeName() | ||
| else: | ||
| type_name = result.type.GetDisplayTypeName() | ||
|
|
||
| return ValueIR( | ||
| expression=expression, | ||
| value=value, | ||
| type_name=type_name, | ||
| error_string=error_string, | ||
| could_evaluate=could_evaluate, | ||
| is_optimized_away=is_optimized_away, | ||
| is_irretrievable=is_irretrievable, | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| # DExTer : Debugging Experience Tester | ||
| # ~~~~~~ ~ ~~ ~ ~~ | ||
| # | ||
| # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | ||
| # See https://llvm.org/LICENSE.txt for license information. | ||
| # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | ||
|
|
||
| from dex.debugger.lldb.LLDB import LLDB |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,224 @@ | ||
| # DExTer : Debugging Experience Tester | ||
| # ~~~~~~ ~ ~~ ~ ~~ | ||
| # | ||
| # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | ||
| # See https://llvm.org/LICENSE.txt for license information. | ||
| # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | ||
| """Interface for communicating with the Visual Studio debugger via DTE.""" | ||
|
|
||
| import abc | ||
| import imp | ||
| import os | ||
| import sys | ||
|
|
||
| from dex.debugger.DebuggerBase import DebuggerBase | ||
| from dex.dextIR import FrameIR, LocIR, StepIR, StopReason, ValueIR | ||
| from dex.dextIR import StackFrame, SourceLocation, ProgramState | ||
| from dex.utils.Exceptions import Error, LoadDebuggerException | ||
| from dex.utils.ReturnCode import ReturnCode | ||
|
|
||
|
|
||
| def _load_com_module(): | ||
| try: | ||
| module_info = imp.find_module( | ||
| 'ComInterface', | ||
| [os.path.join(os.path.dirname(__file__), 'windows')]) | ||
| return imp.load_module('ComInterface', *module_info) | ||
| except ImportError as e: | ||
| raise LoadDebuggerException(e, sys.exc_info()) | ||
|
|
||
|
|
||
| class VisualStudio(DebuggerBase, metaclass=abc.ABCMeta): # pylint: disable=abstract-method | ||
|
|
||
| # Constants for results of Debugger.CurrentMode | ||
| # (https://msdn.microsoft.com/en-us/library/envdte.debugger.currentmode.aspx) | ||
| dbgDesignMode = 1 | ||
| dbgBreakMode = 2 | ||
| dbgRunMode = 3 | ||
|
|
||
| def __init__(self, *args): | ||
| self.com_module = None | ||
| self._debugger = None | ||
| self._solution = None | ||
| self._fn_step = None | ||
| self._fn_go = None | ||
| super(VisualStudio, self).__init__(*args) | ||
|
|
||
| def _custom_init(self): | ||
| try: | ||
| self._debugger = self._interface.Debugger | ||
| self._debugger.HexDisplayMode = False | ||
|
|
||
| self._interface.MainWindow.Visible = ( | ||
| self.context.options.show_debugger) | ||
|
|
||
| self._solution = self._interface.Solution | ||
| self._solution.Create(self.context.working_directory.path, | ||
| 'DexterSolution') | ||
|
|
||
| try: | ||
| self._solution.AddFromFile(self._project_file) | ||
| except OSError: | ||
| raise LoadDebuggerException( | ||
| 'could not debug the specified executable', sys.exc_info()) | ||
|
|
||
| self._fn_step = self._debugger.StepInto | ||
| self._fn_go = self._debugger.Go | ||
|
|
||
| except AttributeError as e: | ||
| raise LoadDebuggerException(str(e), sys.exc_info()) | ||
|
|
||
| def _custom_exit(self): | ||
| if self._interface: | ||
| self._interface.Quit() | ||
|
|
||
| @property | ||
| def _project_file(self): | ||
| return self.context.options.executable | ||
|
|
||
| @abc.abstractproperty | ||
| def _dte_version(self): | ||
| pass | ||
|
|
||
| @property | ||
| def _location(self): | ||
| bp = self._debugger.BreakpointLastHit | ||
| return { | ||
| 'path': getattr(bp, 'File', None), | ||
| 'lineno': getattr(bp, 'FileLine', None), | ||
| 'column': getattr(bp, 'FileColumn', None) | ||
| } | ||
|
|
||
| @property | ||
| def _mode(self): | ||
| return self._debugger.CurrentMode | ||
|
|
||
| def _load_interface(self): | ||
| self.com_module = _load_com_module() | ||
| return self.com_module.DTE(self._dte_version) | ||
|
|
||
| @property | ||
| def version(self): | ||
| try: | ||
| return self._interface.Version | ||
| except AttributeError: | ||
| return None | ||
|
|
||
| def clear_breakpoints(self): | ||
| for bp in self._debugger.Breakpoints: | ||
| bp.Delete() | ||
|
|
||
| def add_breakpoint(self, file_, line): | ||
| self._debugger.Breakpoints.Add('', file_, line) | ||
|
|
||
| def launch(self): | ||
| self.step() | ||
|
|
||
| def step(self): | ||
| self._fn_step() | ||
|
|
||
| def go(self) -> ReturnCode: | ||
| self._fn_go() | ||
| return ReturnCode.OK | ||
|
|
||
| def set_current_stack_frame(self, idx: int = 0): | ||
| thread = self._debugger.CurrentThread | ||
| stack_frames = thread.StackFrames | ||
| try: | ||
| stack_frame = stack_frames[idx] | ||
| self._debugger.CurrentStackFrame = stack_frame.raw | ||
| except IndexError: | ||
| raise Error('attempted to access stack frame {} out of {}' | ||
| .format(idx, len(stack_frames))) | ||
|
|
||
| def get_step_info(self): | ||
| thread = self._debugger.CurrentThread | ||
| stackframes = thread.StackFrames | ||
|
|
||
| frames = [] | ||
| state_frames = [] | ||
|
|
||
|
|
||
| for idx, sf in enumerate(stackframes): | ||
| frame = FrameIR( | ||
| function=self._sanitize_function_name(sf.FunctionName), | ||
| is_inlined=sf.FunctionName.startswith('[Inline Frame]'), | ||
| loc=LocIR(path=None, lineno=None, column=None)) | ||
|
|
||
| fname = frame.function or '' # pylint: disable=no-member | ||
| if any(name in fname for name in self.frames_below_main): | ||
| break | ||
|
|
||
|
|
||
| state_frame = StackFrame(function=frame.function, | ||
| is_inlined=frame.is_inlined, | ||
| watches={}) | ||
|
|
||
| for watch in self.watches: | ||
| state_frame.watches[watch] = self.evaluate_expression( | ||
| watch, idx) | ||
|
|
||
|
|
||
| state_frames.append(state_frame) | ||
| frames.append(frame) | ||
|
|
||
| loc = LocIR(**self._location) | ||
| if frames: | ||
| frames[0].loc = loc | ||
| state_frames[0].location = SourceLocation(**self._location) | ||
|
|
||
| reason = StopReason.BREAKPOINT | ||
| if loc.path is None: # pylint: disable=no-member | ||
| reason = StopReason.STEP | ||
|
|
||
| program_state = ProgramState(frames=state_frames) | ||
|
|
||
| return StepIR( | ||
| step_index=self.step_index, frames=frames, stop_reason=reason, | ||
| program_state=program_state) | ||
|
|
||
| @property | ||
| def is_running(self): | ||
| return self._mode == VisualStudio.dbgRunMode | ||
|
|
||
| @property | ||
| def is_finished(self): | ||
| return self._mode == VisualStudio.dbgDesignMode | ||
|
|
||
| @property | ||
| def frames_below_main(self): | ||
| return [ | ||
| '[Inline Frame] invoke_main', '__scrt_common_main_seh', | ||
| '__tmainCRTStartup', 'mainCRTStartup' | ||
| ] | ||
|
|
||
| def evaluate_expression(self, expression, frame_idx=0) -> ValueIR: | ||
| self.set_current_stack_frame(frame_idx) | ||
| result = self._debugger.GetExpression(expression) | ||
| self.set_current_stack_frame(0) | ||
| value = result.Value | ||
|
|
||
| is_optimized_away = any(s in value for s in [ | ||
| 'Variable is optimized away and not available', | ||
| 'Value is not available, possibly due to optimization', | ||
| ]) | ||
|
|
||
| is_irretrievable = any(s in value for s in [ | ||
| '???', | ||
| '<Unable to read memory>', | ||
| ]) | ||
|
|
||
| # an optimized away value is still counted as being able to be | ||
| # evaluated. | ||
| could_evaluate = (result.IsValidValue or is_optimized_away | ||
| or is_irretrievable) | ||
|
|
||
| return ValueIR( | ||
| expression=expression, | ||
| value=value, | ||
| type_name=result.Type, | ||
| error_string=None, | ||
| is_optimized_away=is_optimized_away, | ||
| could_evaluate=could_evaluate, | ||
| is_irretrievable=is_irretrievable, | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| # DExTer : Debugging Experience Tester | ||
| # ~~~~~~ ~ ~~ ~ ~~ | ||
| # | ||
| # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | ||
| # See https://llvm.org/LICENSE.txt for license information. | ||
| # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | ||
| """Specializations for the Visual Studio 2015 interface.""" | ||
|
|
||
| from dex.debugger.visualstudio.VisualStudio import VisualStudio | ||
|
|
||
|
|
||
| class VisualStudio2015(VisualStudio): | ||
| @classmethod | ||
| def get_name(cls): | ||
| return 'Visual Studio 2015' | ||
|
|
||
| @classmethod | ||
| def get_option_name(cls): | ||
| return 'vs2015' | ||
|
|
||
| @property | ||
| def _dte_version(self): | ||
| return 'VisualStudio.DTE.14.0' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| # DExTer : Debugging Experience Tester | ||
| # ~~~~~~ ~ ~~ ~ ~~ | ||
| # | ||
| # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | ||
| # See https://llvm.org/LICENSE.txt for license information. | ||
| # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | ||
| """Specializations for the Visual Studio 2017 interface.""" | ||
|
|
||
| from dex.debugger.visualstudio.VisualStudio import VisualStudio | ||
|
|
||
|
|
||
| class VisualStudio2017(VisualStudio): | ||
| @classmethod | ||
| def get_name(cls): | ||
| return 'Visual Studio 2017' | ||
|
|
||
| @classmethod | ||
| def get_option_name(cls): | ||
| return 'vs2017' | ||
|
|
||
| @property | ||
| def _dte_version(self): | ||
| return 'VisualStudio.DTE.15.0' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| # DExTer : Debugging Experience Tester | ||
| # ~~~~~~ ~ ~~ ~ ~~ | ||
| # | ||
| # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | ||
| # See https://llvm.org/LICENSE.txt for license information. | ||
| # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | ||
|
|
||
| from dex.debugger.visualstudio.VisualStudio2015 import VisualStudio2015 | ||
| from dex.debugger.visualstudio.VisualStudio2017 import VisualStudio2017 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| # DExTer : Debugging Experience Tester | ||
| # ~~~~~~ ~ ~~ ~ ~~ | ||
| # | ||
| # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | ||
| # See https://llvm.org/LICENSE.txt for license information. | ||
| # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | ||
| """Communication via the Windows COM interface.""" | ||
|
|
||
| import inspect | ||
| import time | ||
| import sys | ||
|
|
||
| # pylint: disable=import-error | ||
| import win32com.client as com | ||
| import win32api | ||
| # pylint: enable=import-error | ||
|
|
||
| from dex.utils.Exceptions import LoadDebuggerException | ||
|
|
||
| _com_error = com.pywintypes.com_error # pylint: disable=no-member | ||
|
|
||
|
|
||
| def get_file_version(file_): | ||
| try: | ||
| info = win32api.GetFileVersionInfo(file_, '\\') | ||
| ms = info['FileVersionMS'] | ||
| ls = info['FileVersionLS'] | ||
| return '.'.join( | ||
| str(s) for s in [ | ||
| win32api.HIWORD(ms), | ||
| win32api.LOWORD(ms), | ||
| win32api.HIWORD(ls), | ||
| win32api.LOWORD(ls) | ||
| ]) | ||
| except com.pywintypes.error: # pylint: disable=no-member | ||
| return 'no versioninfo present' | ||
|
|
||
|
|
||
| def _handle_com_error(e): | ||
| exc = sys.exc_info() | ||
| msg = win32api.FormatMessage(e.hresult) | ||
| try: | ||
| msg = msg.decode('CP1251') | ||
| except AttributeError: | ||
| pass | ||
| msg = msg.strip() | ||
| return msg, exc | ||
|
|
||
|
|
||
| class ComObject(object): | ||
| """Wrap a raw Windows COM object in a class that implements auto-retry of | ||
| failed calls. | ||
| """ | ||
|
|
||
| def __init__(self, raw): | ||
| assert not isinstance(raw, ComObject), raw | ||
| self.__dict__['raw'] = raw | ||
|
|
||
| def __str__(self): | ||
| return self._call(self.raw.__str__) | ||
|
|
||
| def __getattr__(self, key): | ||
| if key in self.__dict__: | ||
| return self.__dict__[key] | ||
| return self._call(self.raw.__getattr__, key) | ||
|
|
||
| def __setattr__(self, key, val): | ||
| if key in self.__dict__: | ||
| self.__dict__[key] = val | ||
| self._call(self.raw.__setattr__, key, val) | ||
|
|
||
| def __getitem__(self, key): | ||
| return self._call(self.raw.__getitem__, key) | ||
|
|
||
| def __setitem__(self, key, val): | ||
| self._call(self.raw.__setitem__, key, val) | ||
|
|
||
| def __call__(self, *args): | ||
| return self._call(self.raw, *args) | ||
|
|
||
| @classmethod | ||
| def _call(cls, fn, *args): | ||
| """COM calls tend to randomly fail due to thread sync issues. | ||
| The Microsoft recommended solution is to set up a message filter object | ||
| to automatically retry failed calls, but this seems prohibitively hard | ||
| from python, so this is a custom solution to do the same thing. | ||
| All COM accesses should go through this function. | ||
| """ | ||
| ex = AssertionError("this should never be raised!") | ||
|
|
||
| assert (inspect.isfunction(fn) or inspect.ismethod(fn) | ||
| or inspect.isbuiltin(fn)), (fn, type(fn)) | ||
| retries = ([0] * 50) + ([1] * 5) | ||
| for r in retries: | ||
| try: | ||
| try: | ||
| result = fn(*args) | ||
| if inspect.ismethod(result) or 'win32com' in str( | ||
| result.__class__): | ||
| result = ComObject(result) | ||
| return result | ||
| except _com_error as e: | ||
| msg, _ = _handle_com_error(e) | ||
| e = WindowsError(msg) # pylint: disable=undefined-variable | ||
| raise e | ||
| except (AttributeError, TypeError, OSError) as e: | ||
| ex = e | ||
| time.sleep(r) | ||
| raise ex | ||
|
|
||
|
|
||
| class DTE(ComObject): | ||
| def __init__(self, class_string): | ||
| try: | ||
| super(DTE, self).__init__(com.DispatchEx(class_string)) | ||
| except _com_error as e: | ||
| msg, exc = _handle_com_error(e) | ||
| raise LoadDebuggerException( | ||
| '{} [{}]'.format(msg, class_string), orig_exception=exc) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| # DExTer : Debugging Experience Tester | ||
| # ~~~~~~ ~ ~~ ~ ~~ | ||
| # | ||
| # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | ||
| # See https://llvm.org/LICENSE.txt for license information. | ||
| # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| # DExTer : Debugging Experience Tester | ||
| # ~~~~~~ ~ ~~ ~ ~~ | ||
| # | ||
| # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | ||
| # See https://llvm.org/LICENSE.txt for license information. | ||
| # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | ||
|
|
||
|
|
||
| class BuilderIR: | ||
| """Data class which represents the compiler related options passed to Dexter | ||
| """ | ||
|
|
||
| def __init__(self, name: str, cflags: str, ldflags: str): | ||
| self.name = name | ||
| self.cflags = cflags | ||
| self.ldflags = ldflags |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| # DExTer : Debugging Experience Tester | ||
| # ~~~~~~ ~ ~~ ~ ~~ | ||
| # | ||
| # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | ||
| # See https://llvm.org/LICENSE.txt for license information. | ||
| # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | ||
|
|
||
|
|
||
| class DebuggerIR: | ||
| """Data class which represents a debugger.""" | ||
|
|
||
| def __init__(self, name: str, version: str): | ||
| self.name = name | ||
| self.version = version |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,129 @@ | ||
| # DExTer : Debugging Experience Tester | ||
| # ~~~~~~ ~ ~~ ~ ~~ | ||
| # | ||
| # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | ||
| # See https://llvm.org/LICENSE.txt for license information. | ||
| # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | ||
| from collections import OrderedDict | ||
| import os | ||
| from typing import List | ||
|
|
||
| from dex.dextIR.BuilderIR import BuilderIR | ||
| from dex.dextIR.DebuggerIR import DebuggerIR | ||
| from dex.dextIR.StepIR import StepIR, StepKind | ||
|
|
||
|
|
||
| def _step_kind_func(context, step): | ||
| if (step.current_location.path is None or | ||
| not os.path.exists(step.current_location.path)): | ||
| return StepKind.FUNC_UNKNOWN | ||
|
|
||
| if any(os.path.samefile(step.current_location.path, f) | ||
| for f in context.options.source_files): | ||
| return StepKind.FUNC | ||
|
|
||
| return StepKind.FUNC_EXTERNAL | ||
|
|
||
|
|
||
| class DextIR: | ||
| """A full Dexter test report. | ||
| This is composed of all the other *IR classes. They are used together to | ||
| record Dexter inputs and the resultant debugger steps, providing a single | ||
| high level access container. | ||
| The Heuristic class works with dexter commands and the generated DextIR to | ||
| determine the debugging score for a given test. | ||
| Args: | ||
| commands: { name (str), commands (list[CommandIR]) | ||
| """ | ||
|
|
||
| def __init__(self, | ||
| dexter_version: str, | ||
| executable_path: str, | ||
| source_paths: List[str], | ||
| builder: BuilderIR = None, | ||
| debugger: DebuggerIR = None, | ||
| commands: OrderedDict = None): | ||
| self.dexter_version = dexter_version | ||
| self.executable_path = executable_path | ||
| self.source_paths = source_paths | ||
| self.builder = builder | ||
| self.debugger = debugger | ||
| self.commands = commands | ||
| self.steps: List[StepIR] = [] | ||
|
|
||
| def __str__(self): | ||
| colors = 'rgby' | ||
| st = '## BEGIN ##\n' | ||
| color_idx = 0 | ||
| for step in self.steps: | ||
| if step.step_kind in (StepKind.FUNC, StepKind.FUNC_EXTERNAL, | ||
| StepKind.FUNC_UNKNOWN): | ||
| color_idx += 1 | ||
|
|
||
| color = colors[color_idx % len(colors)] | ||
| st += '<{}>{}</>\n'.format(color, step) | ||
| st += '## END ({} step{}) ##\n'.format( | ||
| self.num_steps, '' if self.num_steps == 1 else 's') | ||
| return st | ||
|
|
||
| @property | ||
| def num_steps(self): | ||
| return len(self.steps) | ||
|
|
||
| def _get_prev_step_in_this_frame(self, step): | ||
| """Find the most recent step in the same frame as `step`. | ||
| Returns: | ||
| StepIR or None if there is no previous step in this frame. | ||
| """ | ||
| return next((s for s in reversed(self.steps) | ||
| if s.current_function == step.current_function | ||
| and s.num_frames == step.num_frames), None) | ||
|
|
||
| def _get_new_step_kind(self, context, step): | ||
| if step.current_function is None: | ||
| return StepKind.UNKNOWN | ||
|
|
||
| if len(self.steps) == 0: | ||
| return _step_kind_func(context, step) | ||
|
|
||
| prev_step = self.steps[-1] | ||
|
|
||
| if prev_step.current_function is None: | ||
| return StepKind.UNKNOWN | ||
|
|
||
| if prev_step.num_frames < step.num_frames: | ||
| return _step_kind_func(context, step) | ||
|
|
||
| if prev_step.num_frames > step.num_frames: | ||
| frame_step = self._get_prev_step_in_this_frame(step) | ||
| prev_step = frame_step if frame_step is not None else prev_step | ||
|
|
||
| # We're in the same func as prev step, check lineo. | ||
| if prev_step.current_location.lineno > step.current_location.lineno: | ||
| return StepKind.VERTICAL_BACKWARD | ||
|
|
||
| if prev_step.current_location.lineno < step.current_location.lineno: | ||
| return StepKind.VERTICAL_FORWARD | ||
|
|
||
| # We're on the same line as prev step, check column. | ||
| if prev_step.current_location.column > step.current_location.column: | ||
| return StepKind.HORIZONTAL_BACKWARD | ||
|
|
||
| if prev_step.current_location.column < step.current_location.column: | ||
| return StepKind.HORIZONTAL_FORWARD | ||
|
|
||
| # This step is in exactly the same location as the prev step. | ||
| return StepKind.SAME | ||
|
|
||
| def new_step(self, context, step): | ||
| assert isinstance(step, StepIR), type(step) | ||
| step.step_kind = self._get_new_step_kind(context, step) | ||
| self.steps.append(step) | ||
| return step | ||
|
|
||
| def clear_steps(self): | ||
| self.steps.clear() |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| # DExTer : Debugging Experience Tester | ||
| # ~~~~~~ ~ ~~ ~ ~~ | ||
| # | ||
| # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | ||
| # See https://llvm.org/LICENSE.txt for license information. | ||
| # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | ||
| from dex.dextIR.LocIR import LocIR | ||
|
|
||
|
|
||
| class FrameIR: | ||
| """Data class which represents a frame in the call stack""" | ||
|
|
||
| def __init__(self, function: str, is_inlined: bool, loc: LocIR): | ||
| self.function = function | ||
| self.is_inlined = is_inlined | ||
| self.loc = loc |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| # DExTer : Debugging Experience Tester | ||
| # ~~~~~~ ~ ~~ ~ ~~ | ||
| # | ||
| # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | ||
| # See https://llvm.org/LICENSE.txt for license information. | ||
| # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | ||
| import os | ||
|
|
||
|
|
||
| class LocIR: | ||
| """Data class which represents a source location.""" | ||
|
|
||
| def __init__(self, path: str, lineno: int, column: int): | ||
| if path: | ||
| path = os.path.normcase(path) | ||
| self.path = path | ||
| self.lineno = lineno | ||
| self.column = column | ||
|
|
||
| def __str__(self): | ||
| return '{}({}:{})'.format(self.path, self.lineno, self.column) | ||
|
|
||
| def __eq__(self, rhs): | ||
| return (os.path.exists(self.path) and os.path.exists(rhs.path) | ||
| and os.path.samefile(self.path, rhs.path) | ||
| and self.lineno == rhs.lineno | ||
| and self.column == rhs.column) | ||
|
|
||
| def __lt__(self, rhs): | ||
| if self.path != rhs.path: | ||
| return False | ||
|
|
||
| if self.lineno == rhs.lineno: | ||
| return self.column < rhs.column | ||
|
|
||
| return self.lineno < rhs.lineno | ||
|
|
||
| def __gt__(self, rhs): | ||
| if self.path != rhs.path: | ||
| return False | ||
|
|
||
| if self.lineno == rhs.lineno: | ||
| return self.column > rhs.column | ||
|
|
||
| return self.lineno > rhs.lineno |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| # DExTer : Debugging Experience Tester | ||
| # ~~~~~~ ~ ~~ ~ ~~ | ||
| # | ||
| # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | ||
| # See https://llvm.org/LICENSE.txt for license information. | ||
| # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | ||
| """Set of data classes for representing the complete debug program state at a | ||
| fixed point in execution. | ||
| """ | ||
|
|
||
| import os | ||
|
|
||
| from collections import OrderedDict | ||
| from typing import List | ||
|
|
||
| class SourceLocation: | ||
| def __init__(self, path: str = None, lineno: int = None, column: int = None): | ||
| if path: | ||
| path = os.path.normcase(path) | ||
| self.path = path | ||
| self.lineno = lineno | ||
| self.column = column | ||
|
|
||
| def __str__(self): | ||
| return '{}({}:{})'.format(self.path, self.lineno, self.column) | ||
|
|
||
| def match(self, other) -> bool: | ||
| """Returns true iff all the properties that appear in `self` have the | ||
| same value in `other`, but not necessarily vice versa. | ||
| """ | ||
| if not other or not isinstance(other, SourceLocation): | ||
| return False | ||
|
|
||
| if self.path and (self.path != other.path): | ||
| return False | ||
|
|
||
| if self.lineno and (self.lineno != other.lineno): | ||
| return False | ||
|
|
||
| if self.column and (self.column != other.column): | ||
| return False | ||
|
|
||
| return True | ||
|
|
||
|
|
||
| class StackFrame: | ||
| def __init__(self, | ||
| function: str = None, | ||
| is_inlined: bool = None, | ||
| location: SourceLocation = None, | ||
| watches: OrderedDict = None): | ||
| if watches is None: | ||
| watches = {} | ||
|
|
||
| self.function = function | ||
| self.is_inlined = is_inlined | ||
| self.location = location | ||
| self.watches = watches | ||
|
|
||
| def __str__(self): | ||
| return '{}{}: {} | {}'.format( | ||
| self.function, | ||
| ' (inlined)' if self.is_inlined else '', | ||
| self.location, | ||
| {k: str(self.watches[k]) for k in self.watches}) | ||
|
|
||
| def match(self, other) -> bool: | ||
| """Returns true iff all the properties that appear in `self` have the | ||
| same value in `other`, but not necessarily vice versa. | ||
| """ | ||
| if not other or not isinstance(other, StackFrame): | ||
| return False | ||
|
|
||
| if self.location and not self.location.match(other.location): | ||
| return False | ||
|
|
||
| if self.watches: | ||
| for name in iter(self.watches): | ||
| try: | ||
| if isinstance(self.watches[name], dict): | ||
| for attr in iter(self.watches[name]): | ||
| if (getattr(other.watches[name], attr, None) != | ||
| self.watches[name][attr]): | ||
| return False | ||
| else: | ||
| if other.watches[name].value != self.watches[name]: | ||
| return False | ||
| except KeyError: | ||
| return False | ||
|
|
||
| return True | ||
|
|
||
| class ProgramState: | ||
| def __init__(self, frames: List[StackFrame] = None): | ||
| self.frames = frames | ||
|
|
||
| def __str__(self): | ||
| return '\n'.join(map( | ||
| lambda enum: 'Frame {}: {}'.format(enum[0], enum[1]), | ||
| enumerate(self.frames))) | ||
|
|
||
| def match(self, other) -> bool: | ||
| """Returns true iff all the properties that appear in `self` have the | ||
| same value in `other`, but not necessarily vice versa. | ||
| """ | ||
| if not other or not isinstance(other, ProgramState): | ||
| return False | ||
|
|
||
| if self.frames: | ||
| for idx, frame in enumerate(self.frames): | ||
| try: | ||
| if not frame.match(other.frames[idx]): | ||
| return False | ||
| except (IndexError, KeyError): | ||
| return False | ||
|
|
||
| return True |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| # DExTer : Debugging Experience Tester | ||
| # ~~~~~~ ~ ~~ ~ ~~ | ||
| # | ||
| # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | ||
| # See https://llvm.org/LICENSE.txt for license information. | ||
| # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | ||
| """Classes which are used to represent debugger steps.""" | ||
|
|
||
| import json | ||
|
|
||
| from collections import OrderedDict | ||
| from typing import List | ||
| from enum import Enum | ||
| from dex.dextIR.FrameIR import FrameIR | ||
| from dex.dextIR.LocIR import LocIR | ||
| from dex.dextIR.ProgramState import ProgramState | ||
|
|
||
|
|
||
| class StopReason(Enum): | ||
| BREAKPOINT = 0 | ||
| STEP = 1 | ||
| PROGRAM_EXIT = 2 | ||
| ERROR = 3 | ||
| OTHER = 4 | ||
|
|
||
|
|
||
| class StepKind(Enum): | ||
| FUNC = 0 | ||
| FUNC_EXTERNAL = 1 | ||
| FUNC_UNKNOWN = 2 | ||
| VERTICAL_FORWARD = 3 | ||
| SAME = 4 | ||
| VERTICAL_BACKWARD = 5 | ||
| UNKNOWN = 6 | ||
| HORIZONTAL_FORWARD = 7 | ||
| HORIZONTAL_BACKWARD = 8 | ||
|
|
||
|
|
||
| class StepIR: | ||
| """A debugger step. | ||
| Args: | ||
| watches (OrderedDict): { expression (str), result (ValueIR) } | ||
| """ | ||
|
|
||
| def __init__(self, | ||
| step_index: int, | ||
| stop_reason: StopReason, | ||
| frames: List[FrameIR], | ||
| step_kind: StepKind = None, | ||
| watches: OrderedDict = None, | ||
| program_state: ProgramState = None): | ||
| self.step_index = step_index | ||
| self.step_kind = step_kind | ||
| self.stop_reason = stop_reason | ||
| self.program_state = program_state | ||
|
|
||
| if frames is None: | ||
| frames = [] | ||
| self.frames = frames | ||
|
|
||
| if watches is None: | ||
| watches = {} | ||
| self.watches = watches | ||
|
|
||
| def __str__(self): | ||
| try: | ||
| frame = self.current_frame | ||
| frame_info = (frame.function, frame.loc.path, frame.loc.lineno, | ||
| frame.loc.column) | ||
| except AttributeError: | ||
| frame_info = (None, None, None, None) | ||
|
|
||
| step_info = (self.step_index, ) + frame_info + ( | ||
| str(self.stop_reason), str(self.step_kind), | ||
| [w for w in self.watches]) | ||
|
|
||
| return '{}{}'.format('. ' * (self.num_frames - 1), | ||
| json.dumps(step_info)) | ||
|
|
||
| @property | ||
| def num_frames(self): | ||
| return len(self.frames) | ||
|
|
||
| @property | ||
| def current_frame(self): | ||
| if not len(self.frames): | ||
| return None | ||
| return self.frames[0] | ||
|
|
||
| @property | ||
| def current_function(self): | ||
| try: | ||
| return self.current_frame.function | ||
| except AttributeError: | ||
| return None | ||
|
|
||
| @property | ||
| def current_location(self): | ||
| try: | ||
| return self.current_frame.loc | ||
| except AttributeError: | ||
| return LocIR(path=None, lineno=None, column=None) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| # DExTer : Debugging Experience Tester | ||
| # ~~~~~~ ~ ~~ ~ ~~ | ||
| # | ||
| # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | ||
| # See https://llvm.org/LICENSE.txt for license information. | ||
| # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | ||
|
|
||
|
|
||
| class ValueIR: | ||
| """Data class to store the result of an expression evaluation.""" | ||
|
|
||
| def __init__(self, | ||
| expression: str, | ||
| value: str, | ||
| type_name: str, | ||
| could_evaluate: bool, | ||
| error_string: str = None, | ||
| is_optimized_away: bool = False, | ||
| is_irretrievable: bool = False): | ||
| self.expression = expression | ||
| self.value = value | ||
| self.type_name = type_name | ||
| self.could_evaluate = could_evaluate | ||
| self.error_string = error_string | ||
| self.is_optimized_away = is_optimized_away | ||
| self.is_irretrievable = is_irretrievable | ||
|
|
||
| def __str__(self): | ||
| prefix = '"{}": '.format(self.expression) | ||
| if self.error_string is not None: | ||
| return prefix + self.error_string | ||
| if self.value is not None: | ||
| return prefix + '({}) {}'.format(self.type_name, self.value) | ||
| return (prefix + | ||
| 'could_evaluate: {}; irretrievable: {}; optimized_away: {};' | ||
| .format(self.could_evaluate, self.is_irretrievable, | ||
| self.is_optimized_away)) | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| # DExTer : Debugging Experience Tester | ||
| # ~~~~~~ ~ ~~ ~ ~~ | ||
| # | ||
| # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | ||
| # See https://llvm.org/LICENSE.txt for license information. | ||
| # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | ||
| """dextIR: DExTer Intermediate Representation of DExTer's debugger trace output. | ||
| """ | ||
|
|
||
| from dex.dextIR.BuilderIR import BuilderIR | ||
| from dex.dextIR.DextIR import DextIR | ||
| from dex.dextIR.DebuggerIR import DebuggerIR | ||
| from dex.dextIR.FrameIR import FrameIR | ||
| from dex.dextIR.LocIR import LocIR | ||
| from dex.dextIR.StepIR import StepIR, StepKind, StopReason | ||
| from dex.dextIR.ValueIR import ValueIR | ||
| from dex.dextIR.ProgramState import ProgramState, SourceLocation, StackFrame |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| # DExTer : Debugging Experience Tester | ||
| # ~~~~~~ ~ ~~ ~ ~~ | ||
| # | ||
| # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | ||
| # See https://llvm.org/LICENSE.txt for license information. | ||
| # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | ||
|
|
||
| from dex.heuristic.Heuristic import Heuristic, StepValueInfo |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,207 @@ | ||
| # DExTer : Debugging Experience Tester | ||
| # ~~~~~~ ~ ~~ ~ ~~ | ||
| # | ||
| # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | ||
| # See https://llvm.org/LICENSE.txt for license information. | ||
| # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | ||
| """This is the main entry point. | ||
| It implements some functionality common to all subtools such as command line | ||
| parsing and running the unit-testing harnesses, before calling the reequested | ||
| subtool. | ||
| """ | ||
|
|
||
| import imp | ||
| import os | ||
| import sys | ||
|
|
||
| from dex.utils import PrettyOutput, Timer | ||
| from dex.utils import ExtArgParse as argparse | ||
| from dex.utils import get_root_directory | ||
| from dex.utils.Exceptions import Error, ToolArgumentError | ||
| from dex.utils.UnitTests import unit_tests_ok | ||
| from dex.utils.Version import version | ||
| from dex.utils import WorkingDirectory | ||
| from dex.utils.ReturnCode import ReturnCode | ||
|
|
||
|
|
||
| def _output_bug_report_message(context): | ||
| """ In the event of a catastrophic failure, print bug report request to the | ||
| user. | ||
| """ | ||
| context.o.red( | ||
| '\n\n' | ||
| '<g>****************************************</>\n' | ||
| '<b>****************************************</>\n' | ||
| '****************************************\n' | ||
| '** **\n' | ||
| '** <y>This is a bug in <a>DExTer</>.</> **\n' | ||
| '** **\n' | ||
| '** <y>Please report it.</> **\n' | ||
| '** **\n' | ||
| '****************************************\n' | ||
| '<b>****************************************</>\n' | ||
| '<g>****************************************</>\n' | ||
| '\n' | ||
| '<b>system:</>\n' | ||
| '<d>{}</>\n\n' | ||
| '<b>version:</>\n' | ||
| '<d>{}</>\n\n' | ||
| '<b>args:</>\n' | ||
| '<d>{}</>\n' | ||
| '\n'.format(sys.platform, version('DExTer'), | ||
| [sys.executable] + sys.argv), | ||
| stream=PrettyOutput.stderr) | ||
|
|
||
|
|
||
| def get_tools_directory(): | ||
| """ Returns directory path where DExTer tool imports can be | ||
| found. | ||
| """ | ||
| tools_directory = os.path.join(get_root_directory(), 'tools') | ||
| assert os.path.isdir(tools_directory), tools_directory | ||
| return tools_directory | ||
|
|
||
|
|
||
| def get_tool_names(): | ||
| """ Returns a list of expected DExTer Tools | ||
| """ | ||
| return [ | ||
| 'clang-opt-bisect', 'help', 'list-debuggers', 'no-tool-', | ||
| 'run-debugger-internal-', 'test', 'view' | ||
| ] | ||
|
|
||
|
|
||
| def _set_auto_highlights(context): | ||
| """Flag some strings for auto-highlighting. | ||
| """ | ||
| context.o.auto_reds.extend([ | ||
| r'[Ee]rror\:', | ||
| r'[Ee]xception\:', | ||
| r'un(expected|recognized) argument', | ||
| ]) | ||
| context.o.auto_yellows.extend([ | ||
| r'[Ww]arning\:', | ||
| r'\(did you mean ', | ||
| r'During handling of the above exception, another exception', | ||
| ]) | ||
|
|
||
|
|
||
| def _get_options_and_args(context): | ||
| """ get the options and arguments from the commandline | ||
| """ | ||
| parser = argparse.ExtArgumentParser(context, add_help=False) | ||
| parser.add_argument('tool', default=None, nargs='?') | ||
| options, args = parser.parse_known_args(sys.argv[1:]) | ||
|
|
||
| return options, args | ||
|
|
||
|
|
||
| def _get_tool_name(options): | ||
| """ get the name of the dexter tool (if passed) specified on the command | ||
| line, otherwise return 'no_tool_'. | ||
| """ | ||
| tool_name = options.tool | ||
| if tool_name is None: | ||
| tool_name = 'no_tool_' | ||
| else: | ||
| _is_valid_tool_name(tool_name) | ||
| return tool_name | ||
|
|
||
|
|
||
| def _is_valid_tool_name(tool_name): | ||
| """ check tool name matches a tool directory within the dexter tools | ||
| directory. | ||
| """ | ||
| valid_tools = get_tool_names() | ||
| if tool_name not in valid_tools: | ||
| raise Error('invalid tool "{}" (choose from {})'.format( | ||
| tool_name, | ||
| ', '.join([t for t in valid_tools if not t.endswith('-')]))) | ||
|
|
||
|
|
||
| def _import_tool_module(tool_name): | ||
| """ Imports the python module at the tool directory specificed by | ||
| tool_name. | ||
| """ | ||
| # format tool argument to reflect tool directory form. | ||
| tool_name = tool_name.replace('-', '_') | ||
|
|
||
| tools_directory = get_tools_directory() | ||
| module_info = imp.find_module(tool_name, [tools_directory]) | ||
|
|
||
| return imp.load_module(tool_name, *module_info) | ||
|
|
||
|
|
||
| def tool_main(context, tool, args): | ||
| with Timer(tool.name): | ||
| options, defaults = tool.parse_command_line(args) | ||
| Timer.display = options.time_report | ||
| Timer.indent = options.indent_timer_level | ||
| Timer.fn = context.o.blue | ||
| context.options = options | ||
| context.version = version(tool.name) | ||
|
|
||
| if options.version: | ||
| context.o.green('{}\n'.format(context.version)) | ||
| return ReturnCode.OK | ||
|
|
||
| if (options.unittest != 'off' and not unit_tests_ok(context)): | ||
| raise Error('<d>unit test failures</>') | ||
|
|
||
| if options.colortest: | ||
| context.o.colortest() | ||
| return ReturnCode.OK | ||
|
|
||
| try: | ||
| tool.handle_base_options(defaults) | ||
| except ToolArgumentError as e: | ||
| raise Error(e) | ||
|
|
||
| dir_ = context.options.working_directory | ||
| with WorkingDirectory(context, dir=dir_) as context.working_directory: | ||
| return_code = tool.go() | ||
|
|
||
| return return_code | ||
|
|
||
|
|
||
| class Context(object): | ||
| """Context encapsulates globally useful objects and data; passed to many | ||
| Dexter functions. | ||
| """ | ||
|
|
||
| def __init__(self): | ||
| self.o: PrettyOutput = None | ||
| self.working_directory: str = None | ||
| self.options: dict = None | ||
| self.version: str = None | ||
| self.root_directory: str = None | ||
|
|
||
|
|
||
| def main() -> ReturnCode: | ||
|
|
||
| context = Context() | ||
|
|
||
| with PrettyOutput() as context.o: | ||
| try: | ||
| context.root_directory = get_root_directory() | ||
| # Flag some strings for auto-highlighting. | ||
| _set_auto_highlights(context) | ||
| options, args = _get_options_and_args(context) | ||
| # raises 'Error' if command line tool is invalid. | ||
| tool_name = _get_tool_name(options) | ||
| module = _import_tool_module(tool_name) | ||
| return tool_main(context, module.Tool(context), args) | ||
| except Error as e: | ||
| context.o.auto( | ||
| '\nerror: {}\n'.format(str(e)), stream=PrettyOutput.stderr) | ||
| try: | ||
| if context.options.error_debug: | ||
| raise | ||
| except AttributeError: | ||
| pass | ||
| return ReturnCode._ERROR | ||
| except (KeyboardInterrupt, SystemExit): | ||
| raise | ||
| except: # noqa | ||
| _output_bug_report_message(context) | ||
| raise |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,148 @@ | ||
| # DExTer : Debugging Experience Tester | ||
| # ~~~~~~ ~ ~~ ~ ~~ | ||
| # | ||
| # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | ||
| # See https://llvm.org/LICENSE.txt for license information. | ||
| # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | ||
| """Base class for subtools that do build/run tests.""" | ||
|
|
||
| import abc | ||
| from datetime import datetime | ||
| import os | ||
| import sys | ||
|
|
||
| from dex.builder import add_builder_tool_arguments | ||
| from dex.builder import handle_builder_tool_options | ||
| from dex.debugger.Debuggers import add_debugger_tool_arguments | ||
| from dex.debugger.Debuggers import handle_debugger_tool_options | ||
| from dex.heuristic.Heuristic import add_heuristic_tool_arguments | ||
| from dex.tools.ToolBase import ToolBase | ||
| from dex.utils import get_root_directory, warn | ||
| from dex.utils.Exceptions import Error, ToolArgumentError | ||
| from dex.utils.ReturnCode import ReturnCode | ||
|
|
||
|
|
||
| class TestToolBase(ToolBase): | ||
| def __init__(self, *args, **kwargs): | ||
| super(TestToolBase, self).__init__(*args, **kwargs) | ||
| self.build_script: str = None | ||
|
|
||
| def add_tool_arguments(self, parser, defaults): | ||
| parser.description = self.__doc__ | ||
| add_builder_tool_arguments(parser) | ||
| add_debugger_tool_arguments(parser, self.context, defaults) | ||
| add_heuristic_tool_arguments(parser) | ||
|
|
||
| parser.add_argument( | ||
| 'test_path', | ||
| type=str, | ||
| metavar='<test-path>', | ||
| nargs='?', | ||
| default=os.path.abspath( | ||
| os.path.join(get_root_directory(), '..', 'tests')), | ||
| help='directory containing test(s)') | ||
|
|
||
| parser.add_argument( | ||
| '--results-directory', | ||
| type=str, | ||
| metavar='<directory>', | ||
| default=os.path.abspath( | ||
| os.path.join(get_root_directory(), '..', 'results', | ||
| datetime.now().strftime('%Y-%m-%d-%H%M-%S'))), | ||
| help='directory to save results') | ||
|
|
||
| def handle_options(self, defaults): | ||
| options = self.context.options | ||
|
|
||
| # We accept either or both of --binary and --builder. | ||
| if not options.binary and not options.builder: | ||
| raise Error('expected --builder or --binary') | ||
|
|
||
| # --binary overrides --builder | ||
| if options.binary: | ||
| if options.builder: | ||
| warn(self.context, "overriding --builder with --binary\n") | ||
|
|
||
| options.binary = os.path.abspath(options.binary) | ||
| if not os.path.isfile(options.binary): | ||
| raise Error('<d>could not find binary file</> <r>"{}"</>' | ||
| .format(options.binary)) | ||
| else: | ||
| try: | ||
| self.build_script = handle_builder_tool_options(self.context) | ||
| except ToolArgumentError as e: | ||
| raise Error(e) | ||
|
|
||
| try: | ||
| handle_debugger_tool_options(self.context, defaults) | ||
| except ToolArgumentError as e: | ||
| raise Error(e) | ||
|
|
||
| options.test_path = os.path.abspath(options.test_path) | ||
| if not os.path.isfile(options.test_path) and not os.path.isdir(options.test_path): | ||
| raise Error( | ||
| '<d>could not find test path</> <r>"{}"</>'.format( | ||
| options.test_path)) | ||
|
|
||
| options.results_directory = os.path.abspath(options.results_directory) | ||
| if not os.path.isdir(options.results_directory): | ||
| try: | ||
| os.makedirs(options.results_directory, exist_ok=True) | ||
| except OSError as e: | ||
| raise Error( | ||
| '<d>could not create directory</> <r>"{}"</> <y>({})</>'. | ||
| format(options.results_directory, e.strerror)) | ||
|
|
||
| def go(self) -> ReturnCode: # noqa | ||
| options = self.context.options | ||
|
|
||
| options.executable = os.path.join( | ||
| self.context.working_directory.path, 'tmp.exe') | ||
|
|
||
| if os.path.isdir(options.test_path): | ||
|
|
||
| subdirs = sorted([ | ||
| r for r, _, f in os.walk(options.test_path) | ||
| if 'test.cfg' in f | ||
| ]) | ||
|
|
||
| for subdir in subdirs: | ||
|
|
||
| # TODO: read file extensions from the test.cfg file instead so | ||
| # that this isn't just limited to C and C++. | ||
| options.source_files = [ | ||
| os.path.normcase(os.path.join(subdir, f)) | ||
| for f in os.listdir(subdir) if any( | ||
| f.endswith(ext) for ext in ['.c', '.cpp']) | ||
| ] | ||
|
|
||
| self._run_test(self._get_test_name(subdir)) | ||
| else: | ||
| options.source_files = [options.test_path] | ||
| self._run_test(self._get_test_name(options.test_path)) | ||
|
|
||
| return self._handle_results() | ||
|
|
||
| @staticmethod | ||
| def _is_current_directory(test_directory): | ||
| return test_directory == '.' | ||
|
|
||
| def _get_test_name(self, test_path): | ||
| """Get the test name from either the test file, or the sub directory | ||
| path it's stored in. | ||
| """ | ||
| # test names are distinguished by their relative path from the | ||
| # specified test path. | ||
| test_name = os.path.relpath(test_path, | ||
| self.context.options.test_path) | ||
| if self._is_current_directory(test_name): | ||
| test_name = os.path.basename(test_path) | ||
| return test_name | ||
|
|
||
| @abc.abstractmethod | ||
| def _run_test(self, test_dir): | ||
| pass | ||
|
|
||
| @abc.abstractmethod | ||
| def _handle_results(self) -> ReturnCode: | ||
| pass |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,135 @@ | ||
| # DExTer : Debugging Experience Tester | ||
| # ~~~~~~ ~ ~~ ~ ~~ | ||
| # | ||
| # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | ||
| # See https://llvm.org/LICENSE.txt for license information. | ||
| # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | ||
| """Base class for all subtools.""" | ||
|
|
||
| import abc | ||
| import os | ||
| import tempfile | ||
|
|
||
| from dex import __version__ | ||
| from dex.utils import ExtArgParse | ||
| from dex.utils import PrettyOutput | ||
| from dex.utils.ReturnCode import ReturnCode | ||
|
|
||
|
|
||
| class ToolBase(object, metaclass=abc.ABCMeta): | ||
| def __init__(self, context): | ||
| self.context = context | ||
| self.parser = None | ||
|
|
||
| @abc.abstractproperty | ||
| def name(self): | ||
| pass | ||
|
|
||
| @abc.abstractmethod | ||
| def add_tool_arguments(self, parser, defaults): | ||
| pass | ||
|
|
||
| def parse_command_line(self, args): | ||
| """Define two parsers: pparser and self.parser. | ||
| pparser deals with args that need to be parsed prior to any of those of | ||
| self.parser. For example, any args which may affect the state of | ||
| argparse error output. | ||
| """ | ||
|
|
||
| class defaults(object): | ||
| pass | ||
|
|
||
| pparser = ExtArgParse.ExtArgumentParser( | ||
| self.context, add_help=False, prog=self.name) | ||
|
|
||
| pparser.add_argument( | ||
| '--no-color-output', | ||
| action='store_true', | ||
| default=False, | ||
| help='do not use colored output on stdout/stderr') | ||
| pparser.add_argument( | ||
| '--time-report', | ||
| action='store_true', | ||
| default=False, | ||
| help='display timing statistics') | ||
|
|
||
| self.parser = ExtArgParse.ExtArgumentParser( | ||
| self.context, parents=[pparser], prog=self.name) | ||
| self.parser.add_argument( | ||
| '-v', | ||
| '--verbose', | ||
| action='store_true', | ||
| default=False, | ||
| help='enable verbose output') | ||
| self.parser.add_argument( | ||
| '-V', | ||
| '--version', | ||
| action='store_true', | ||
| default=False, | ||
| help='display the DExTer version and exit') | ||
| self.parser.add_argument( | ||
| '-w', | ||
| '--no-warnings', | ||
| action='store_true', | ||
| default=False, | ||
| help='suppress warning output') | ||
| self.parser.add_argument( | ||
| '--unittest', | ||
| type=str, | ||
| choices=['off', 'show-failures', 'show-all'], | ||
| default='off', | ||
| help='run the DExTer codebase unit tests') | ||
|
|
||
| suppress = ExtArgParse.SUPPRESS # pylint: disable=no-member | ||
| self.parser.add_argument( | ||
| '--colortest', action='store_true', default=False, help=suppress) | ||
| self.parser.add_argument( | ||
| '--error-debug', action='store_true', default=False, help=suppress) | ||
| defaults.working_directory = os.path.join(tempfile.gettempdir(), | ||
| 'dexter') | ||
| self.parser.add_argument( | ||
| '--indent-timer-level', type=int, default=1, help=suppress) | ||
| self.parser.add_argument( | ||
| '--working-directory', | ||
| type=str, | ||
| metavar='<file>', | ||
| default=None, | ||
| display_default=defaults.working_directory, | ||
| help='location of working directory') | ||
| self.parser.add_argument( | ||
| '--save-temps', | ||
| action='store_true', | ||
| default=False, | ||
| help='save temporary files') | ||
|
|
||
| self.add_tool_arguments(self.parser, defaults) | ||
|
|
||
| # If an error is encountered during pparser, show the full usage text | ||
| # including self.parser options. Strip the preceding 'usage: ' to avoid | ||
| # having it appear twice. | ||
| pparser.usage = self.parser.format_usage().lstrip('usage: ') | ||
|
|
||
| options, args = pparser.parse_known_args(args) | ||
|
|
||
| if options.no_color_output: | ||
| PrettyOutput.stdout.color_enabled = False | ||
| PrettyOutput.stderr.color_enabled = False | ||
|
|
||
| options = self.parser.parse_args(args, namespace=options) | ||
| return options, defaults | ||
|
|
||
| def handle_base_options(self, defaults): | ||
| self.handle_options(defaults) | ||
|
|
||
| options = self.context.options | ||
|
|
||
| if options.working_directory is None: | ||
| options.working_directory = defaults.working_directory | ||
|
|
||
| @abc.abstractmethod | ||
| def handle_options(self, defaults): | ||
| pass | ||
|
|
||
| @abc.abstractmethod | ||
| def go(self) -> ReturnCode: | ||
| pass |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| # DExTer : Debugging Experience Tester | ||
| # ~~~~~~ ~ ~~ ~ ~~ | ||
| # | ||
| # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | ||
| # See https://llvm.org/LICENSE.txt for license information. | ||
| # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | ||
|
|
||
| from dex.tools.Main import Context, get_tool_names, get_tools_directory, main, tool_main | ||
| from dex.tools.TestToolBase import TestToolBase | ||
| from dex.tools.ToolBase import ToolBase |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,286 @@ | ||
| # DExTer : Debugging Experience Tester | ||
| # ~~~~~~ ~ ~~ ~ ~~ | ||
| # | ||
| # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | ||
| # See https://llvm.org/LICENSE.txt for license information. | ||
| # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | ||
| """Clang opt-bisect tool.""" | ||
|
|
||
| from collections import defaultdict | ||
| import os | ||
| import csv | ||
| import re | ||
| import pickle | ||
|
|
||
| from dex.builder import run_external_build_script | ||
| from dex.debugger.Debuggers import empty_debugger_steps, get_debugger_steps | ||
| from dex.heuristic import Heuristic | ||
| from dex.tools import TestToolBase | ||
| from dex.utils.Exceptions import DebuggerException, Error | ||
| from dex.utils.Exceptions import BuildScriptException, HeuristicException | ||
| from dex.utils.PrettyOutputBase import Stream | ||
| from dex.utils.ReturnCode import ReturnCode | ||
|
|
||
|
|
||
| class BisectPass(object): | ||
| def __init__(self, no, description, description_no_loc): | ||
| self.no = no | ||
| self.description = description | ||
| self.description_no_loc = description_no_loc | ||
|
|
||
| self.penalty = 0 | ||
| self.differences = [] | ||
|
|
||
|
|
||
| class Tool(TestToolBase): | ||
| """Use the LLVM "-opt-bisect-limit=<n>" flag to get information on the | ||
| contribution of each LLVM pass to the overall DExTer score when using | ||
| clang. | ||
| Clang is run multiple times, with an increasing value of n, measuring the | ||
| debugging experience at each value. | ||
| """ | ||
|
|
||
| _re_running_pass = re.compile( | ||
| r'^BISECT\: running pass \((\d+)\) (.+?)( \(.+\))?$') | ||
|
|
||
| def __init__(self, *args, **kwargs): | ||
| super(Tool, self).__init__(*args, **kwargs) | ||
| self._all_bisect_pass_summary = defaultdict(list) | ||
|
|
||
| @property | ||
| def name(self): | ||
| return 'DExTer clang opt bisect' | ||
|
|
||
| def _get_bisect_limits(self): | ||
| options = self.context.options | ||
|
|
||
| max_limit = 999999 | ||
| limits = [max_limit for _ in options.source_files] | ||
| all_passes = [ | ||
| l for l in self._clang_opt_bisect_build(limits)[1].splitlines() | ||
| if l.startswith('BISECT: running pass (') | ||
| ] | ||
|
|
||
| results = [] | ||
| for i, pass_ in enumerate(all_passes[1:]): | ||
| if pass_.startswith('BISECT: running pass (1)'): | ||
| results.append(all_passes[i]) | ||
| results.append(all_passes[-1]) | ||
|
|
||
| assert len(results) == len( | ||
| options.source_files), (results, options.source_files) | ||
|
|
||
| limits = [ | ||
| int(Tool._re_running_pass.match(r).group(1)) for r in results | ||
| ] | ||
|
|
||
| return limits | ||
|
|
||
| def _run_test(self, test_name): # noqa | ||
| options = self.context.options | ||
|
|
||
| per_pass_score = [] | ||
| current_bisect_pass_summary = defaultdict(list) | ||
|
|
||
| max_limits = self._get_bisect_limits() | ||
| overall_limit = sum(max_limits) | ||
| prev_score = 1.0 | ||
| prev_steps_str = None | ||
|
|
||
| for current_limit in range(overall_limit + 1): | ||
| # Take the overall limit number and split it across buckets for | ||
| # each source file. | ||
| limit_remaining = current_limit | ||
| file_limits = [0] * len(max_limits) | ||
| for i, max_limit in enumerate(max_limits): | ||
| if limit_remaining < max_limit: | ||
| file_limits[i] += limit_remaining | ||
| break | ||
| else: | ||
| file_limits[i] = max_limit | ||
| limit_remaining -= file_limits[i] | ||
|
|
||
| f = [l for l in file_limits if l] | ||
| current_file_index = len(f) - 1 if f else 0 | ||
|
|
||
| _, err, builderIR = self._clang_opt_bisect_build(file_limits) | ||
| err_lines = err.splitlines() | ||
| # Find the last line that specified a running pass. | ||
| for l in err_lines[::-1]: | ||
| match = Tool._re_running_pass.match(l) | ||
| if match: | ||
| pass_info = match.groups() | ||
| break | ||
| else: | ||
| pass_info = (0, None, None) | ||
|
|
||
| try: | ||
| steps = get_debugger_steps(self.context) | ||
| except DebuggerException: | ||
| steps = empty_debugger_steps(self.context) | ||
|
|
||
| steps.builder = builderIR | ||
|
|
||
| try: | ||
| heuristic = Heuristic(self.context, steps) | ||
| except HeuristicException as e: | ||
| raise Error(e) | ||
|
|
||
| score_difference = heuristic.score - prev_score | ||
| prev_score = heuristic.score | ||
|
|
||
| isnan = heuristic.score != heuristic.score | ||
| if isnan or score_difference < 0: | ||
| color1 = 'r' | ||
| color2 = 'r' | ||
| elif score_difference > 0: | ||
| color1 = 'g' | ||
| color2 = 'g' | ||
| else: | ||
| color1 = 'y' | ||
| color2 = 'd' | ||
|
|
||
| summary = '<{}>running pass {}/{} on "{}"'.format( | ||
| color2, pass_info[0], max_limits[current_file_index], | ||
| test_name) | ||
| if len(options.source_files) > 1: | ||
| summary += ' [{}/{}]'.format(current_limit, overall_limit) | ||
|
|
||
| pass_text = ''.join(p for p in pass_info[1:] if p) | ||
| summary += ': {} <{}>{:+.4f}</> <{}>{}</></>\n'.format( | ||
| heuristic.summary_string, color1, score_difference, color2, | ||
| pass_text) | ||
|
|
||
| self.context.o.auto(summary) | ||
|
|
||
| heuristic_verbose_output = heuristic.verbose_output | ||
|
|
||
| if options.verbose: | ||
| self.context.o.auto(heuristic_verbose_output) | ||
|
|
||
| steps_str = str(steps) | ||
| steps_changed = steps_str != prev_steps_str | ||
| prev_steps_str = steps_str | ||
|
|
||
| # If this is the first pass, or something has changed, write a text | ||
| # file containing verbose information on the current status. | ||
| if current_limit == 0 or score_difference or steps_changed: | ||
| file_name = '-'.join( | ||
| str(s) for s in [ | ||
| 'status', test_name, '{{:0>{}}}'.format( | ||
| len(str(overall_limit))).format(current_limit), | ||
| '{:.4f}'.format(heuristic.score).replace( | ||
| '.', '_'), pass_info[1] | ||
| ] if s is not None) | ||
|
|
||
| file_name = ''.join( | ||
| c for c in file_name | ||
| if c.isalnum() or c in '()-_./ ').strip().replace( | ||
| ' ', '_').replace('/', '_') | ||
|
|
||
| output_text_path = os.path.join(options.results_directory, | ||
| '{}.txt'.format(file_name)) | ||
| with open(output_text_path, 'w') as fp: | ||
| self.context.o.auto(summary + '\n', stream=Stream(fp)) | ||
| self.context.o.auto(str(steps) + '\n', stream=Stream(fp)) | ||
| self.context.o.auto( | ||
| heuristic_verbose_output + '\n', stream=Stream(fp)) | ||
|
|
||
| output_dextIR_path = os.path.join(options.results_directory, | ||
| '{}.dextIR'.format(file_name)) | ||
| with open(output_dextIR_path, 'wb') as fp: | ||
| pickle.dump(steps, fp, protocol=pickle.HIGHEST_PROTOCOL) | ||
|
|
||
| per_pass_score.append((test_name, pass_text, | ||
| heuristic.score)) | ||
|
|
||
| if pass_info[1]: | ||
| self._all_bisect_pass_summary[pass_info[1]].append( | ||
| score_difference) | ||
|
|
||
| current_bisect_pass_summary[pass_info[1]].append( | ||
| score_difference) | ||
|
|
||
| per_pass_score_path = os.path.join( | ||
| options.results_directory, | ||
| '{}-per_pass_score.csv'.format(test_name)) | ||
|
|
||
| with open(per_pass_score_path, mode='w', newline='') as fp: | ||
| writer = csv.writer(fp, delimiter=',') | ||
| writer.writerow(['Source File', 'Pass', 'Score']) | ||
|
|
||
| for path, pass_, score in per_pass_score: | ||
| writer.writerow([path, pass_, score]) | ||
| self.context.o.blue('wrote "{}"\n'.format(per_pass_score_path)) | ||
|
|
||
| pass_summary_path = os.path.join( | ||
| options.results_directory, '{}-pass-summary.csv'.format(test_name)) | ||
|
|
||
| self._write_pass_summary(pass_summary_path, | ||
| current_bisect_pass_summary) | ||
|
|
||
| def _handle_results(self) -> ReturnCode: | ||
| options = self.context.options | ||
| pass_summary_path = os.path.join(options.results_directory, | ||
| 'overall-pass-summary.csv') | ||
|
|
||
| self._write_pass_summary(pass_summary_path, | ||
| self._all_bisect_pass_summary) | ||
| return ReturnCode.OK | ||
|
|
||
|
|
||
| def _clang_opt_bisect_build(self, opt_bisect_limits): | ||
| options = self.context.options | ||
| compiler_options = [ | ||
| '{} -mllvm -opt-bisect-limit={}'.format(options.cflags, | ||
| opt_bisect_limit) | ||
| for opt_bisect_limit in opt_bisect_limits | ||
| ] | ||
| linker_options = options.ldflags | ||
|
|
||
| try: | ||
| return run_external_build_script( | ||
| self.context, | ||
| source_files=options.source_files, | ||
| compiler_options=compiler_options, | ||
| linker_options=linker_options, | ||
| script_path=self.build_script, | ||
| executable_file=options.executable) | ||
| except BuildScriptException as e: | ||
| raise Error(e) | ||
|
|
||
| def _write_pass_summary(self, path, pass_summary): | ||
| # Get a list of tuples. | ||
| pass_summary_list = list(pass_summary.items()) | ||
|
|
||
| for i, item in enumerate(pass_summary_list): | ||
| # Add elems for the sum, min, and max of the values, as well as | ||
| # 'interestingness' which is whether any of these values are | ||
| # non-zero. | ||
| pass_summary_list[i] += (sum(item[1]), min(item[1]), max(item[1]), | ||
| any(item[1])) | ||
|
|
||
| # Split the pass name into the basic name and kind. | ||
| pass_summary_list[i] += tuple(item[0].rsplit(' on ', 1)) | ||
|
|
||
| # Sort the list by the following columns in order of precedence: | ||
| # - Is interesting (True first) | ||
| # - Sum (smallest first) | ||
| # - Number of times pass ran (largest first) | ||
| # - Kind (alphabetically) | ||
| # - Name (alphabetically) | ||
| pass_summary_list.sort( | ||
| key=lambda tup: (not tup[5], tup[2], -len(tup[1]), tup[7], tup[6])) | ||
|
|
||
| with open(path, mode='w', newline='') as fp: | ||
| writer = csv.writer(fp, delimiter=',') | ||
| writer.writerow( | ||
| ['Pass', 'Kind', 'Sum', 'Min', 'Max', 'Interesting']) | ||
|
|
||
| for (_, vals, sum_, min_, max_, interesting, name, | ||
| kind) in pass_summary_list: | ||
| writer.writerow([name, kind, sum_, min_, max_, interesting] + | ||
| vals) | ||
|
|
||
| self.context.o.blue('wrote "{}"\n'.format(path)) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| # DExTer : Debugging Experience Tester | ||
| # ~~~~~~ ~ ~~ ~ ~~ | ||
| # | ||
| # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | ||
| # See https://llvm.org/LICENSE.txt for license information. | ||
| # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | ||
|
|
||
| from dex.tools.clang_opt_bisect.Tool import Tool |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| # DExTer : Debugging Experience Tester | ||
| # ~~~~~~ ~ ~~ ~ ~~ | ||
| # | ||
| # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | ||
| # See https://llvm.org/LICENSE.txt for license information. | ||
| # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | ||
| """Help tool.""" | ||
|
|
||
| import imp | ||
| import textwrap | ||
|
|
||
| from dex.tools import ToolBase, get_tool_names, get_tools_directory, tool_main | ||
| from dex.utils.ReturnCode import ReturnCode | ||
|
|
||
|
|
||
| class Tool(ToolBase): | ||
| """Provides help info on subtools.""" | ||
|
|
||
| @property | ||
| def name(self): | ||
| return 'DExTer help' | ||
|
|
||
| @property | ||
| def _visible_tool_names(self): | ||
| return [t for t in get_tool_names() if not t.endswith('-')] | ||
|
|
||
| def add_tool_arguments(self, parser, defaults): | ||
| parser.description = Tool.__doc__ | ||
| parser.add_argument( | ||
| 'tool', | ||
| choices=self._visible_tool_names, | ||
| nargs='?', | ||
| help='name of subtool') | ||
|
|
||
| def handle_options(self, defaults): | ||
| pass | ||
|
|
||
| @property | ||
| def _default_text(self): | ||
| s = '\n<b>The following subtools are available:</>\n\n' | ||
| tools_directory = get_tools_directory() | ||
| for tool_name in sorted(self._visible_tool_names): | ||
| internal_name = tool_name.replace('-', '_') | ||
| module_info = imp.find_module(internal_name, [tools_directory]) | ||
| tool_doc = imp.load_module(internal_name, | ||
| *module_info).Tool.__doc__ | ||
| tool_doc = tool_doc.strip() if tool_doc else '' | ||
| tool_doc = textwrap.fill(' '.join(tool_doc.split()), 80) | ||
| s += '<g>{}</>\n{}\n\n'.format(tool_name, tool_doc) | ||
| return s | ||
|
|
||
| def go(self) -> ReturnCode: | ||
| if self.context.options.tool is None: | ||
| self.context.o.auto(self._default_text) | ||
| return ReturnCode.OK | ||
|
|
||
| tool_name = self.context.options.tool.replace('-', '_') | ||
| tools_directory = get_tools_directory() | ||
| module_info = imp.find_module(tool_name, [tools_directory]) | ||
| module = imp.load_module(tool_name, *module_info) | ||
| return tool_main(self.context, module.Tool(self.context), ['--help']) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| # DExTer : Debugging Experience Tester | ||
| # ~~~~~~ ~ ~~ ~ ~~ | ||
| # | ||
| # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | ||
| # See https://llvm.org/LICENSE.txt for license information. | ||
| # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | ||
|
|
||
| from dex.tools.help.Tool import Tool |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| # DExTer : Debugging Experience Tester | ||
| # ~~~~~~ ~ ~~ ~ ~~ | ||
| # | ||
| # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | ||
| # See https://llvm.org/LICENSE.txt for license information. | ||
| # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | ||
| """List debuggers tool.""" | ||
|
|
||
| from dex.debugger.Debuggers import add_debugger_tool_base_arguments | ||
| from dex.debugger.Debuggers import handle_debugger_tool_base_options | ||
| from dex.debugger.Debuggers import Debuggers | ||
| from dex.tools import ToolBase | ||
| from dex.utils import Timer | ||
| from dex.utils.Exceptions import DebuggerException, Error | ||
| from dex.utils.ReturnCode import ReturnCode | ||
|
|
||
|
|
||
| class Tool(ToolBase): | ||
| """List all of the potential debuggers that DExTer knows about and whether | ||
| there is currently a valid interface available for them. | ||
| """ | ||
|
|
||
| @property | ||
| def name(self): | ||
| return 'DExTer list debuggers' | ||
|
|
||
| def add_tool_arguments(self, parser, defaults): | ||
| parser.description = Tool.__doc__ | ||
| add_debugger_tool_base_arguments(parser, defaults) | ||
|
|
||
| def handle_options(self, defaults): | ||
| handle_debugger_tool_base_options(self.context, defaults) | ||
|
|
||
| def go(self) -> ReturnCode: | ||
| with Timer('list debuggers'): | ||
| try: | ||
| Debuggers(self.context).list() | ||
| except DebuggerException as e: | ||
| raise Error(e) | ||
| return ReturnCode.OK |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| # DExTer : Debugging Experience Tester | ||
| # ~~~~~~ ~ ~~ ~ ~~ | ||
| # | ||
| # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | ||
| # See https://llvm.org/LICENSE.txt for license information. | ||
| # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | ||
|
|
||
| from dex.tools.list_debuggers.Tool import Tool |