* [DexLimitSteps](Commands.md#DexLimitSteps)
* [DexLabel](Commands.md#DexLabel)
* [DexWatch](Commands.md#DexWatch)
+* [DexDeclareAddress](Commands.md#DexDeclareAddress)
* [DexDeclareFile](Commands.md#DexDeclareFile)
* [DexFinishTest](Commands.md#DexFinishTest)
This command does not contribute to the heuristic score.
----
+## DexDeclareAddress
+ DexDeclareAddress(declared_address, expr, **on_line[, **hit_count])
+
+ Args:
+ declared_address (str): The unique name of an address, which can be used
+ in DexExpectWatch-commands.
+ expr (str): An expression to evaluate to provide the value of this
+ address.
+ on_line (int): The line at which the value of the expression will be
+ assigned to the address.
+ hit_count (int): If provided, reads the value of the source expression
+ after the line has been stepped onto the given number
+ of times ('hit_count = 0' gives default behaviour).
+
+### Description
+Declares a variable that can be used in DexExpectWatch- commands as an expected
+value by using the `address(str[, int])` function. This is primarily
+useful for checking the values of pointer variables, which are generally
+determined at run-time (and so cannot be consistently matched by a hard-coded
+expected value), but may be consistent relative to each other. An example use of
+this command is as follows, using a set of pointer variables "foo", "bar", and
+"baz":
+
+ DexDeclareAddress('my_addr', 'bar', on_line=12)
+ DexExpectWatchValue('foo', address('my_addr'), on_line=10)
+ DexExpectWatchValue('bar', address('my_addr'), on_line=12)
+ DexExpectWatchValue('baz', address('my_addr', 16), on_line=14)
+
+On the first line, we declare the name of our variable 'my_addr'. This name must
+be unique (the same name cannot be declared twice), and attempting to reference
+an undeclared variable with `address` will fail. The value of the address
+variable will be assigned as the value of 'bar' when line 12 is first stepped
+on.
+
+On lines 2-4, we use the `address` function to refer to our variable. The first
+usage occurs on line 10, before the line where 'my_addr' is assigned its value;
+this is a valid use, as we assign the address value and check for correctness
+after gathering all debug information for the test. Thus the first test command
+will pass if 'foo' on line 10 has the same value as 'bar' on line 12.
+
+The second command will pass iff 'bar' is available at line 12 - even if the
+variable and lines are identical in DexDeclareAddress and DexExpectWatchValue,
+the latter will still expect a valid value. Similarly, if the variable for a
+DexDeclareAddress command is not available at the given line, any test against
+that address will fail.
+
+The `address` function also accepts an optional integer argument representing an
+offset (which may be negative) to be applied to the address value, so
+`address('my_addr', 16)` resolves to `my_addr + 16`. In the above example, this
+means that we expect `baz == bar + 16`.
+
+### Heuristic
+This command does not contribute to the heuristic score.
+
+----
## DexDeclareFile
DexDeclareFile(declared_file)
from dex.command.CommandBase import CommandBase
from dex.command.commands.DexDeclareFile import DexDeclareFile
+from dex.command.commands.DexDeclareAddress import DexDeclareAddress
from dex.command.commands.DexExpectProgramState import DexExpectProgramState
from dex.command.commands.DexExpectStepKind import DexExpectStepKind
from dex.command.commands.DexExpectStepOrder import DexExpectStepOrder
from dex.command.commands.DexExpectWatchType import DexExpectWatchType
from dex.command.commands.DexExpectWatchValue import DexExpectWatchValue
+from dex.command.commands.DexExpectWatchBase import AddressExpression, DexExpectWatchBase
from dex.command.commands.DexLabel import DexLabel
from dex.command.commands.DexLimitSteps import DexLimitSteps
from dex.command.commands.DexFinishTest import DexFinishTest
{ name (str): command (class) }
"""
return {
+ DexDeclareAddress.get_name() : DexDeclareAddress,
DexDeclareFile.get_name() : DexDeclareFile,
DexExpectProgramState.get_name() : DexExpectProgramState,
DexExpectStepKind.get_name() : DexExpectStepKind,
return valid_commands
-def _build_command(command_type, labels, raw_text: str, path: str, lineno: str) -> CommandBase:
+def _build_command(command_type, labels, addresses, raw_text: str, path: str, lineno: str) -> CommandBase:
"""Build a command object from raw text.
This function will call eval().
return line
raise format_unresolved_label_err(label_name, raw_text, path, lineno)
+ def get_address_object(address_name: str, offset: int=0):
+ if address_name not in addresses:
+ raise format_undeclared_address_err(address_name, raw_text, path, lineno)
+ return AddressExpression(address_name, offset)
+
valid_commands = _merge_subcommands(
command_type.get_name(), {
'ref': label_to_line,
+ 'address': get_address_object,
command_type.get_name(): command_type,
})
err.info = f'Unresolved label: \'{label}\''
return err
+def format_undeclared_address_err(address: str, src: str, filename: str, lineno) -> CommandParseError:
+ err = CommandParseError()
+ err.src = src
+ err.caret = '' # Don't bother trying to point to the bad address.
+ err.filename = filename
+ err.lineno = lineno
+ err.info = f'Undeclared address: \'{address}\''
+ return err
def format_parse_err(msg: str, path: str, lines: list, point: TextPoint) -> CommandParseError:
err = CommandParseError()
raise err
labels[label.eval()] = label.get_line()
+def add_address(addresses, address, cmd_path, cmd_lineno):
+ # Enforce unique address variables.
+ address_name = address.get_address_name()
+ if address_name in addresses:
+ err = CommandParseError()
+ err.info = f'Found duplicate address: \'{address_name}\''
+ err.lineno = cmd_lineno
+ err.filename = cmd_path
+ err.src = address.raw_text
+ # Don't both trying to point to it since we're only printing the raw
+ # command, which isn't much text.
+ err.caret = ''
+ raise err
+ addresses.append(address_name)
def _find_all_commands_in_file(path, file_lines, valid_commands, source_root_dir):
labels = {} # dict of {name: line}.
+ addresses = [] # list of addresses.
+ address_resolutions = {}
cmd_path = path
declared_files = set()
commands = defaultdict(dict)
command = _build_command(
valid_commands[command_name],
labels,
+ addresses,
raw_text,
cmd_path,
cmd_point.get_lineno(),
else:
if type(command) is DexLabel:
add_line_label(labels, command, path, cmd_point.get_lineno())
+ elif type(command) is DexDeclareAddress:
+ add_address(addresses, command, path, cmd_point.get_lineno())
elif type(command) is DexDeclareFile:
cmd_path = command.declared_file
if not os.path.isabs(cmd_path):
--- /dev/null
+# 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
+"""Commmand sets the path for all following commands to 'declared_file'.
+"""
+
+import os
+
+from dex.command.CommandBase import CommandBase, StepExpectInfo
+
+class DexDeclareAddress(CommandBase):
+ def __init__(self, addr_name, expression, **kwargs):
+
+ if not isinstance(addr_name, str):
+ raise TypeError('invalid argument type')
+
+ self.addr_name = addr_name
+ self.expression = expression
+ self.on_line = kwargs.pop('on_line')
+ self.hit_count = kwargs.pop('hit_count', 0)
+
+ self.address_resolutions = None
+
+ super(DexDeclareAddress, self).__init__()
+
+ @staticmethod
+ def get_name():
+ return __class__.__name__
+
+ def get_watches(self):
+ return [StepExpectInfo(self.expression, self.path, 0, range(self.on_line, self.on_line + 1))]
+
+ def get_address_name(self):
+ return self.addr_name
+
+ def eval(self, step_collection):
+ assert os.path.exists(self.path)
+ self.address_resolutions[self.get_address_name()] = None
+ for step in step_collection.steps:
+ loc = step.current_location
+
+ if (loc.path and os.path.exists(loc.path) and
+ os.path.samefile(loc.path, self.path) and
+ loc.lineno == self.on_line):
+ if self.hit_count > 0:
+ self.hit_count -= 1
+ continue
+ try:
+ watch = step.program_state.frames[0].watches[self.expression]
+ except KeyError:
+ pass
+ else:
+ hex_val = int(watch.value, 16)
+ self.address_resolutions[self.get_address_name()] = hex_val
+ break
import abc
import difflib
import os
+import math
from collections import namedtuple
from dex.command.CommandBase import CommandBase, StepExpectInfo
from dex.command.StepValueInfo import StepValueInfo
+class AddressExpression(object):
+ def __init__(self, name, offset=0):
+ self.name = name
+ self.offset = offset
+ def is_resolved(self, resolutions):
+ return self.name in resolutions
+
+ # Given the resolved value of the address, resolve the final value of
+ # this expression.
+ def resolved_value(self, resolutions):
+ if not self.name in resolutions or resolutions[self.name] is None:
+ return None
+ # Technically we should fill(8) if we're debugging on a 32bit architecture?
+ return format_address(resolutions[self.name] + self.offset)
+
+def format_address(value, address_width=64):
+ return "0x" + hex(value)[2:].zfill(math.ceil(address_width/4))
+
+def resolved_value(value, resolutions):
+ return value.resolved_value(resolutions) if isinstance(value, AddressExpression) else value
class DexExpectWatchBase(CommandBase):
def __init__(self, *args, **kwargs):
raise TypeError('expected at least two args')
self.expression = args[0]
- self.values = [str(arg) for arg in args[1:]]
+ self.values = [arg if isinstance(arg, AddressExpression) else str(arg) for arg in args[1:]]
try:
on_line = kwargs.pop('on_line')
self._from_line = on_line
# unexpected value.
self.unexpected_watches = []
+ # List of StepValueInfos for all observed watches that were not
+ # invalid, irretrievable, or optimized out (combines expected and
+ # unexpected).
+ self.observed_watches = []
+
+ # dict of address names to their final resolved values, None until it
+ # gets assigned externally.
+ self.address_resolutions = None
+
super(DexExpectWatchBase, self).__init__()
+ def resolve_value(self, value):
+ return value.resolved_value(self.address_resolutions) if isinstance(value, AddressExpression) else value
+
+ def describe_value(self, value):
+ if isinstance(value, AddressExpression):
+ offset = ""
+ if value.offset > 0:
+ offset = f"+{value.offset}"
+ elif value.offset < 0:
+ offset = str(value.offset)
+ desc = f"address '{value.name}'{offset}"
+ if self.resolve_value(value) is not None:
+ desc += f" ({self.resolve_value(value)})"
+ return desc
+ return value
def get_watches(self):
return [StepExpectInfo(self.expression, self.path, 0, range(self._from_line, self._to_line + 1))]
@property
def missing_values(self):
- return sorted(list(self._missing_values))
+ return sorted(list(self.describe_value(v) for v in self._missing_values))
@property
def encountered_values(self):
- return sorted(list(set(self.values) - self._missing_values))
+ return sorted(list(set(self.describe_value(v) for v in set(self.values) - self._missing_values)))
@abc.abstractmethod
def _get_expected_field(self, watch):
self.irretrievable_watches.append(step_info)
return
- if step_info.expected_value not in self.values:
+ # Check to see if this value matches with a resolved address.
+ matching_address = None
+ for v in self.values:
+ if (isinstance(v, AddressExpression) and
+ v.name in self.address_resolutions and
+ self.resolve_value(v) == step_info.expected_value):
+ matching_address = v
+ break
+
+ # If this is not an expected value, either a direct value or an address,
+ # then this is an unexpected watch.
+ if step_info.expected_value not in self.values and matching_address is None:
self.unexpected_watches.append(step_info)
return
self.expected_watches.append(step_info)
+ value_to_remove = matching_address if matching_address is not None else step_info.expected_value
try:
- self._missing_values.remove(step_info.expected_value)
+ self._missing_values.remove(value_to_remove)
except KeyError:
pass
value_change_watches.append(watch)
prev_value = watch.expected_value
+ resolved_values = [self.resolve_value(v) for v in self.values]
self.misordered_watches = self._check_watch_order(
value_change_watches, [
- v for v in self.values if v in
+ v for v in resolved_values if v in
[w.expected_value for w in self.expected_watches]
])
import os
from itertools import groupby
from dex.command.StepValueInfo import StepValueInfo
+from dex.command.commands.DexExpectWatchBase import format_address
PenaltyCommand = namedtuple('PenaltyCommand', ['pen_dict', 'max_penalty'])
def __init__(self, context, steps):
self.context = context
self.penalties = {}
+ self.address_resolutions = {}
worst_penalty = max([
self.penalty_variable_optimized, self.penalty_irretrievable,
self.penalty_missing_step, self.penalty_misordered_steps
])
+ # Before evaluating scoring commands, evaluate address values.
+ try:
+ for command in steps.commands['DexDeclareAddress']:
+ command.address_resolutions = self.address_resolutions
+ command.eval(steps)
+ except KeyError:
+ pass
+
# Get DexExpectWatchType results.
try:
for command in steps.commands['DexExpectWatchType']:
# Get DexExpectWatchValue results.
try:
for command in steps.commands['DexExpectWatchValue']:
+ command.address_resolutions = self.address_resolutions
command.eval(steps)
maximum_possible_penalty = min(3, len(
command.values)) * worst_penalty
@property
def verbose_output(self): # noqa
string = ''
+
+ # Add address resolutions if present.
+ if self.address_resolutions:
+ if self.resolved_addresses:
+ string += '\nResolved Addresses:\n'
+ for addr, res in self.resolved_addresses.items():
+ string += f" '{addr}': {res}\n"
+ if self.unresolved_addresses:
+ string += '\n'
+ string += f'Unresolved Addresses:\n {self.unresolved_addresses}\n'
+
string += ('\n')
for command in sorted(self.penalties):
pen_cmd = self.penalties[command]
return string
@property
+ def resolved_addresses(self):
+ return {addr: format_address(res) for addr, res in self.address_resolutions.items() if res is not None}
+
+ @property
+ def unresolved_addresses(self):
+ return [addr for addr, res in self.address_resolutions.items() if res is None]
+
+ @property
def penalty_variable_optimized(self):
return self.context.options.penalty_variable_optimized
--- /dev/null
+// Purpose:
+// Test that when a \DexDeclareAddress never resolves to a value, it is
+// counted as a missing value in any \DexExpectWatchValues.
+//
+// REQUIRES: system-linux
+//
+// RUN: not %dexter_regression_test -- %s | FileCheck %s
+// CHECK: missing_dex_address.cpp
+
+int main() {
+ int *x = nullptr;
+ x = new int(5); // DexLabel('start_line')
+ if (false) {
+ (void)0; // DexLabel('unreachable')
+ }
+ delete x; // DexLabel('end_line')
+}
+
+// DexDeclareAddress('x', 'x', on_line=ref('unreachable'))
+// DexExpectWatchValue('x', 0, address('x'), from_line=ref('start_line'), to_line=ref('end_line'))
--- /dev/null
+// Purpose:
+// Test that a \DexDeclareAddress value can have its value defined after
+// the first reference to that value.
+//
+// REQUIRES: system-linux
+//
+// RUN: %dexter_regression_test -- %s | FileCheck %s
+// CHECK: address_after_ref.cpp
+
+int main() {
+ int *x = new int(5);
+ int *y = x; // DexLabel('first_line')
+ delete x; // DexLabel('last_line')
+}
+
+// DexDeclareAddress('y', 'y', on_line=ref('last_line'))
+// DexExpectWatchValue('x', address('y'), on_line=ref('first_line'))
--- /dev/null
+// Purpose:
+// Test that a \DexDeclareAddress command can be passed 'hit_count' as an
+// optional keyword argument that captures the value of the given
+// expression after the target line has been stepped on a given number of
+// times.
+//
+// REQUIRES: system-linux
+//
+// RUN: %dexter_regression_test -- %s | FileCheck %s
+// CHECK: address_hit_count.cpp
+
+int main() {
+ int *x = new int[3];
+ for (int *y = x; y < x + 3; ++y)
+ *y = 0; // DexLabel('test_line')
+ delete x;
+}
+
+// DexDeclareAddress('y', 'y', on_line=ref('test_line'), hit_count=2)
+// DexExpectWatchValue('y', address('y', -8), address('y', -4), address('y'), on_line=ref('test_line'))
--- /dev/null
+// Purpose:
+// Test that a \DexDeclareAddress value can be used to compare the
+// addresses of two local variables that refer to the same address.
+//
+// REQUIRES: system-linux
+//
+// RUN: %dexter_regression_test -- %s | FileCheck %s
+// CHECK: expression_address.cpp
+
+int main() {
+ int x = 5;
+ int &y = x;
+ x = 3; // DexLabel('test_line')
+}
+
+// DexDeclareAddress('x_addr', '&x', on_line=ref('test_line'))
+// DexExpectWatchValue('&x', address('x_addr'), on_line=ref('test_line'))
+// DexExpectWatchValue('&y', address('x_addr'), on_line=ref('test_line'))
--- /dev/null
+// Purpose:
+// Test that a \DexDeclareAddress value can be used to compare two equal
+// pointer variables.
+//
+// REQUIRES: system-linux
+//
+// RUN: %dexter_regression_test -- %s | FileCheck %s
+// CHECK: identical_address.cpp
+
+int main() {
+ int *x = new int(5);
+ int *y = x;
+ delete x; // DexLabel('test_line')
+}
+
+// DexDeclareAddress('x', 'x', on_line=ref('test_line'))
+// DexExpectWatchValue('x', address('x'), on_line=ref('test_line'))
+// DexExpectWatchValue('y', address('x'), on_line=ref('test_line'))
--- /dev/null
+// Purpose:
+// Test that multiple \DexDeclareAddress references that point to different
+// addresses can be used within a single \DexExpectWatchValue.
+//
+// REQUIRES: system-linux
+//
+// RUN: %dexter_regression_test -- %s | FileCheck %s
+// CHECK: multiple_address.cpp
+
+int main() {
+ int *x = new int(5);
+ int *y = new int(4);
+ int *z = x;
+ *z = 0; // DexLabel('start_line')
+ z = y;
+ *z = 0;
+ delete x; // DexLabel('end_line')
+ delete y;
+}
+
+// DexDeclareAddress('x', 'x', on_line=ref('start_line'))
+// DexDeclareAddress('y', 'y', on_line=ref('start_line'))
+// DexExpectWatchValue('z', address('x'), address('y'), from_line=ref('start_line'), to_line=ref('end_line'))
+// DexExpectWatchValue('*z', 5, 0, 4, 0, from_line=ref('start_line'), to_line=ref('end_line'))
--- /dev/null
+// Purpose:
+// Test that a \DexDeclareAddress value can be used to compare two pointer
+// variables that have a fixed offset between them.
+//
+// REQUIRES: system-linux
+//
+// RUN: %dexter_regression_test -- %s | FileCheck %s
+// CHECK: offset_address.cpp
+
+int main() {
+ int *x = new int[5];
+ int *y = x + 3;
+ delete x; // DexLabel('test_line')
+}
+
+// DexDeclareAddress('x', 'x', on_line=ref('test_line'))
+// DexExpectWatchValue('x', address('x'), on_line=ref('test_line'))
+// DexExpectWatchValue('y', address('x', 12), on_line=ref('test_line'))
--- /dev/null
+// Purpose:
+// Test that a \DexDeclareAddress value can be used to check the change in
+// value of a variable over time, relative to its initial value.
+//
+// REQUIRES: system-linux
+//
+// RUN: %dexter_regression_test -- %s | FileCheck %s
+// CHECK: self_comparison.cpp
+
+int main() {
+ int *x = new int[3];
+ for (int *y = x; y < x + 3; ++y)
+ *y = 0; // DexLabel('test_line')
+ delete x;
+}
+
+// DexDeclareAddress('y', 'y', on_line=ref('test_line'))
+// DexExpectWatchValue('y', address('y'), address('y', 4), address('y', 8), on_line=ref('test_line'))
--- /dev/null
+// Purpose:
+// Test that address values in a \DexExpectWatchValue are printed with
+// their address name along with the address' resolved value (if any), and
+// that when verbose output is enabled the complete map of resolved
+// addresses and list of unresolved addresses will also be printed.
+//
+// Note: Currently "misordered result" is the only penalty that does not
+// display the address properly; if it is implemented, this test should be
+// updated.
+//
+// REQUIRES: system-linux
+//
+// RUN: not %dexter_regression_test -v -- %s | FileCheck %s
+
+// CHECK: Resolved Addresses:
+// CHECK-NEXT: 'x_2': 0x[[X2_VAL:[0-9a-f]+]]
+// CHECK-NEXT: 'y': 0x[[Y_VAL:[0-9a-f]+]]
+// CHECK: Unresolved Addresses:
+// CHECK-NEXT: ['x_1']
+
+// CHECK-LABEL: [x] ExpectValue
+// CHECK: expected encountered watches:
+// CHECK-NEXT: address 'x_2' (0x[[X2_VAL]])
+// CHECK: missing values:
+// CHECK-NEXT: address 'x_1'
+
+// CHECK-LABEL: [z] ExpectValue
+// CHECK: expected encountered watches:
+// CHECK-NEXT: address 'x_2' (0x[[X2_VAL]])
+// CHECK-NEXT: address 'y' (0x[[Y_VAL]])
+// CHECK: misordered result:
+// CHECK-NEXT: step 4 (0x[[Y_VAL]])
+// CHECK-NEXT: step 5 (0x[[X2_VAL]])
+
+int main() {
+ int *x = new int(5);
+ int *y = new int(4);
+ if (false) {
+ (void)0; // DexLabel('unreachable')
+ }
+ int *z = y;
+ z = x; // DexLabel('start_line')
+ delete y;
+ delete x; // DexLabel('end_line')
+}
+
+// DexDeclareAddress('x_1', 'x', on_line=ref('unreachable'))
+// DexDeclareAddress('x_2', 'x', on_line=ref('end_line'))
+// DexDeclareAddress('y', 'y', on_line=ref('start_line'))
+// DexExpectWatchValue('x', address('x_1'), address('x_2'), from_line=ref('start_line'), to_line=ref('end_line'))
+// DexExpectWatchValue('z', address('x_2'), address('y'), from_line=ref('start_line'), to_line=ref('end_line'))
--- /dev/null
+// Purpose:
+// Check that declaring duplicate addresses gives a useful error message.
+//
+// RUN: not %dexter_regression_test -v -- %s | FileCheck %s --match-full-lines
+
+
+int main() {
+ int *result = new int(0);
+ delete result; // DexLabel('test_line')
+}
+
+// CHECK: parser error:{{.*}}err_duplicate_address.cpp([[# @LINE + 4]]): Found duplicate address: 'oops'
+// CHECK-NEXT: {{Dex}}DeclareAddress('oops', 'result', on_line=ref('test_line'))
+
+// DexDeclareAddress('oops', 'result', on_line=ref('test_line'))
+// DexDeclareAddress('oops', 'result', on_line=ref('test_line'))
--- /dev/null
+// Purpose:
+// Check that using an undeclared address gives a useful error message.
+//
+// RUN: not %dexter_regression_test -v -- %s | FileCheck %s --match-full-lines
+
+
+int main() {
+ int *result = new int(0);
+ delete result; // DexLabel('test_line')
+}
+
+
+// CHECK: parser error:{{.*}}err_undeclared_addr.cpp([[# @LINE + 3]]): Undeclared address: 'result'
+// CHECK-NEXT: {{Dex}}ExpectWatchValue('result', address('result'), on_line=ref('test_line'))
+
+// DexExpectWatchValue('result', address('result'), on_line=ref('test_line'))