[Dexter] Add DexDeclareAddress command and address function

This patch adds a new dexter command, DexDeclareAddress, which is used
to test the relative values of pointer variables. The motivation for
adding this command is to allow meaningful assertions to be made about
pointers that go beyond checking variable availability and null
equality.

The full explanation and syntax is in Commands.md.

Reviewed By: Orlando

Differential Revision: https://reviews.llvm.org/D111447
This commit is contained in:
Stephen Tozer 2021-12-01 13:00:22 +00:00
parent dee85a4ffa
commit 0428d44d4c
16 changed files with 481 additions and 7 deletions

View File

@ -9,6 +9,7 @@
* [DexLimitSteps](Commands.md#DexLimitSteps)
* [DexLabel](Commands.md#DexLabel)
* [DexWatch](Commands.md#DexWatch)
* [DexDeclareAddress](Commands.md#DexDeclareAddress)
* [DexDeclareFile](Commands.md#DexDeclareFile)
* [DexFinishTest](Commands.md#DexFinishTest)
@ -230,6 +231,61 @@ arithmetic operators to get offsets from labels:
DexExpectWatchValues(..., on_line=ref('my_line_name') - 5)
### Heuristic
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.

View File

@ -19,11 +19,13 @@ from dex.utils.Exceptions import CommandParseError
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
@ -39,6 +41,7 @@ def _get_valid_commands():
{ name (str): command (class) }
"""
return {
DexDeclareAddress.get_name() : DexDeclareAddress,
DexDeclareFile.get_name() : DexDeclareFile,
DexExpectProgramState.get_name() : DexExpectProgramState,
DexExpectStepKind.get_name() : DexExpectStepKind,
@ -73,7 +76,7 @@ def _merge_subcommands(command_name: str, valid_commands: dict) -> dict:
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().
@ -90,9 +93,15 @@ def _build_command(command_type, labels, raw_text: str, path: str, lineno: str)
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,
})
@ -178,6 +187,14 @@ def format_unresolved_label_err(label: str, src: str, filename: str, lineno) ->
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()
@ -210,9 +227,25 @@ def add_line_label(labels, label, cmd_path, cmd_lineno):
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)
@ -258,6 +291,7 @@ def _find_all_commands_in_file(path, file_lines, valid_commands, source_root_dir
command = _build_command(
valid_commands[command_name],
labels,
addresses,
raw_text,
cmd_path,
cmd_point.get_lineno(),
@ -277,6 +311,8 @@ def _find_all_commands_in_file(path, file_lines, valid_commands, source_root_dir
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):

View File

@ -0,0 +1,58 @@
# 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

View File

@ -12,12 +12,33 @@
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):
@ -25,7 +46,7 @@ class DexExpectWatchBase(CommandBase):
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
@ -66,8 +87,32 @@ class DexExpectWatchBase(CommandBase):
# 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))]
@ -78,11 +123,11 @@ class DexExpectWatchBase(CommandBase):
@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):
@ -104,13 +149,25 @@ class DexExpectWatchBase(CommandBase):
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
@ -177,8 +234,9 @@ class DexExpectWatchBase(CommandBase):
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]
])

View File

@ -15,6 +15,7 @@ import difflib
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'])
@ -101,6 +102,7 @@ class Heuristic(object):
def __init__(self, context, steps):
self.context = context
self.penalties = {}
self.address_resolutions = {}
worst_penalty = max([
self.penalty_variable_optimized, self.penalty_irretrievable,
@ -109,6 +111,14 @@ class Heuristic(object):
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']:
@ -126,6 +136,7 @@ class Heuristic(object):
# 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
@ -425,6 +436,17 @@ class Heuristic(object):
@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]
@ -456,6 +478,14 @@ class Heuristic(object):
string += ('\n')
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

View File

@ -0,0 +1,20 @@
// 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'))

View File

@ -0,0 +1,17 @@
// 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'))

View File

@ -0,0 +1,20 @@
// 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'))

View File

@ -0,0 +1,18 @@
// 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'))

View File

@ -0,0 +1,18 @@
// 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'))

View File

@ -0,0 +1,24 @@
// 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'))

View File

@ -0,0 +1,18 @@
// 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'))

View File

@ -0,0 +1,18 @@
// 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'))

View File

@ -0,0 +1,51 @@
// 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'))

View File

@ -0,0 +1,16 @@
// 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'))

View File

@ -0,0 +1,16 @@
// 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'))