|
@@ -0,0 +1,440 @@
|
|
|
|
+import re
|
|
|
|
+import pprint
|
|
|
|
+import os
|
|
|
|
+from subprocess import check_output
|
|
|
|
+from optparse import OptionParser
|
|
|
|
+
|
|
|
|
+# Constants
|
|
|
|
+rtl_ext_end = ".dfinish"
|
|
|
|
+rtl_ext = None # e.g. '.c.270r.dfinish'. The number '270' will change with gcc version and is auto-detected by the
|
|
|
|
+ # function find_rtl_ext
|
|
|
|
+dir = r'.' # Working directory
|
|
|
|
+su_ext = '.su'
|
|
|
|
+obj_ext = '.o'
|
|
|
|
+manual_ext = '.msu'
|
|
|
|
+read_elf_path = "arm-none-eabi-readelf.exe" # You may need to enter the full path here
|
|
|
|
+stdout_encoding = "utf-8" # System dependant
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+class Printable:
|
|
|
|
+ def __repr__(self):
|
|
|
|
+ return "<" + type(self).__name__ + "> " + pprint.pformat(vars(self), indent=4, width=1)
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+class Symbol(Printable):
|
|
|
|
+ pass
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+def read_symbols(file):
|
|
|
|
+ from subprocess import check_output
|
|
|
|
+
|
|
|
|
+ def to_symbol(read_elf_line):
|
|
|
|
+ v = read_elf_line.split()
|
|
|
|
+
|
|
|
|
+ s2 = Symbol()
|
|
|
|
+ s2.value = int(v[1], 16)
|
|
|
|
+ s2.size = int(v[2])
|
|
|
|
+ s2.type = v[3]
|
|
|
|
+ s2.binding = v[4]
|
|
|
|
+ if len(v) >= 8:
|
|
|
|
+ s2.name = v[7]
|
|
|
|
+ else:
|
|
|
|
+ s2.name = ""
|
|
|
|
+
|
|
|
|
+ return s2
|
|
|
|
+
|
|
|
|
+ output = check_output([read_elf_path, "-s", "-W", file]).decode(stdout_encoding)
|
|
|
|
+ lines = output.splitlines()[3:]
|
|
|
|
+ return [to_symbol(line) for line in lines]
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+def read_obj(tu, call_graph):
|
|
|
|
+ """
|
|
|
|
+ Reads the file tu.o and gets the binding (global or local) for each function
|
|
|
|
+ :param tu: name of the translation unit (e.g. for main.c, this would be 'main')
|
|
|
|
+ :param call_graph: a object used to store information about each function, results go here
|
|
|
|
+ """
|
|
|
|
+ symbols = read_symbols(tu[0:tu.rindex(".")] + obj_ext)
|
|
|
|
+
|
|
|
|
+ for s in symbols:
|
|
|
|
+
|
|
|
|
+ if s.type == 'FUNC':
|
|
|
|
+ if s.binding == 'GLOBAL':
|
|
|
|
+ # Check for multiple declarations
|
|
|
|
+ if s.name in call_graph['globals'] or s.name in call_graph['locals']:
|
|
|
|
+ raise Exception('Multiple declarations of {}'.format(s.name))
|
|
|
|
+ call_graph['globals'][s.name] = {'tu': tu, 'name': s.name, 'binding': s.binding}
|
|
|
|
+ elif s.binding == 'LOCAL':
|
|
|
|
+ # Check for multiple declarations
|
|
|
|
+ if s.name in call_graph['locals'] and tu in call_graph['locals'][s.name]:
|
|
|
|
+ raise Exception('Multiple declarations of {}'.format(s.name))
|
|
|
|
+
|
|
|
|
+ if s.name not in call_graph['locals']:
|
|
|
|
+ call_graph['locals'][s.name] = {}
|
|
|
|
+
|
|
|
|
+ call_graph['locals'][s.name][tu] = {'tu': tu, 'name': s.name, 'binding': s.binding}
|
|
|
|
+ elif s.binding == 'WEAK':
|
|
|
|
+ if s.name in call_graph['weak']:
|
|
|
|
+ raise Exception('Multiple declarations of {}'.format(s.name))
|
|
|
|
+ call_graph['weak'][s.name] = {'tu': tu, 'name': s.name, 'binding': s.binding}
|
|
|
|
+ else:
|
|
|
|
+ raise Exception('Error Unknown Binding "{}" for symbol: {}'.format(s.binding, s.name))
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+def find_fxn(tu, fxn, call_graph):
|
|
|
|
+ """
|
|
|
|
+ Looks up the dictionary associated with the function.
|
|
|
|
+ :param tu: The translation unit in which to look for locals functions
|
|
|
|
+ :param fxn: The function name
|
|
|
|
+ :param call_graph: a object used to store information about each function
|
|
|
|
+ :return: the dictionary for the given function or None
|
|
|
|
+ """
|
|
|
|
+
|
|
|
|
+ if fxn in call_graph['globals']:
|
|
|
|
+ return call_graph['globals'][fxn]
|
|
|
|
+ else:
|
|
|
|
+ try:
|
|
|
|
+ return call_graph['locals'][fxn][tu]
|
|
|
|
+ except KeyError:
|
|
|
|
+ return None
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+def find_demangled_fxn(tu, fxn, call_graph):
|
|
|
|
+ """
|
|
|
|
+ Looks up the dictionary associated with the function.
|
|
|
|
+ :param tu: The translation unit in which to look for locals functions
|
|
|
|
+ :param fxn: The function name
|
|
|
|
+ :param call_graph: a object used to store information about each function
|
|
|
|
+ :return: the dictionary for the given function or None
|
|
|
|
+ """
|
|
|
|
+ for f in call_graph['globals'].values():
|
|
|
|
+ if 'demangledName' in f:
|
|
|
|
+ if f['demangledName'] == fxn:
|
|
|
|
+ return f
|
|
|
|
+ for f in call_graph['locals'].values():
|
|
|
|
+ if tu in f:
|
|
|
|
+ if 'demangledName' in f[tu]:
|
|
|
|
+ if f[tu]['demangledName'] == fxn:
|
|
|
|
+ return f[tu]
|
|
|
|
+ return None
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+def read_rtl(tu, call_graph):
|
|
|
|
+ """
|
|
|
|
+ Read an RTL file and finds callees for each function and if there are calls via function pointer.
|
|
|
|
+ :param tu: the translation unit
|
|
|
|
+ :param call_graph: a object used to store information about each function, results go here
|
|
|
|
+ """
|
|
|
|
+
|
|
|
|
+ # Construct A Call Graph
|
|
|
|
+ function = re.compile(r'^;; Function (.*) \((\S+), funcdef_no=\d+(, [a-z_]+=\d+)*\)( \([a-z ]+\))?$')
|
|
|
|
+ static_call = re.compile(r'^.*\(call.*"(.*)".*$')
|
|
|
|
+ other_call = re.compile(r'^.*call .*$')
|
|
|
|
+
|
|
|
|
+ for line_ in open(tu + rtl_ext).readlines():
|
|
|
|
+ m = function.match(line_)
|
|
|
|
+ if m:
|
|
|
|
+ fxn_name = m.group(2)
|
|
|
|
+ fxn_dict2 = find_fxn(tu, fxn_name, call_graph)
|
|
|
|
+ if not fxn_dict2:
|
|
|
|
+ pprint.pprint(call_graph)
|
|
|
|
+ raise Exception("Error locating function {} in {}".format(fxn_name, tu))
|
|
|
|
+
|
|
|
|
+ fxn_dict2['demangledName'] = m.group(1)
|
|
|
|
+ fxn_dict2['calls'] = set()
|
|
|
|
+ fxn_dict2['has_ptr_call'] = False
|
|
|
|
+ continue
|
|
|
|
+
|
|
|
|
+ m = static_call.match(line_)
|
|
|
|
+ if m:
|
|
|
|
+ fxn_dict2['calls'].add(m.group(1))
|
|
|
|
+ # print("Call: {0} -> {1}".format(current_fxn, m.group(1)))
|
|
|
|
+ continue
|
|
|
|
+
|
|
|
|
+ m = other_call.match(line_)
|
|
|
|
+ if m:
|
|
|
|
+ fxn_dict2['has_ptr_call'] = True
|
|
|
|
+ continue
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+def read_su(tu, call_graph):
|
|
|
|
+ """
|
|
|
|
+ Reads the 'local_stack' for each function. Local stack ignores stack used by callees.
|
|
|
|
+ :param tu: the translation unit
|
|
|
|
+ :param call_graph: a object used to store information about each function, results go here
|
|
|
|
+ :return:
|
|
|
|
+ """
|
|
|
|
+
|
|
|
|
+ su_line = re.compile(r'^([^ :]+):([\d]+):([\d]+):(.+)\t(\d+)\t(\S+)$')
|
|
|
|
+ i = 1
|
|
|
|
+
|
|
|
|
+ for line in open(tu[0:tu.rindex(".")] + su_ext).readlines():
|
|
|
|
+ m = su_line.match(line)
|
|
|
|
+ if m:
|
|
|
|
+ fxn = m.group(4)
|
|
|
|
+ fxn_dict2 = find_demangled_fxn(tu, fxn, call_graph)
|
|
|
|
+ fxn_dict2['local_stack'] = int(m.group(5))
|
|
|
|
+ else:
|
|
|
|
+ print("error parsing line {} in file {}".format(i, tu))
|
|
|
|
+ i += 1
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+def read_manual(file, call_graph):
|
|
|
|
+ """
|
|
|
|
+ reads the manual stack useage files.
|
|
|
|
+ :param file: the file name
|
|
|
|
+ :param call_graph: a object used to store information about each function, results go here
|
|
|
|
+ """
|
|
|
|
+
|
|
|
|
+ for line in open(file).readlines():
|
|
|
|
+ fxn, stack_sz = line.split()
|
|
|
|
+ if fxn in call_graph:
|
|
|
|
+ raise Exception("Redeclared Function {}".format(fxn))
|
|
|
|
+ call_graph['globals'][fxn] = {'wcs': int(stack_sz),
|
|
|
|
+ 'calls': set(),
|
|
|
|
+ 'has_ptr_call': False,
|
|
|
|
+ 'local_stack': int(stack_sz),
|
|
|
|
+ 'is_manual': True,
|
|
|
|
+ 'name': fxn,
|
|
|
|
+ 'tu': '#MANUAL',
|
|
|
|
+ 'binding': 'GLOBAL'}
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+def validate_all_data(call_graph):
|
|
|
|
+ """
|
|
|
|
+ Check that every entry in the call graph has the following fields:
|
|
|
|
+ .calls, .has_ptr_call, .local_stack, .scope, .src_line
|
|
|
|
+ """
|
|
|
|
+
|
|
|
|
+ def validate_dict(d):
|
|
|
|
+ if not ('calls' in d and 'has_ptr_call' in d and 'local_stack' in d
|
|
|
|
+ and 'name' in d and 'tu' in d):
|
|
|
|
+ print("Error data is missing in fxn dictionary {}".format(d))
|
|
|
|
+
|
|
|
|
+ # Loop through every global and local function
|
|
|
|
+ # and resolve each call, save results in r_calls
|
|
|
|
+ for fxn_dict2 in call_graph['globals'].values():
|
|
|
|
+ validate_dict(fxn_dict2)
|
|
|
|
+
|
|
|
|
+ for l_dict in call_graph['locals'].values():
|
|
|
|
+ for fxn_dict2 in l_dict.values():
|
|
|
|
+ validate_dict(fxn_dict2)
|
|
|
|
+
|
|
|
|
+def resolve_all_calls(call_graph):
|
|
|
|
+ def resolve_calls(fxn_dict2):
|
|
|
|
+ fxn_dict2['r_calls'] = []
|
|
|
|
+ fxn_dict2['unresolved_calls'] = set()
|
|
|
|
+
|
|
|
|
+ for call in fxn_dict2['calls']:
|
|
|
|
+ call_dict = find_fxn(fxn_dict2['tu'], call, call_graph)
|
|
|
|
+ if call_dict:
|
|
|
|
+ fxn_dict2['r_calls'].append(call_dict)
|
|
|
|
+ else:
|
|
|
|
+ fxn_dict2['unresolved_calls'].add(call)
|
|
|
|
+
|
|
|
|
+ # Loop through every global and local function
|
|
|
|
+ # and resolve each call, save results in r_calls
|
|
|
|
+ for fxn_dict in call_graph['globals'].values():
|
|
|
|
+ resolve_calls(fxn_dict)
|
|
|
|
+
|
|
|
|
+ for l_dict in call_graph['locals'].values():
|
|
|
|
+ for fxn_dict in l_dict.values():
|
|
|
|
+ resolve_calls(fxn_dict)
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+def calc_all_wcs(call_graph):
|
|
|
|
+ def calc_wcs(fxn_dict2, call_graph1, parents):
|
|
|
|
+ """
|
|
|
|
+ Calculates the worst case stack for a fxn that is declared (or called from) in a given file.
|
|
|
|
+ :param parents: This function gets called recursively through the call graph. If a function has recursion the
|
|
|
|
+ tuple file, fxn will be in the parents stack and everything between the top of the stack and the matching entry
|
|
|
|
+ has recursion.
|
|
|
|
+ :return:
|
|
|
|
+ """
|
|
|
|
+
|
|
|
|
+ # If the wcs is already known, then nothing to do
|
|
|
|
+ if 'wcs' in fxn_dict2:
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ # Check for pointer calls
|
|
|
|
+ if fxn_dict2['has_ptr_call']:
|
|
|
|
+ fxn_dict2['wcs'] = 'unbounded'
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ # Check for recursion
|
|
|
|
+ if fxn_dict2 in parents:
|
|
|
|
+ fxn_dict2['wcs'] = 'unbounded'
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ # Calculate WCS
|
|
|
|
+ call_max = 0
|
|
|
|
+ for call_dict in fxn_dict2['r_calls']:
|
|
|
|
+
|
|
|
|
+ # Calculate the WCS for the called function
|
|
|
|
+ parents.append(fxn_dict2)
|
|
|
|
+ calc_wcs(call_dict, call_graph1, parents)
|
|
|
|
+ parents.pop()
|
|
|
|
+
|
|
|
|
+ # If the called function is unbounded, so is this function
|
|
|
|
+ if call_dict['wcs'] == 'unbounded':
|
|
|
|
+ fxn_dict2['wcs'] = 'unbounded'
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ # Keep track of the call with the largest stack use
|
|
|
|
+ call_max = max(call_max, call_dict['wcs'])
|
|
|
|
+
|
|
|
|
+ # Propagate Unresolved Calls
|
|
|
|
+ for unresolved_call in call_dict['unresolved_calls']:
|
|
|
|
+ fxn_dict2['unresolved_calls'].add(unresolved_call)
|
|
|
|
+
|
|
|
|
+ fxn_dict2['wcs'] = call_max + fxn_dict2['local_stack']
|
|
|
|
+
|
|
|
|
+ # Loop through every global and local function
|
|
|
|
+ # and resolve each call, save results in r_calls
|
|
|
|
+ for fxn_dict in call_graph['globals'].values():
|
|
|
|
+ calc_wcs(fxn_dict, call_graph, [])
|
|
|
|
+
|
|
|
|
+ for l_dict in call_graph['locals'].values():
|
|
|
|
+ for fxn_dict in l_dict.values():
|
|
|
|
+ calc_wcs(fxn_dict, call_graph, [])
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+def print_all_fxns(call_graph):
|
|
|
|
+
|
|
|
|
+ def print_fxn(row_format, fxn_dict2):
|
|
|
|
+ unresolved = fxn_dict2['unresolved_calls']
|
|
|
|
+ stack = str(fxn_dict2['wcs'])
|
|
|
|
+ if unresolved:
|
|
|
|
+ unresolved_str = '({})'.format(' ,'.join(unresolved))
|
|
|
|
+ if stack != 'unbounded':
|
|
|
|
+ stack = "unbounded:" + stack
|
|
|
|
+ else:
|
|
|
|
+ unresolved_str = ''
|
|
|
|
+
|
|
|
|
+ print(row_format.format(fxn_dict2['tu'], fxn_dict2['demangledName'], stack, unresolved_str))
|
|
|
|
+
|
|
|
|
+ def get_order(val):
|
|
|
|
+ if val == 'unbounded':
|
|
|
|
+ return 1
|
|
|
|
+ else:
|
|
|
|
+ return -val
|
|
|
|
+
|
|
|
|
+ # Loop through every global and local function
|
|
|
|
+ # and resolve each call, save results in r_calls
|
|
|
|
+ d_list = []
|
|
|
|
+ for fxn_dict in call_graph['globals'].values():
|
|
|
|
+ d_list.append(fxn_dict)
|
|
|
|
+
|
|
|
|
+ for l_dict in call_graph['locals'].values():
|
|
|
|
+ for fxn_dict in l_dict.values():
|
|
|
|
+ d_list.append(fxn_dict)
|
|
|
|
+
|
|
|
|
+ d_list.sort(key=lambda item: get_order(item['wcs']))
|
|
|
|
+
|
|
|
|
+ # Calculate table width
|
|
|
|
+ tu_width = max(max([len(d['tu']) for d in d_list]), 16)
|
|
|
|
+ name_width = max(max([len(d['name']) for d in d_list]), 13)
|
|
|
|
+ row_format = "{:<" + str(tu_width + 2) + "} {:<" + str(name_width + 2) + "} {:>14} {:<17}"
|
|
|
|
+
|
|
|
|
+ # Print out the table
|
|
|
|
+ print("")
|
|
|
|
+ print(row_format.format('Translation Unit', 'Function Name', 'Stack', 'Unresolved Dependencies'))
|
|
|
|
+ for d in d_list:
|
|
|
|
+ print_fxn(row_format, d)
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+def find_rtl_ext():
|
|
|
|
+ # Find the rtl_extension
|
|
|
|
+ global rtl_ext
|
|
|
|
+
|
|
|
|
+ for root, directories, filenames in os.walk('.'):
|
|
|
|
+ for f in filenames:
|
|
|
|
+ if (f.endswith(rtl_ext_end)):
|
|
|
|
+ rtl_ext = f[f[:-len(rtl_ext_end)].rindex("."):]
|
|
|
|
+ print("rtl_ext = " + rtl_ext)
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ print("Could not find any files ending with '.dfinish'. Check that the script is being run from the correct "
|
|
|
|
+ "directory. Check that the code was compiled with the correct flags")
|
|
|
|
+ exit(-1)
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+def find_files():
|
|
|
|
+ tu = []
|
|
|
|
+ manual = []
|
|
|
|
+ all_files = []
|
|
|
|
+ for root, directories, filenames in os.walk(dir):
|
|
|
|
+ for filename in filenames:
|
|
|
|
+ all_files.append(os.path.join(root,filename))
|
|
|
|
+
|
|
|
|
+ files = [f for f in all_files if os.path.isfile(f) and f.endswith(rtl_ext)]
|
|
|
|
+ for f in files:
|
|
|
|
+ base = f[0:-len(rtl_ext)]
|
|
|
|
+ short_base = base[0:base.rindex(".")]
|
|
|
|
+ if short_base + su_ext in all_files and short_base + obj_ext in all_files:
|
|
|
|
+ tu.append(base)
|
|
|
|
+ print('Reading: {}{}, {}{}, {}{}'.format(base, rtl_ext, short_base, su_ext, short_base, obj_ext))
|
|
|
|
+
|
|
|
|
+ files = [f for f in all_files if os.path.isfile(f) and f.endswith(manual_ext)]
|
|
|
|
+ for f in files:
|
|
|
|
+ manual.append(f)
|
|
|
|
+ print('Reading: {}'.format(f))
|
|
|
|
+
|
|
|
|
+ # Print some diagnostic messages
|
|
|
|
+ if not tu:
|
|
|
|
+ print("Could not find any translation units to analyse")
|
|
|
|
+ exit(-1)
|
|
|
|
+
|
|
|
|
+ return tu, manual
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+def main():
|
|
|
|
+
|
|
|
|
+ # Find the appropriate RTL extension
|
|
|
|
+ find_rtl_ext()
|
|
|
|
+
|
|
|
|
+ # Find all input files
|
|
|
|
+ call_graph = {'locals': {}, 'globals': {}, 'weak': {}}
|
|
|
|
+ tu_list, manual_list = find_files()
|
|
|
|
+
|
|
|
|
+ # Read the input files
|
|
|
|
+ for tu in tu_list:
|
|
|
|
+ read_obj(tu, call_graph) # This must be first
|
|
|
|
+
|
|
|
|
+ for fxn in call_graph['weak'].values():
|
|
|
|
+ if fxn['name'] not in call_graph['globals'].keys():
|
|
|
|
+ call_graph['globals'][fxn['name']] = fxn
|
|
|
|
+
|
|
|
|
+ for tu in tu_list:
|
|
|
|
+ read_rtl(tu, call_graph)
|
|
|
|
+ for tu in tu_list:
|
|
|
|
+ read_su(tu, call_graph)
|
|
|
|
+
|
|
|
|
+ # Read manual files
|
|
|
|
+ for m in manual_list:
|
|
|
|
+ read_manual(m, call_graph)
|
|
|
|
+
|
|
|
|
+ # Validate Data
|
|
|
|
+ validate_all_data(call_graph)
|
|
|
|
+
|
|
|
|
+ # Resolve All Function Calls
|
|
|
|
+ resolve_all_calls(call_graph)
|
|
|
|
+
|
|
|
|
+ # Calculate Worst Case Stack For Each Function
|
|
|
|
+ calc_all_wcs(call_graph)
|
|
|
|
+
|
|
|
|
+ # Print A Nice Message With Each Function and the WCS
|
|
|
|
+ print_all_fxns(call_graph)
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+def ThreadStackStaticAnalysis(env):
|
|
|
|
+ print('Start thread stack static analysis...')
|
|
|
|
+
|
|
|
|
+ import rtconfig
|
|
|
|
+ read_elf_path = rtconfig.EXEC_PATH + r'\readelf.exe'
|
|
|
|
+ main()
|
|
|
|
+
|
|
|
|
+ print('\nThread stack static analysis done!')
|
|
|
|
+ return
|