stack_analyzer: Add new syntax for function pointer arrays

Makes it far simpler to support hooks, console commands, host
commands.

BRANCH=poppy,fizz
BUG=chromium:648840
TEST=Add new array annotation, run stack_analyzer

Change-Id: I8ed074ba5534661ed59f4f713bb4ba194e712f4e
Signed-off-by: Nicolas Boichat <drinkcat@chromium.org>
Reviewed-on: https://chromium-review.googlesource.com/966042
Reviewed-by: Vincent Palatin <vpalatin@chromium.org>
This commit is contained in:
Nicolas Boichat
2018-03-16 10:13:56 +08:00
committed by chrome-bot
parent 324a7ab083
commit 0cbb4b6f9d
3 changed files with 187 additions and 14 deletions

View File

@@ -72,3 +72,31 @@ add:
```
The source `tcpm_transmit[driver/tcpm/tcpm.h:142]` must be a full signature (function_name[path:line number]).
So the resolver can know which indirect call you want to annotate and eliminate (even if it is inlined).
Annotating arrays (hooks, console commands, host commands)
----------------------------------------------------------
When a callsite calls a number of functions based on values from an constant
array (in `.rodata` section), one can use the following syntax:
```
hook_task[common/hooks.c:197]:
- { name: __deferred_funcs, stride: 4, offset: 0 }
- { name: __hooks_second, stride: 8, offset: 0 }
- { name: __hooks_tick, stride: 8, offset: 0 }
```
Where `name` is the symbol name for the start of the array (the end of the array
is `<name>_end`), stride is the array element size, and offset is the offset of
the function pointer in the structure. For example, above, `__deferred_funcs` is
a simple array of function pointers, while `__hooks_tick` is an array of
`struct hook_data` (size 8, pointer at offset 0):
```
struct hook_data {
/* Hook processing routine. */
void (*routine)(void);
/* Priority; low numbers = higher priority. */
int priority;
};
```

View File

@@ -387,17 +387,20 @@ class StackAnalyzer(object):
ANNOTATION_ERROR_NOTFOUND = 'function is not found'
ANNOTATION_ERROR_AMBIGUOUS = 'signature is ambiguous'
def __init__(self, options, symbols, tasklist, annotation):
def __init__(self, options, symbols, rodata, tasklist, annotation):
"""Constructor.
Args:
options: Namespace from argparse.parse_args().
symbols: Symbol list.
rodata: Content of .rodata section (offset, data)
tasklist: Task list.
annotation: Annotation config.
"""
self.options = options
self.symbols = symbols
self.rodata_offset = rodata[0]
self.rodata = rodata[1]
self.tasklist = tasklist
self.annotation = annotation
self.address_to_line_cache = {}
@@ -746,6 +749,57 @@ class StackAnalyzer(object):
return (name_result.group('name').strip(), path, linenum)
def ExpandArray(dic):
"""Parse and expand a symbol array
Args:
dic: Dictionary for the array annotation
Returns:
array of (symbol name, None, None).
"""
# TODO(drinkcat): This function is quite inefficient, as it goes through
# the symbol table multiple times.
begin_name = dic['name']
end_name = dic['name'] + "_end"
offset = dic['offset'] if 'offset' in dic else 0
stride = dic['stride']
begin_address = None
end_address = None
for symbol in self.symbols:
if (symbol.name == begin_name):
begin_address = symbol.address
if (symbol.name == end_name):
end_address = symbol.address
if (not begin_address or not end_address):
return None
output = []
# TODO(drinkcat): This is inefficient as we go from address to symbol
# object then to symbol name, and later on we'll go back from symbol name
# to symbol object.
for addr in range(begin_address+offset, end_address, stride):
# TODO(drinkcat): Not all architectures need to drop the first bit.
val = self.rodata[(addr-self.rodata_offset)/4] & 0xfffffffe
name = None
for symbol in self.symbols:
if (symbol.address == val):
result = self.FUNCTION_PREFIX_NAME_RE.match(symbol.name)
name = result.group('name')
break
if not name:
raise StackAnalyzerError('Cannot find function for address %s.',
hex(val))
output.append((name, None, None))
return output
add_rules = collections.defaultdict(set)
remove_rules = list()
invalid_sigtxts = set()
@@ -758,11 +812,18 @@ class StackAnalyzer(object):
continue
for dst_sigtxt in dst_sigtxts:
dst_sig = NormalizeSignature(dst_sigtxt)
if dst_sig is None:
invalid_sigtxts.add(dst_sigtxt)
if isinstance(dst_sigtxt, dict):
dst_sig = ExpandArray(dst_sigtxt)
if dst_sig is None:
invalid_sigtxts.add(str(dst_sigtxt))
else:
add_rules[src_sig].update(dst_sig)
else:
add_rules[src_sig].add(dst_sig)
dst_sig = NormalizeSignature(dst_sigtxt)
if dst_sig is None:
invalid_sigtxts.add(dst_sigtxt)
else:
add_rules[src_sig].add(dst_sig)
if 'remove' in self.annotation and self.annotation['remove'] is not None:
for sigtxt_path in self.annotation['remove']:
@@ -1385,6 +1446,54 @@ def ParseSymbolText(symbol_text):
return symbols
def ParseRoDataText(rodata_text):
"""Parse the content of rodata
Args:
symbol_text: Text of the rodata dump.
Returns:
symbols: Symbol list.
"""
# Examples: 8018ab0 00040048 00010000 10020000 4b8e0108 ...H........K...
# 100a7294 00000000 00000000 01000000 ............
base_offset = None
offset = None
rodata = []
for line in rodata_text.splitlines():
line = line.strip()
space = line.find(' ')
if space < 0:
continue
try:
address = int(line[0:space], 16)
except ValueError:
continue
if not base_offset:
base_offset = address
offset = address
elif address != offset:
raise StackAnalyzerError('objdump of rodata not contiguous.')
for i in range(0, 4):
num = line[(space + 1 + i*9):(space + 9 + i*9)]
if len(num.strip()) > 0:
val = int(num, 16)
else:
val = 0
# TODO(drinkcat): Not all platforms are necessarily big-endian
rodata.append((val & 0x000000ff) << 24 |
(val & 0x0000ff00) << 8 |
(val & 0x00ff0000) >> 8 |
(val & 0xff000000) >> 24)
offset = offset + 4*4
return (base_offset, rodata)
def LoadTasklist(section, export_taskinfo, symbols):
"""Load the task information.
@@ -1465,12 +1574,17 @@ def main():
symbol_text = subprocess.check_output([options.objdump,
'-t',
options.elf_path])
rodata_text = subprocess.check_output([options.objdump,
'-s',
'-j', '.rodata',
options.elf_path])
except subprocess.CalledProcessError:
raise StackAnalyzerError('objdump failed to dump symbol table.')
raise StackAnalyzerError('objdump failed to dump symbol table or rodata.')
except OSError:
raise StackAnalyzerError('Failed to run objdump.')
symbols = ParseSymbolText(symbol_text)
rodata = ParseRoDataText(rodata_text)
# Load the tasklist.
try:
@@ -1480,7 +1594,7 @@ def main():
tasklist = LoadTasklist(options.section, export_taskinfo, symbols)
analyzer = StackAnalyzer(options, symbols, tasklist, annotation)
analyzer = StackAnalyzer(options, symbols, rodata, tasklist, annotation)
analyzer.Analyze()
except StackAnalyzerError as e:
print('Error: {}'.format(e))

View File

@@ -149,16 +149,22 @@ class StackAnalyzerTest(unittest.TestCase):
sa.Symbol(0x12000, 'F', 0x13C, 'trackpad_range'),
sa.Symbol(0x13000, 'F', 0x200, 'inlined_mul'),
sa.Symbol(0x13100, 'F', 0x200, 'inlined_mul'),
sa.Symbol(0x13100, 'F', 0x200, 'inlined_mul_alias')]
sa.Symbol(0x13100, 'F', 0x200, 'inlined_mul_alias'),
sa.Symbol(0x20000, 'O', 0x0, '__array'),
sa.Symbol(0x20010, 'O', 0x0, '__array_end'),
]
tasklist = [sa.Task('HOOKS', 'hook_task', 2048, 0x1000),
sa.Task('CONSOLE', 'console_task', 460, 0x2000)]
# Array at 0x20000 that contains pointers to hook_task and console_task,
# with stride=8, offset=4
rodata = (0x20000, [ 0xDEAD1000, 0x00001000, 0xDEAD2000, 0x00002000 ])
options = mock.MagicMock(elf_path='./ec.RW.elf',
export_taskinfo='fake',
section='RW',
objdump='objdump',
addr2line='addr2line',
annotation=None)
self.analyzer = sa.StackAnalyzer(options, symbols, tasklist, {})
self.analyzer = sa.StackAnalyzer(options, symbols, rodata, tasklist, {})
def testParseSymbolText(self):
symbol_text = (
@@ -176,6 +182,17 @@ class StackAnalyzerTest(unittest.TestCase):
sa.Symbol(0xdeadbee, 'O', 0x0, '__foo_doo_coo_end')]
self.assertEqual(symbols, expect_symbols)
def testParseRoData(self):
rodata_text = (
'\n'
'Contents of section .rodata:\n'
' 20000 dead1000 00100000 dead2000 00200000 He..f.He..s.\n'
)
rodata = sa.ParseRoDataText(rodata_text)
expect_rodata = (0x20000,
[ 0x0010adde, 0x00001000, 0x0020adde, 0x00002000 ])
self.assertEqual(rodata, expect_rodata)
def testLoadTasklist(self):
def tasklist_to_taskinfos(pointer, tasklist):
taskinfos = []
@@ -235,16 +252,26 @@ class StackAnalyzerTest(unittest.TestCase):
}
(add_rules, remove_rules, invalid_sigtxts) = self.analyzer.LoadAnnotation()
self.assertEqual(add_rules, {})
self.assertEqual(remove_rules, [
self.assertEqual(list.sort(remove_rules), list.sort([
[('a', None, None), ('1', None, None), ('x', None, None)],
[('a', None, None), ('0', None, None), ('x', None, None)],
[('a', None, None), ('2', None, None), ('x', None, None)],
[('b', os.path.abspath('x'), 3), ('1', None, None), ('x', None, None)],
[('b', os.path.abspath('x'), 3), ('0', None, None), ('x', None, None)],
[('b', os.path.abspath('x'), 3), ('2', None, None), ('x', None, None)],
])
]))
self.assertEqual(invalid_sigtxts, {'['})
self.analyzer.annotation = {
'add': {
'touchpad_calc': [ dict(name='__array', stride=8, offset=4) ],
}
}
(add_rules, remove_rules, invalid_sigtxts) = self.analyzer.LoadAnnotation()
self.assertEqual(add_rules, {
('touchpad_calc', None, None):
set([('console_task', None, None), ('hook_task', None, None)])})
funcs = {
0x1000: sa.Function(0x1000, 'hook_task', 0, []),
0x2000: sa.Function(0x2000, 'console_task', 0, []),
@@ -288,7 +315,6 @@ class StackAnalyzerTest(unittest.TestCase):
['inlined_mul', 'inlined_mul_alias', 'console_task'],
],
}
(add_rules, remove_rules, invalid_sigtxts) = self.analyzer.LoadAnnotation()
self.assertEqual(invalid_sigtxts, {'touchpad?calc['})
@@ -631,6 +657,11 @@ class StackAnalyzerTest(unittest.TestCase):
def testMain(self, parseargs_mock, checkoutput_mock):
symbol_text = ('1000 g F .text 0000015c .hidden hook_task\n'
'2000 g F .text 0000051c .hidden console_task\n')
rodata_text = (
'\n'
'Contents of section .rodata:\n'
' 20000 dead1000 00100000 dead2000 00200000 He..f.He..s.\n'
)
args = mock.MagicMock(elf_path='./ec.RW.elf',
export_taskinfo='fake',
@@ -675,7 +706,7 @@ class StackAnalyzerTest(unittest.TestCase):
args.annotation = None
with mock.patch('__builtin__.print') as print_mock:
checkoutput_mock.return_value = symbol_text
checkoutput_mock.side_effect = [symbol_text, rodata_text]
sa.main()
print_mock.assert_called_once_with(
'Error: Failed to load export_taskinfo.')
@@ -684,7 +715,7 @@ class StackAnalyzerTest(unittest.TestCase):
checkoutput_mock.side_effect = subprocess.CalledProcessError(1, '')
sa.main()
print_mock.assert_called_once_with(
'Error: objdump failed to dump symbol table.')
'Error: objdump failed to dump symbol table or rodata.')
with mock.patch('__builtin__.print') as print_mock:
checkoutput_mock.side_effect = OSError()