From 0cbb4b6f9dc71f6c6d90fd628f466bb61bae9dca Mon Sep 17 00:00:00 2001 From: Nicolas Boichat Date: Fri, 16 Mar 2018 10:13:56 +0800 Subject: 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 Reviewed-on: https://chromium-review.googlesource.com/966042 Reviewed-by: Vincent Palatin --- extra/stack_analyzer/README.md | 28 ++++++ extra/stack_analyzer/stack_analyzer.py | 128 ++++++++++++++++++++++-- extra/stack_analyzer/stack_analyzer_unittest.py | 45 +++++++-- 3 files changed, 187 insertions(+), 14 deletions(-) diff --git a/extra/stack_analyzer/README.md b/extra/stack_analyzer/README.md index a13b3b66db..bead6d10b4 100644 --- a/extra/stack_analyzer/README.md +++ b/extra/stack_analyzer/README.md @@ -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 `_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; +}; +``` diff --git a/extra/stack_analyzer/stack_analyzer.py b/extra/stack_analyzer/stack_analyzer.py index a839515b9e..329e4faa6b 100755 --- a/extra/stack_analyzer/stack_analyzer.py +++ b/extra/stack_analyzer/stack_analyzer.py @@ -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)) diff --git a/extra/stack_analyzer/stack_analyzer_unittest.py b/extra/stack_analyzer/stack_analyzer_unittest.py index 1fe4944844..d159b2e14a 100755 --- a/extra/stack_analyzer/stack_analyzer_unittest.py +++ b/extra/stack_analyzer/stack_analyzer_unittest.py @@ -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() -- cgit v1.2.1