Skip to content

Commit

Permalink
Remove watch commands, allow arbitrary expressions in Program State (#37
Browse files Browse the repository at this point in the history
)

* Allow expression evaluation on all frames in VS

* Remove unused print

* Remove unused string function

* Rename local_vars -> watches in program state

* Reinsert useful watches, fix formatting, reinsert ValueIR.str

* Add frame index arg to base evaluate_expression method

* Add return type annotation to all instances of evaluate_expression

* Minor style changes
  • Loading branch information
SLTozer authored and jmorse committed Jul 19, 2019
1 parent 18fcf75 commit 922295e
Show file tree
Hide file tree
Showing 9 changed files with 83 additions and 44 deletions.
4 changes: 4 additions & 0 deletions dex/command/CommandBase.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"""

import abc
from typing import List

class CommandBase(object, metaclass=abc.ABCMeta):
def __init__(self):
Expand All @@ -39,6 +40,9 @@ def get_name():
return __class__.__name__
"""

def get_watches(self) -> List[str]:
return []

@abc.abstractmethod
def eval(self):
pass
Expand Down
7 changes: 7 additions & 0 deletions dex/command/commands/DexExpectProgramState.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
during execution.
"""

from itertools import chain

from dex.command.CommandBase import CommandBase
from dex.dextIR import ProgramState, SourceLocation, StackFrame, DextIR

Expand Down Expand Up @@ -62,6 +64,11 @@ def __init__(self, *args, **kwargs):
def get_name():
return __class__.__name__

def get_watches(self):
frame_expects = chain.from_iterable(frame.watches
for frame in self.expected_program_state.frames)
return set(frame_expects)

def eval(self, step_collection: DextIR) -> bool:
for step in step_collection.steps:
if self.expected_program_state.match(step.program_state):
Expand Down
6 changes: 5 additions & 1 deletion dex/command/commands/DexExpectWatchValue.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@ def __init__(self, *args, **kwargs):

super(DexExpectWatchValue, self).__init__()


def get_watches(self):
return [self.expression]

@property
def line_range(self):
return list(range(self._from_line, self._to_line + 1))
Expand Down Expand Up @@ -179,7 +183,7 @@ def eval(self, step_collection):

if (loc.path == self.path and loc.lineno in self.line_range):
try:
watch = step.watches[self.expression]
watch = step.program_state.frames[0].watches[self.expression]
except KeyError:
pass
else:
Expand Down
11 changes: 8 additions & 3 deletions dex/debugger/DebuggerBase.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
import traceback

from dex.command import get_command_object
from dex.dextIR import DebuggerIR
from dex.dextIR import DebuggerIR, ValueIR
from dex.utils.Exceptions import DebuggerException
from dex.utils.Exceptions import NotYetLoadedDebuggerException
from dex.utils.ReturnCode import ReturnCode
Expand All @@ -42,6 +42,7 @@ def __init__(self, context, step_collection):
self._interface = None
self.has_loaded = False
self._loading_error = NotYetLoadedDebuggerException()
self.watches = set()

try:
self._interface = self._load_interface()
Expand Down Expand Up @@ -110,7 +111,7 @@ def add_breakpoints(self):

def _update_step_watches(self, step_info):
loc = step_info.current_location
watch_cmds = ['DexWatch', 'DexUnreachable', 'DexExpectStepOrder']
watch_cmds = ['DexUnreachable', 'DexExpectStepOrder']
towatch = chain.from_iterable(self.steps.commands[x]
for x in watch_cmds
if x in self.steps.commands)
Expand All @@ -136,6 +137,10 @@ def start(self):
self.steps.clear_steps()
self.launch()

for command in chain.from_iterable(self.steps.commands.values()):
command_obj = get_command_object(command)
self.watches.update(command_obj.get_watches())

max_steps = self.context.options.max_steps
for _ in range(max_steps):
while self.is_running:
Expand Down Expand Up @@ -227,5 +232,5 @@ def frames_below_main(self):
pass

@abc.abstractmethod
def evaluate_expression(self, expression):
def evaluate_expression(self, expression, frame_idx=0) -> ValueIR:
pass
13 changes: 8 additions & 5 deletions dex/debugger/lldb/LLDB.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,9 +177,11 @@ def get_step_info(self):
state_frame = StackFrame(function=frame.function,
is_inlined=frame.is_inlined,
location=SourceLocation(**loc_dict),
local_vars={})
for expr in sb_frame.GetVariables(True, True, True, True):
state_frame.local_vars[expr.name] = expr.value
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:
Expand All @@ -205,8 +207,9 @@ def is_finished(self):
def frames_below_main(self):
return ['__scrt_common_main_seh', '__libc_start_main']

def evaluate_expression(self, expression):
result = self._thread.GetFrameAtIndex(0).EvaluateExpression(expression)
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
Expand Down
37 changes: 26 additions & 11 deletions dex/debugger/visualstudio/VisualStudio.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,11 @@
import imp
import os
import sys
from itertools import chain

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 LoadDebuggerException
from dex.utils.Exceptions import Error, LoadDebuggerException
from dex.utils.ReturnCode import ReturnCode


Expand Down Expand Up @@ -138,14 +137,25 @@ 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 sf in stackframes:

for idx, sf in enumerate(stackframes):
frame = FrameIR(
function=self._sanitize_function_name(sf.FunctionName),
is_inlined=sf.FunctionName.startswith('[Inline Frame]'),
Expand All @@ -155,19 +165,22 @@ def get_step_info(self):
if any(name in fname for name in self.frames_below_main):
break

frames.append(frame)

state_frame = StackFrame(function=frame.function,
is_inlined=frame.is_inlined,
local_vars={})
for expr in chain(sf.Arguments, sf.Locals):
state_frame.local_vars[expr.Name] = expr.Value
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)
frames[0].loc = loc
state_frames[0].location = SourceLocation(**self._location)

reason = StopReason.BREAKPOINT
if loc.path is None: # pylint: disable=no-member
Expand All @@ -194,8 +207,10 @@ def frames_below_main(self):
'__tmainCRTStartup', 'mainCRTStartup'
]

def evaluate_expression(self, expression):
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 [
Expand Down
38 changes: 18 additions & 20 deletions dex/dextIR/ProgramState.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ 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

Expand All @@ -61,52 +64,55 @@ def __init__(self,
function: str = None,
is_inlined: bool = None,
location: SourceLocation = None,
local_vars: OrderedDict = None):
if local_vars is None:
local_vars = {}
watches: OrderedDict = None):
if watches is None:
watches = {}

self.function = function
self.is_inlined = is_inlined
self.location = location
self.local_vars = local_vars
self.watches = watches

def __str__(self):
return '{}{}: {} | {}'.format(
self.function,
' (inlined)' if self.is_inlined else '',
self.location,
self.local_vars)
{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.local_vars:
for name in iter(self.local_vars):
if self.watches:
for name in iter(self.watches):
try:
if other.local_vars[name] != self.local_vars[name]:
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,
global_vars: OrderedDict = None):
def __init__(self, frames: List[StackFrame] = None):
self.frames = frames
self.global_vars = global_vars

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

Expand All @@ -118,12 +124,4 @@ def match(self, other) -> bool:
except (IndexError, KeyError):
return False

if self.global_vars:
for name in iter(self.global_vars):
try:
if other.global_variables[name] != self.global_vars[name]:
return False
except IndexError:
return False

return True
3 changes: 3 additions & 0 deletions dex/dextIR/ValueIR.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,6 @@ def __init__(self,
self.error_string = error_string
self.is_optimized_away = is_optimized_away
self.is_irretrievable = is_irretrievable

def __str__(self):
return self.value
8 changes: 4 additions & 4 deletions tests/nostdlib/gcd/test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,22 @@ DexExpectProgramState({
'location': {
'lineno': 11
},
'local_vars': {
'watches': {
'lhs': '37', 'rhs': '0'
}
},
{
'local_vars': {
'watches': {
'lhs': '111', 'rhs': '37'
}
},
{
'local_vars': {
'watches': {
'lhs': '259', 'rhs': '111'
}
},
{
'local_vars': {
'watches': {
'lhs': '111', 'rhs': '259'
}
}
Expand Down

0 comments on commit 922295e

Please sign in to comment.