405 changes: 405 additions & 0 deletions debuginfo-tests/dexter/dex/debugger/dbgeng/control.py

Large diffs are not rendered by default.

163 changes: 163 additions & 0 deletions debuginfo-tests/dexter/dex/debugger/dbgeng/dbgeng.py
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)
80 changes: 80 additions & 0 deletions debuginfo-tests/dexter/dex/debugger/dbgeng/probe_process.py
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
185 changes: 185 additions & 0 deletions debuginfo-tests/dexter/dex/debugger/dbgeng/setup.py
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)
499 changes: 499 additions & 0 deletions debuginfo-tests/dexter/dex/debugger/dbgeng/symbols.py

Large diffs are not rendered by default.

98 changes: 98 additions & 0 deletions debuginfo-tests/dexter/dex/debugger/dbgeng/symgroup.py
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))))
200 changes: 200 additions & 0 deletions debuginfo-tests/dexter/dex/debugger/dbgeng/sysobjs.py
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))
47 changes: 47 additions & 0 deletions debuginfo-tests/dexter/dex/debugger/dbgeng/utils.py
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)
244 changes: 244 additions & 0 deletions debuginfo-tests/dexter/dex/debugger/lldb/LLDB.py
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,
)
8 changes: 8 additions & 0 deletions debuginfo-tests/dexter/dex/debugger/lldb/__init__.py
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
224 changes: 224 additions & 0 deletions debuginfo-tests/dexter/dex/debugger/visualstudio/VisualStudio.py
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'
9 changes: 9 additions & 0 deletions debuginfo-tests/dexter/dex/debugger/visualstudio/__init__.py
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
16 changes: 16 additions & 0 deletions debuginfo-tests/dexter/dex/dextIR/BuilderIR.py
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
14 changes: 14 additions & 0 deletions debuginfo-tests/dexter/dex/dextIR/DebuggerIR.py
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
129 changes: 129 additions & 0 deletions debuginfo-tests/dexter/dex/dextIR/DextIR.py
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()
16 changes: 16 additions & 0 deletions debuginfo-tests/dexter/dex/dextIR/FrameIR.py
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
45 changes: 45 additions & 0 deletions debuginfo-tests/dexter/dex/dextIR/LocIR.py
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
117 changes: 117 additions & 0 deletions debuginfo-tests/dexter/dex/dextIR/ProgramState.py
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
103 changes: 103 additions & 0 deletions debuginfo-tests/dexter/dex/dextIR/StepIR.py
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)
38 changes: 38 additions & 0 deletions debuginfo-tests/dexter/dex/dextIR/ValueIR.py
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))

17 changes: 17 additions & 0 deletions debuginfo-tests/dexter/dex/dextIR/__init__.py
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
497 changes: 497 additions & 0 deletions debuginfo-tests/dexter/dex/heuristic/Heuristic.py

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions debuginfo-tests/dexter/dex/heuristic/__init__.py
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
207 changes: 207 additions & 0 deletions debuginfo-tests/dexter/dex/tools/Main.py
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
148 changes: 148 additions & 0 deletions debuginfo-tests/dexter/dex/tools/TestToolBase.py
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
135 changes: 135 additions & 0 deletions debuginfo-tests/dexter/dex/tools/ToolBase.py
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
10 changes: 10 additions & 0 deletions debuginfo-tests/dexter/dex/tools/__init__.py
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
286 changes: 286 additions & 0 deletions debuginfo-tests/dexter/dex/tools/clang_opt_bisect/Tool.py
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))
8 changes: 8 additions & 0 deletions debuginfo-tests/dexter/dex/tools/clang_opt_bisect/__init__.py
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
61 changes: 61 additions & 0 deletions debuginfo-tests/dexter/dex/tools/help/Tool.py
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'])
8 changes: 8 additions & 0 deletions debuginfo-tests/dexter/dex/tools/help/__init__.py
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
40 changes: 40 additions & 0 deletions debuginfo-tests/dexter/dex/tools/list_debuggers/Tool.py
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
8 changes: 8 additions & 0 deletions debuginfo-tests/dexter/dex/tools/list_debuggers/__init__.py
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
Loading