#!/usr/bin/env python3 # Copyright 2017 The ChromiumOS Authors # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """Tests for Stack Analyzer classes and functions.""" from __future__ import print_function import os import subprocess import unittest import mock # pylint:disable=import-error import stack_analyzer as sa class ObjectTest(unittest.TestCase): """Tests for classes of basic objects.""" def testTask(self): task_a = sa.Task("a", "a_task", 1234) task_b = sa.Task("b", "b_task", 5678, 0x1000) self.assertEqual(task_a, task_a) self.assertNotEqual(task_a, task_b) self.assertNotEqual(task_a, None) def testSymbol(self): symbol_a = sa.Symbol(0x1234, "F", 32, "a") symbol_b = sa.Symbol(0x234, "O", 42, "b") self.assertEqual(symbol_a, symbol_a) self.assertNotEqual(symbol_a, symbol_b) self.assertNotEqual(symbol_a, None) def testCallsite(self): callsite_a = sa.Callsite(0x1002, 0x3000, False) callsite_b = sa.Callsite(0x1002, 0x3000, True) self.assertEqual(callsite_a, callsite_a) self.assertNotEqual(callsite_a, callsite_b) self.assertNotEqual(callsite_a, None) def testFunction(self): func_a = sa.Function(0x100, "a", 0, []) func_b = sa.Function(0x200, "b", 0, []) self.assertEqual(func_a, func_a) self.assertNotEqual(func_a, func_b) self.assertNotEqual(func_a, None) class ArmAnalyzerTest(unittest.TestCase): """Tests for class ArmAnalyzer.""" def AppendConditionCode(self, opcodes): rets = [] for opcode in opcodes: rets.extend(opcode + cc for cc in sa.ArmAnalyzer.CONDITION_CODES) return rets def testInstructionMatching(self): jump_list = self.AppendConditionCode(["b", "bx"]) jump_list += list(opcode + ".n" for opcode in jump_list) + list( opcode + ".w" for opcode in jump_list ) for opcode in jump_list: self.assertIsNotNone(sa.ArmAnalyzer.JUMP_OPCODE_RE.match(opcode)) self.assertIsNone(sa.ArmAnalyzer.JUMP_OPCODE_RE.match("bl")) self.assertIsNone(sa.ArmAnalyzer.JUMP_OPCODE_RE.match("blx")) cbz_list = ["cbz", "cbnz", "cbz.n", "cbnz.n", "cbz.w", "cbnz.w"] for opcode in cbz_list: self.assertIsNotNone( sa.ArmAnalyzer.CBZ_CBNZ_OPCODE_RE.match(opcode) ) self.assertIsNone(sa.ArmAnalyzer.CBZ_CBNZ_OPCODE_RE.match("cbn")) call_list = self.AppendConditionCode(["bl", "blx"]) call_list += list(opcode + ".n" for opcode in call_list) for opcode in call_list: self.assertIsNotNone(sa.ArmAnalyzer.CALL_OPCODE_RE.match(opcode)) self.assertIsNone(sa.ArmAnalyzer.CALL_OPCODE_RE.match("ble")) result = sa.ArmAnalyzer.CALL_OPERAND_RE.match("53f90 ") self.assertIsNotNone(result) self.assertEqual(result.group(1), "53f90") self.assertEqual(result.group(2), "get_time+0x18") result = sa.ArmAnalyzer.CBZ_CBNZ_OPERAND_RE.match("r6, 53f90 ") self.assertIsNotNone(result) self.assertEqual(result.group(1), "53f90") self.assertEqual(result.group(2), "get+0x0") self.assertIsNotNone(sa.ArmAnalyzer.PUSH_OPCODE_RE.match("push")) self.assertIsNone(sa.ArmAnalyzer.PUSH_OPCODE_RE.match("pushal")) self.assertIsNotNone(sa.ArmAnalyzer.STM_OPCODE_RE.match("stmdb")) self.assertIsNone(sa.ArmAnalyzer.STM_OPCODE_RE.match("lstm")) self.assertIsNotNone(sa.ArmAnalyzer.SUB_OPCODE_RE.match("sub")) self.assertIsNotNone(sa.ArmAnalyzer.SUB_OPCODE_RE.match("subs")) self.assertIsNotNone(sa.ArmAnalyzer.SUB_OPCODE_RE.match("subw")) self.assertIsNotNone(sa.ArmAnalyzer.SUB_OPCODE_RE.match("sub.w")) self.assertIsNotNone(sa.ArmAnalyzer.SUB_OPCODE_RE.match("subs.w")) result = sa.ArmAnalyzer.SUB_OPERAND_RE.match("sp, sp, #1668 ; 0x684") self.assertIsNotNone(result) self.assertEqual(result.group(1), "1668") result = sa.ArmAnalyzer.SUB_OPERAND_RE.match("sp, #1668") self.assertIsNotNone(result) self.assertEqual(result.group(1), "1668") self.assertIsNone(sa.ArmAnalyzer.SUB_OPERAND_RE.match("sl, #1668")) def testAnalyzeFunction(self): analyzer = sa.ArmAnalyzer() symbol = sa.Symbol(0x10, "F", 0x100, "foo") instructions = [ (0x10, "push", "{r4, r5, r6, r7, lr}"), (0x12, "subw", "sp, sp, #16 ; 0x10"), (0x16, "movs", "lr, r1"), (0x18, "beq.n", "26 "), (0x1A, "bl", "30 "), (0x1E, "bl", "deadbeef "), (0x22, "blx", "0 "), (0x26, "push", "{r1}"), (0x28, "stmdb", "sp!, {r4, r5, r6, r7, r8, r9, lr}"), (0x2C, "stmdb", "sp!, {r4}"), (0x30, "stmdb", "sp, {r4}"), (0x34, "bx.n", "10 "), (0x36, "bx.n", "r3"), (0x38, "ldr", "pc, [r10]"), ] (size, callsites) = analyzer.AnalyzeFunction(symbol, instructions) self.assertEqual(size, 72) expect_callsites = [ sa.Callsite(0x1E, 0xDEADBEEF, False), sa.Callsite(0x22, 0x0, False), sa.Callsite(0x34, 0x10, True), sa.Callsite(0x36, None, True), sa.Callsite(0x38, None, True), ] self.assertEqual(callsites, expect_callsites) class StackAnalyzerTest(unittest.TestCase): """Tests for class StackAnalyzer.""" def setUp(self): symbols = [ sa.Symbol(0x1000, "F", 0x15C, "hook_task"), sa.Symbol(0x2000, "F", 0x51C, "console_task"), sa.Symbol(0x3200, "O", 0x124, "__just_data"), sa.Symbol(0x4000, "F", 0x11C, "touchpad_calc"), sa.Symbol(0x5000, "F", 0x12C, "touchpad_calc.constprop.42"), 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(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, rodata, tasklist, {}) def testParseSymbolText(self): symbol_text = ( "0 g F .text e8 Foo\n" "0000dead w F .text 000000e8 .hidden Bar\n" "deadbeef l O .bss 00000004 .hidden Woooo\n" "deadbee g O .rodata 00000008 __Hooo_ooo\n" "deadbee g .rodata 00000000 __foo_doo_coo_end\n" ) symbols = sa.ParseSymbolText(symbol_text) expect_symbols = [ sa.Symbol(0x0, "F", 0xE8, "Foo"), sa.Symbol(0xDEAD, "F", 0xE8, "Bar"), sa.Symbol(0xDEADBEEF, "O", 0x4, "Woooo"), sa.Symbol(0xDEADBEE, "O", 0x8, "__Hooo_ooo"), 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 = [] for task in tasklist: taskinfos.append( sa.TaskInfo( name=task.name.encode("utf-8"), routine=task.routine_name.encode("utf-8"), stack_size=task.stack_max_size, ) ) TaskInfoArray = sa.TaskInfo * len(taskinfos) pointer.contents.contents = TaskInfoArray(*taskinfos) return len(taskinfos) def ro_taskinfos(pointer): return tasklist_to_taskinfos(pointer, expect_ro_tasklist) def rw_taskinfos(pointer): return tasklist_to_taskinfos(pointer, expect_rw_tasklist) expect_ro_tasklist = [ sa.Task("HOOKS", "hook_task", 2048, 0x1000), ] expect_rw_tasklist = [ sa.Task("HOOKS", "hook_task", 2048, 0x1000), sa.Task("WOOKS", "hook_task", 4096, 0x1000), sa.Task("CONSOLE", "console_task", 460, 0x2000), ] export_taskinfo = mock.MagicMock( get_ro_taskinfos=mock.MagicMock(side_effect=ro_taskinfos), get_rw_taskinfos=mock.MagicMock(side_effect=rw_taskinfos), ) tasklist = sa.LoadTasklist("RO", export_taskinfo, self.analyzer.symbols) self.assertEqual(tasklist, expect_ro_tasklist) tasklist = sa.LoadTasklist("RW", export_taskinfo, self.analyzer.symbols) self.assertEqual(tasklist, expect_rw_tasklist) def testResolveAnnotation(self): self.analyzer.annotation = {} ( add_rules, remove_rules, invalid_sigtxts, ) = self.analyzer.LoadAnnotation() self.assertEqual(add_rules, {}) self.assertEqual(remove_rules, []) self.assertEqual(invalid_sigtxts, set()) self.analyzer.annotation = {"add": None, "remove": None} ( add_rules, remove_rules, invalid_sigtxts, ) = self.analyzer.LoadAnnotation() self.assertEqual(add_rules, {}) self.assertEqual(remove_rules, []) self.assertEqual(invalid_sigtxts, set()) self.analyzer.annotation = { "add": None, "remove": [ [["a", "b"], ["0", "[", "2"], "x"], [["a", "b[x:3]"], ["0", "1", "2"], "x"], ], } ( add_rules, remove_rules, invalid_sigtxts, ) = self.analyzer.LoadAnnotation() self.assertEqual(add_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, []), 0x4000: sa.Function(0x4000, "touchpad_calc", 0, []), 0x5000: sa.Function(0x5000, "touchpad_calc.constprop.42", 0, []), 0x13000: sa.Function(0x13000, "inlined_mul", 0, []), 0x13100: sa.Function(0x13100, "inlined_mul", 0, []), } funcs[0x1000].callsites = [sa.Callsite(0x1002, None, False, None)] # Set address_to_line_cache to fake the results of addr2line. self.analyzer.address_to_line_cache = { (0x1000, False): [("hook_task", os.path.abspath("a.c"), 10)], (0x1002, False): [("toot_calc", os.path.abspath("t.c"), 1234)], (0x2000, False): [("console_task", os.path.abspath("b.c"), 20)], (0x4000, False): [("toudhpad_calc", os.path.abspath("a.c"), 20)], (0x5000, False): [ ("touchpad_calc.constprop.42", os.path.abspath("b.c"), 40) ], (0x12000, False): [("trackpad_range", os.path.abspath("t.c"), 10)], (0x13000, False): [("inlined_mul", os.path.abspath("x.c"), 12)], (0x13100, False): [("inlined_mul", os.path.abspath("x.c"), 12)], } self.analyzer.annotation = { "add": { "hook_task.lto.573": ["touchpad_calc.lto.2501[a.c]"], "console_task": ["touchpad_calc[b.c]", "inlined_mul_alias"], "hook_task[q.c]": ["hook_task"], "inlined_mul[x.c]": ["inlined_mul"], "toot_calc[t.c:1234]": ["hook_task"], }, "remove": [ ["touchpad?calc["], "touchpad_calc", ["touchpad_calc[a.c]"], ["task_unk[a.c]"], ["touchpad_calc[x/a.c]"], ["trackpad_range"], ["inlined_mul"], ["inlined_mul", "console_task", "touchpad_calc[a.c]"], ["inlined_mul", "inlined_mul_alias", "console_task"], ["inlined_mul", "inlined_mul_alias", "console_task"], ], } ( add_rules, remove_rules, invalid_sigtxts, ) = self.analyzer.LoadAnnotation() self.assertEqual(invalid_sigtxts, {"touchpad?calc["}) signature_set = set() for src_sig, dst_sigs in add_rules.items(): signature_set.add(src_sig) signature_set.update(dst_sigs) for remove_sigs in remove_rules: signature_set.update(remove_sigs) (signature_map, failed_sigs) = self.analyzer.MapAnnotation( funcs, signature_set ) result = self.analyzer.ResolveAnnotation(funcs) (add_set, remove_list, eliminated_addrs, failed_sigs) = result expect_signature_map = { ("hook_task", None, None): {funcs[0x1000]}, ("touchpad_calc", os.path.abspath("a.c"), None): {funcs[0x4000]}, ("touchpad_calc", os.path.abspath("b.c"), None): {funcs[0x5000]}, ("console_task", None, None): {funcs[0x2000]}, ("inlined_mul_alias", None, None): {funcs[0x13100]}, ("inlined_mul", os.path.abspath("x.c"), None): { funcs[0x13000], funcs[0x13100], }, ("inlined_mul", None, None): {funcs[0x13000], funcs[0x13100]}, } self.assertEqual(len(signature_map), len(expect_signature_map)) for sig, funclist in signature_map.items(): self.assertEqual(set(funclist), expect_signature_map[sig]) self.assertEqual( add_set, { (funcs[0x1000], funcs[0x4000]), (funcs[0x1000], funcs[0x1000]), (funcs[0x2000], funcs[0x5000]), (funcs[0x2000], funcs[0x13100]), (funcs[0x13000], funcs[0x13000]), (funcs[0x13000], funcs[0x13100]), (funcs[0x13100], funcs[0x13000]), (funcs[0x13100], funcs[0x13100]), }, ) expect_remove_list = [ [funcs[0x4000]], [funcs[0x13000]], [funcs[0x13100]], [funcs[0x13000], funcs[0x2000], funcs[0x4000]], [funcs[0x13100], funcs[0x2000], funcs[0x4000]], [funcs[0x13000], funcs[0x13100], funcs[0x2000]], [funcs[0x13100], funcs[0x13100], funcs[0x2000]], ] self.assertEqual(len(remove_list), len(expect_remove_list)) for remove_path in remove_list: self.assertTrue(remove_path in expect_remove_list) self.assertEqual(eliminated_addrs, {0x1002}) self.assertEqual( failed_sigs, { ("touchpad?calc[", sa.StackAnalyzer.ANNOTATION_ERROR_INVALID), ("touchpad_calc", sa.StackAnalyzer.ANNOTATION_ERROR_AMBIGUOUS), ("hook_task[q.c]", sa.StackAnalyzer.ANNOTATION_ERROR_NOTFOUND), ("task_unk[a.c]", sa.StackAnalyzer.ANNOTATION_ERROR_NOTFOUND), ( "touchpad_calc[x/a.c]", sa.StackAnalyzer.ANNOTATION_ERROR_NOTFOUND, ), ("trackpad_range", sa.StackAnalyzer.ANNOTATION_ERROR_NOTFOUND), }, ) def testPreprocessAnnotation(self): funcs = { 0x1000: sa.Function(0x1000, "hook_task", 0, []), 0x2000: sa.Function(0x2000, "console_task", 0, []), 0x4000: sa.Function(0x4000, "touchpad_calc", 0, []), } funcs[0x1000].callsites = [ sa.Callsite(0x1002, 0x1000, False, funcs[0x1000]) ] funcs[0x2000].callsites = [ sa.Callsite(0x2002, 0x1000, False, funcs[0x1000]), sa.Callsite(0x2006, None, True, None), ] add_set = { (funcs[0x2000], funcs[0x2000]), (funcs[0x2000], funcs[0x4000]), (funcs[0x4000], funcs[0x1000]), (funcs[0x4000], funcs[0x2000]), } remove_list = [ [funcs[0x1000]], [funcs[0x2000], funcs[0x2000]], [funcs[0x4000], funcs[0x1000]], [funcs[0x2000], funcs[0x4000], funcs[0x2000]], [funcs[0x4000], funcs[0x1000], funcs[0x4000]], ] eliminated_addrs = {0x2006} remaining_remove_list = self.analyzer.PreprocessAnnotation( funcs, add_set, remove_list, eliminated_addrs ) expect_funcs = { 0x1000: sa.Function(0x1000, "hook_task", 0, []), 0x2000: sa.Function(0x2000, "console_task", 0, []), 0x4000: sa.Function(0x4000, "touchpad_calc", 0, []), } expect_funcs[0x2000].callsites = [ sa.Callsite(None, 0x4000, False, expect_funcs[0x4000]) ] expect_funcs[0x4000].callsites = [ sa.Callsite(None, 0x2000, False, expect_funcs[0x2000]) ] self.assertEqual(funcs, expect_funcs) self.assertEqual( remaining_remove_list, [ [funcs[0x2000], funcs[0x4000], funcs[0x2000]], ], ) def testAndesAnalyzeDisassembly(self): disasm_text = ( "\n" "build/{BOARD}/RW/ec.RW.elf: file format elf32-nds32le" "\n" "Disassembly of section .text:\n" "\n" "00000900 :\n" " ...\n" "00001000 :\n" " 1000: fc 42\tpush25 $r10, #16 ! {$r6~$r10, $fp, $gp, $lp}\n" " 1004: 47 70\t\tmovi55 $r0, #1\n" " 1006: b1 13\tbnezs8 100929de \n" " 1008: 00 01 5c fc\tbne $r6, $r0, 2af6a\n" "00002000 :\n" " 2000: fc 00\t\tpush25 $r6, #0 ! {$r6, $fp, $gp, $lp} \n" " 2002: f0 0e fc c5\tjal 1000 \n" " 2006: f0 0e bd 3b\tj 53968 \n" " 200a: de ad be ef\tswi.gp $r0, [ + #-11036]\n" "00004000 :\n" " 4000: 47 70\t\tmovi55 $r0, #1\n" "00010000 :" ) function_map = self.analyzer.AnalyzeDisassembly(disasm_text) func_hook_task = sa.Function( 0x1000, "hook_task", 48, [sa.Callsite(0x1006, 0x100929DE, True, None)], ) expect_funcmap = { 0x1000: func_hook_task, 0x2000: sa.Function( 0x2000, "console_task", 16, [ sa.Callsite(0x2002, 0x1000, False, func_hook_task), sa.Callsite(0x2006, 0x53968, True, None), ], ), 0x4000: sa.Function(0x4000, "touchpad_calc", 0, []), } self.assertEqual(function_map, expect_funcmap) def testArmAnalyzeDisassembly(self): disasm_text = ( "\n" "build/{BOARD}/RW/ec.RW.elf: file format elf32-littlearm" "\n" "Disassembly of section .text:\n" "\n" "00000900 :\n" " ...\n" "00001000 :\n" " 1000: dead beef\tfake\n" " 1004: 4770\t\tbx lr\n" " 1006: b113\tcbz r3, 100929de \n" " 1008: 00015cfc\t.word 0x00015cfc\n" "00002000 :\n" " 2000: b508\t\tpush {r3, lr} ; malformed comments,; r0, r1 \n" " 2002: f00e fcc5\tbl 1000 \n" " 2006: f00e bd3b\tb.w 53968 \n" " 200a: dead beef\tfake\n" "00004000 :\n" " 4000: 4770\t\tbx lr\n" "00010000 :" ) function_map = self.analyzer.AnalyzeDisassembly(disasm_text) func_hook_task = sa.Function( 0x1000, "hook_task", 0, [sa.Callsite(0x1006, 0x100929DE, True, None)], ) expect_funcmap = { 0x1000: func_hook_task, 0x2000: sa.Function( 0x2000, "console_task", 8, [ sa.Callsite(0x2002, 0x1000, False, func_hook_task), sa.Callsite(0x2006, 0x53968, True, None), ], ), 0x4000: sa.Function(0x4000, "touchpad_calc", 0, []), } self.assertEqual(function_map, expect_funcmap) def testAnalyzeCallGraph(self): funcs = { 0x1000: sa.Function(0x1000, "hook_task", 0, []), 0x2000: sa.Function(0x2000, "console_task", 8, []), 0x3000: sa.Function(0x3000, "task_a", 12, []), 0x4000: sa.Function(0x4000, "task_b", 96, []), 0x5000: sa.Function(0x5000, "task_c", 32, []), 0x6000: sa.Function(0x6000, "task_d", 100, []), 0x7000: sa.Function(0x7000, "task_e", 24, []), 0x8000: sa.Function(0x8000, "task_f", 20, []), 0x9000: sa.Function(0x9000, "task_g", 20, []), 0x10000: sa.Function(0x10000, "task_x", 16, []), } funcs[0x1000].callsites = [ sa.Callsite(0x1002, 0x3000, False, funcs[0x3000]), sa.Callsite(0x1006, 0x4000, False, funcs[0x4000]), ] funcs[0x2000].callsites = [ sa.Callsite(0x2002, 0x5000, False, funcs[0x5000]), sa.Callsite(0x2006, 0x2000, False, funcs[0x2000]), sa.Callsite(0x200A, 0x10000, False, funcs[0x10000]), ] funcs[0x3000].callsites = [ sa.Callsite(0x3002, 0x4000, False, funcs[0x4000]), sa.Callsite(0x3006, 0x1000, False, funcs[0x1000]), ] funcs[0x4000].callsites = [ sa.Callsite(0x4002, 0x6000, True, funcs[0x6000]), sa.Callsite(0x4006, 0x7000, False, funcs[0x7000]), sa.Callsite(0x400A, 0x8000, False, funcs[0x8000]), ] funcs[0x5000].callsites = [ sa.Callsite(0x5002, 0x4000, False, funcs[0x4000]) ] funcs[0x7000].callsites = [ sa.Callsite(0x7002, 0x7000, False, funcs[0x7000]) ] funcs[0x8000].callsites = [ sa.Callsite(0x8002, 0x9000, False, funcs[0x9000]) ] funcs[0x9000].callsites = [ sa.Callsite(0x9002, 0x4000, False, funcs[0x4000]) ] funcs[0x10000].callsites = [ sa.Callsite(0x10002, 0x2000, False, funcs[0x2000]) ] cycles = self.analyzer.AnalyzeCallGraph( funcs, [ [funcs[0x2000]] * 2, [funcs[0x10000], funcs[0x2000]] * 3, [funcs[0x1000], funcs[0x3000], funcs[0x1000]], ], ) expect_func_stack = { 0x1000: ( 268, [ funcs[0x1000], funcs[0x3000], funcs[0x4000], funcs[0x8000], funcs[0x9000], funcs[0x4000], funcs[0x7000], ], ), 0x2000: ( 208, [ funcs[0x2000], funcs[0x10000], funcs[0x2000], funcs[0x10000], funcs[0x2000], funcs[0x5000], funcs[0x4000], funcs[0x7000], ], ), 0x3000: ( 280, [ funcs[0x3000], funcs[0x1000], funcs[0x3000], funcs[0x4000], funcs[0x8000], funcs[0x9000], funcs[0x4000], funcs[0x7000], ], ), 0x4000: (120, [funcs[0x4000], funcs[0x7000]]), 0x5000: (152, [funcs[0x5000], funcs[0x4000], funcs[0x7000]]), 0x6000: (100, [funcs[0x6000]]), 0x7000: (24, [funcs[0x7000]]), 0x8000: ( 160, [funcs[0x8000], funcs[0x9000], funcs[0x4000], funcs[0x7000]], ), 0x9000: (140, [funcs[0x9000], funcs[0x4000], funcs[0x7000]]), 0x10000: ( 200, [ funcs[0x10000], funcs[0x2000], funcs[0x10000], funcs[0x2000], funcs[0x5000], funcs[0x4000], funcs[0x7000], ], ), } expect_cycles = [ {funcs[0x4000], funcs[0x8000], funcs[0x9000]}, {funcs[0x7000]}, ] for func in funcs.values(): (stack_max_usage, stack_max_path) = expect_func_stack[func.address] self.assertEqual(func.stack_max_usage, stack_max_usage) self.assertEqual(func.stack_max_path, stack_max_path) self.assertEqual(len(cycles), len(expect_cycles)) for cycle in cycles: self.assertTrue(cycle in expect_cycles) @mock.patch("subprocess.check_output") def testAddressToLine(self, checkoutput_mock): checkoutput_mock.return_value = "fake_func\n/test.c:1" self.assertEqual( self.analyzer.AddressToLine(0x1234), [("fake_func", "/test.c", 1)] ) checkoutput_mock.assert_called_once_with( ["addr2line", "-f", "-e", "./ec.RW.elf", "1234"], encoding="utf-8" ) checkoutput_mock.reset_mock() checkoutput_mock.return_value = "fake_func\n/a.c:1\nbake_func\n/b.c:2\n" self.assertEqual( self.analyzer.AddressToLine(0x1234, True), [("fake_func", "/a.c", 1), ("bake_func", "/b.c", 2)], ) checkoutput_mock.assert_called_once_with( ["addr2line", "-f", "-e", "./ec.RW.elf", "1234", "-i"], encoding="utf-8", ) checkoutput_mock.reset_mock() checkoutput_mock.return_value = ( "fake_func\n/test.c:1 (discriminator 128)" ) self.assertEqual( self.analyzer.AddressToLine(0x12345), [("fake_func", "/test.c", 1)] ) checkoutput_mock.assert_called_once_with( ["addr2line", "-f", "-e", "./ec.RW.elf", "12345"], encoding="utf-8" ) checkoutput_mock.reset_mock() checkoutput_mock.return_value = "??\n:?\nbake_func\n/b.c:2\n" self.assertEqual( self.analyzer.AddressToLine(0x123456), [None, ("bake_func", "/b.c", 2)], ) checkoutput_mock.assert_called_once_with( ["addr2line", "-f", "-e", "./ec.RW.elf", "123456"], encoding="utf-8" ) checkoutput_mock.reset_mock() with self.assertRaisesRegexp( sa.StackAnalyzerError, "addr2line failed to resolve lines." ): checkoutput_mock.side_effect = subprocess.CalledProcessError(1, "") self.analyzer.AddressToLine(0x5678) with self.assertRaisesRegexp( sa.StackAnalyzerError, "Failed to run addr2line." ): checkoutput_mock.side_effect = OSError() self.analyzer.AddressToLine(0x9012) @mock.patch("subprocess.check_output") @mock.patch("stack_analyzer.StackAnalyzer.AddressToLine") def testAndesAnalyze(self, addrtoline_mock, checkoutput_mock): disasm_text = ( "\n" "build/{BOARD}/RW/ec.RW.elf: file format elf32-nds32le" "\n" "Disassembly of section .text:\n" "\n" "00000900 :\n" " ...\n" "00001000 :\n" " 1000: fc 00\t\tpush25 $r10, #16 ! {$r6~$r10, $fp, $gp, $lp}\n" " 1002: 47 70\t\tmovi55 $r0, #1\n" " 1006: 00 01 5c fc\tbne $r6, $r0, 2af6a\n" "00002000 :\n" " 2000: fc 00\t\tpush25 $r6, #0 ! {$r6, $fp, $gp, $lp} \n" " 2002: f0 0e fc c5\tjal 1000 \n" " 2006: f0 0e bd 3b\tj 53968 \n" " 200a: 12 34 56 78\tjral5 $r0\n" ) addrtoline_mock.return_value = [("??", "??", 0)] self.analyzer.annotation = { "exception_frame_size": 64, "remove": [["fake_func"]], } with mock.patch("builtins.print") as print_mock: checkoutput_mock.return_value = disasm_text self.analyzer.Analyze() print_mock.assert_has_calls( [ mock.call( "Task: HOOKS, Max size: 96 (32 + 64), Allocated size: 2048" ), mock.call("Call Trace:"), mock.call(" hook_task (32) [??:0] 1000"), mock.call( "Task: CONSOLE, Max size: 112 (48 + 64), Allocated size: 460" ), mock.call("Call Trace:"), mock.call(" console_task (16) [??:0] 2000"), mock.call(" -> ??[??:0] 2002"), mock.call(" hook_task (32) [??:0] 1000"), mock.call("Unresolved indirect callsites:"), mock.call(" In function console_task:"), mock.call(" -> ??[??:0] 200a"), mock.call("Unresolved annotation signatures:"), mock.call(" fake_func: function is not found"), ] ) with self.assertRaisesRegexp( sa.StackAnalyzerError, "Failed to run objdump." ): checkoutput_mock.side_effect = OSError() self.analyzer.Analyze() with self.assertRaisesRegexp( sa.StackAnalyzerError, "objdump failed to disassemble." ): checkoutput_mock.side_effect = subprocess.CalledProcessError(1, "") self.analyzer.Analyze() @mock.patch("subprocess.check_output") @mock.patch("stack_analyzer.StackAnalyzer.AddressToLine") def testArmAnalyze(self, addrtoline_mock, checkoutput_mock): disasm_text = ( "\n" "build/{BOARD}/RW/ec.RW.elf: file format elf32-littlearm" "\n" "Disassembly of section .text:\n" "\n" "00000900 :\n" " ...\n" "00001000 :\n" " 1000: b508\t\tpush {r3, lr}\n" " 1002: 4770\t\tbx lr\n" " 1006: 00015cfc\t.word 0x00015cfc\n" "00002000 :\n" " 2000: b508\t\tpush {r3, lr}\n" " 2002: f00e fcc5\tbl 1000 \n" " 2006: f00e bd3b\tb.w 53968 \n" " 200a: 1234 5678\tb.w sl\n" ) addrtoline_mock.return_value = [("??", "??", 0)] self.analyzer.annotation = { "exception_frame_size": 64, "remove": [["fake_func"]], } with mock.patch("builtins.print") as print_mock: checkoutput_mock.return_value = disasm_text self.analyzer.Analyze() print_mock.assert_has_calls( [ mock.call( "Task: HOOKS, Max size: 72 (8 + 64), Allocated size: 2048" ), mock.call("Call Trace:"), mock.call(" hook_task (8) [??:0] 1000"), mock.call( "Task: CONSOLE, Max size: 80 (16 + 64), Allocated size: 460" ), mock.call("Call Trace:"), mock.call(" console_task (8) [??:0] 2000"), mock.call(" -> ??[??:0] 2002"), mock.call(" hook_task (8) [??:0] 1000"), mock.call("Unresolved indirect callsites:"), mock.call(" In function console_task:"), mock.call(" -> ??[??:0] 200a"), mock.call("Unresolved annotation signatures:"), mock.call(" fake_func: function is not found"), ] ) with self.assertRaisesRegexp( sa.StackAnalyzerError, "Failed to run objdump." ): checkoutput_mock.side_effect = OSError() self.analyzer.Analyze() with self.assertRaisesRegexp( sa.StackAnalyzerError, "objdump failed to disassemble." ): checkoutput_mock.side_effect = subprocess.CalledProcessError(1, "") self.analyzer.Analyze() @mock.patch("subprocess.check_output") @mock.patch("stack_analyzer.ParseArgs") 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", section="RW", objdump="objdump", addr2line="addr2line", annotation="fake", ) parseargs_mock.return_value = args with mock.patch("os.path.exists") as path_mock: path_mock.return_value = False with mock.patch("builtins.print") as print_mock: with mock.patch("builtins.open", mock.mock_open()) as open_mock: sa.main() print_mock.assert_any_call( "Warning: Annotation file fake does not exist." ) with mock.patch("os.path.exists") as path_mock: path_mock.return_value = True with mock.patch("builtins.print") as print_mock: with mock.patch("builtins.open", mock.mock_open()) as open_mock: open_mock.side_effect = IOError() sa.main() print_mock.assert_called_once_with( "Error: Failed to open annotation file fake." ) with mock.patch("builtins.print") as print_mock: with mock.patch("builtins.open", mock.mock_open()) as open_mock: open_mock.return_value.read.side_effect = ["{", ""] sa.main() open_mock.assert_called_once_with("fake", "r") print_mock.assert_called_once_with( "Error: Failed to parse annotation file fake." ) with mock.patch("builtins.print") as print_mock: with mock.patch( "builtins.open", mock.mock_open(read_data="") ) as open_mock: sa.main() print_mock.assert_called_once_with( "Error: Invalid annotation file fake." ) args.annotation = None with mock.patch("builtins.print") as print_mock: checkoutput_mock.side_effect = [symbol_text, rodata_text] sa.main() print_mock.assert_called_once_with( "Error: Failed to load export_taskinfo." ) with mock.patch("builtins.print") as print_mock: checkoutput_mock.side_effect = subprocess.CalledProcessError(1, "") sa.main() print_mock.assert_called_once_with( "Error: objdump failed to dump symbol table or rodata." ) with mock.patch("builtins.print") as print_mock: checkoutput_mock.side_effect = OSError() sa.main() print_mock.assert_called_once_with("Error: Failed to run objdump.") if __name__ == "__main__": unittest.main()