kopia lustrzana https://github.com/SP8EBC/ParaTNC
431 wiersze
15 KiB
Python
431 wiersze
15 KiB
Python
|
# pylint: disable = invalid-name, too-few-public-methods
|
||
|
|
||
|
import re
|
||
|
import pprint
|
||
|
import os
|
||
|
import sys
|
||
|
from typing import List, Tuple, Dict, Any
|
||
|
from subprocess import check_output
|
||
|
|
||
|
# Constants
|
||
|
rtl_ext_end = ".dfinish"
|
||
|
su_ext = '.su'
|
||
|
obj_ext = '.o'
|
||
|
manual_ext = '.msu'
|
||
|
read_elf_path = os.getenv("CROSS_COMPILE", "") + "readelf"
|
||
|
stdout_encoding = "utf-8" # System dependant
|
||
|
|
||
|
|
||
|
class Printable:
|
||
|
def __repr__(self) -> str:
|
||
|
return "<" + type(self).__name__ + "> " + pprint.pformat(vars(self), indent=4, width=1)
|
||
|
|
||
|
|
||
|
class Symbol(Printable):
|
||
|
# value: int = -1
|
||
|
# size: int = -1
|
||
|
type: str = "uninitialized"
|
||
|
binding: str = "uninitialized"
|
||
|
name: str = "uninitialized"
|
||
|
|
||
|
|
||
|
CallNode = Dict[str, Any]
|
||
|
|
||
|
|
||
|
def calc_wcs(fxn_dict2: CallNode, parents: List[CallNode]) -> None:
|
||
|
"""
|
||
|
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
|
||
|
calc_wcs(call_dict, parents + [fxn_dict2])
|
||
|
|
||
|
# 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']
|
||
|
|
||
|
|
||
|
class CallGraph:
|
||
|
globals: Dict[str, CallNode] = {}
|
||
|
locals: Dict[str, Dict[str, CallNode]] = {}
|
||
|
weak: Dict[str, CallNode] = {}
|
||
|
|
||
|
def read_obj(self, tu: str) -> None:
|
||
|
"""
|
||
|
Reads the file tu.o and gets the binding (global or local) for each function
|
||
|
:param self: a object used to store information about each function, results go here
|
||
|
:param tu: name of the translation unit (e.g. for main.c, this would be 'main')
|
||
|
"""
|
||
|
|
||
|
for s in read_symbols(tu[0:tu.rindex(".")] + obj_ext):
|
||
|
|
||
|
if s.type == 'FUNC':
|
||
|
if s.binding == 'GLOBAL':
|
||
|
# Check for multiple declarations
|
||
|
if s.name in self.globals or s.name in self.locals:
|
||
|
raise Exception(f'Multiple declarations of {s.name}')
|
||
|
self.globals[s.name] = {'tu': tu, 'name': s.name, 'binding': s.binding}
|
||
|
elif s.binding == 'LOCAL':
|
||
|
# Check for multiple declarations
|
||
|
if s.name in self.locals and tu in self.locals[s.name]:
|
||
|
raise Exception(f'Multiple declarations of {s.name}')
|
||
|
|
||
|
if s.name not in self.locals:
|
||
|
self.locals[s.name] = {}
|
||
|
|
||
|
self.locals[s.name][tu] = {'tu': tu, 'name': s.name, 'binding': s.binding}
|
||
|
elif s.binding == 'WEAK':
|
||
|
if s.name in self.weak:
|
||
|
raise Exception(f'Multiple declarations of {s.name}')
|
||
|
self.weak[s.name] = {'tu': tu, 'name': s.name, 'binding': s.binding}
|
||
|
else:
|
||
|
raise Exception(f'Error Unknown Binding "{s.binding}" for symbol: {s.name}')
|
||
|
|
||
|
def find_fxn(self, tu: str, fxn: str):
|
||
|
"""
|
||
|
Looks up the dictionary associated with the function.
|
||
|
:param self: a object used to store information about each function
|
||
|
:param tu: The translation unit in which to look for locals functions
|
||
|
:param fxn: The function name
|
||
|
:return: the dictionary for the given function or None
|
||
|
"""
|
||
|
|
||
|
if fxn in self.globals:
|
||
|
return self.globals[fxn]
|
||
|
try:
|
||
|
return self.locals[fxn][tu]
|
||
|
except KeyError:
|
||
|
return None
|
||
|
|
||
|
def find_demangled_fxn(self, tu: str, fxn: str):
|
||
|
"""
|
||
|
Looks up the dictionary associated with the function.
|
||
|
:param self: a object used to store information about each function
|
||
|
:param tu: The translation unit in which to look for locals functions
|
||
|
:param fxn: The function name
|
||
|
:return: the dictionary for the given function or None
|
||
|
"""
|
||
|
for f in self.globals.values():
|
||
|
if 'demangledName' in f:
|
||
|
if f['demangledName'] == fxn:
|
||
|
return f
|
||
|
for f in self.locals.values():
|
||
|
if tu in f:
|
||
|
if 'demangledName' in f[tu]:
|
||
|
if f[tu]['demangledName'] == fxn:
|
||
|
return f[tu]
|
||
|
return None
|
||
|
|
||
|
def read_rtl(self, tu: str, rtl_ext: str) -> None:
|
||
|
"""
|
||
|
Read an RTL file and finds callees for each function and if there are calls via function pointer.
|
||
|
:param self: a object used to store information about each function, results go here
|
||
|
:param tu: the translation unit
|
||
|
"""
|
||
|
|
||
|
# 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 .*$')
|
||
|
|
||
|
with open(tu + rtl_ext, "rt", encoding="latin_1") as file_:
|
||
|
for line_ in file_:
|
||
|
m = function.match(line_)
|
||
|
if m:
|
||
|
fxn_name = m.group(2)
|
||
|
fxn_dict2 = self.find_fxn(tu, fxn_name)
|
||
|
if not fxn_dict2:
|
||
|
pprint.pprint(self)
|
||
|
raise Exception(f"Error locating function {fxn_name} in {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(self, tu: str) -> None:
|
||
|
"""
|
||
|
Reads the 'local_stack' for each function. Local stack ignores stack used by callees.
|
||
|
:param self: a object used to store information about each function, results go here
|
||
|
:param tu: the translation unit
|
||
|
:return:
|
||
|
"""
|
||
|
# Needs to be able to handle both cases, i.e.:
|
||
|
# c:\\userlibs\\gcc\\arm-none-eabi\\include\\assert.h:41:6:__assert_func 16 static
|
||
|
# main.c:113:6:vAssertCalled 8 static
|
||
|
# Now Matches six groups https://regex101.com/r/Imi0sq/1
|
||
|
|
||
|
su_line = re.compile(r'^(.+):(\d+):(\d+):(.+)\t(\d+)\t(\S+)$')
|
||
|
i = 1
|
||
|
|
||
|
with open(tu[0:tu.rindex(".")] + su_ext, "rt", encoding="latin_1") as file_:
|
||
|
for line in file_:
|
||
|
m = su_line.match(line)
|
||
|
if m:
|
||
|
fxn = m.group(4)
|
||
|
fxn_dict2 = self.find_demangled_fxn(tu, fxn)
|
||
|
fxn_dict2['local_stack'] = int(m.group(5))
|
||
|
else:
|
||
|
print(f"error parsing line {i} in file {tu}")
|
||
|
i += 1
|
||
|
|
||
|
def read_manual(self, file: str) -> None:
|
||
|
"""
|
||
|
reads the manual stack useage files.
|
||
|
:param self: a object used to store information about each function, results go here
|
||
|
:param file: the file name
|
||
|
"""
|
||
|
|
||
|
with open(file, "rt", encoding="latin_1") as file_:
|
||
|
for line in file_:
|
||
|
fxn, stack_sz = line.split()
|
||
|
if fxn in self.globals:
|
||
|
raise Exception(f"Redeclared Function {fxn}")
|
||
|
self.globals[fxn] = {'wcs': int(stack_sz),
|
||
|
'calls': set(),
|
||
|
'has_ptr_call': False,
|
||
|
'local_stack': int(stack_sz),
|
||
|
'is_manual': True,
|
||
|
'name': fxn,
|
||
|
'demangledName': fxn,
|
||
|
'tu': '#MANUAL',
|
||
|
'binding': 'GLOBAL'}
|
||
|
|
||
|
def validate_all_data(self) -> None:
|
||
|
"""
|
||
|
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(f"Error data is missing in fxn dictionary {d}")
|
||
|
|
||
|
# Loop through every global and local function
|
||
|
# and resolve each call, save results in r_calls
|
||
|
for fxn_dict2 in self.globals.values():
|
||
|
validate_dict(fxn_dict2)
|
||
|
|
||
|
for l_dict in self.locals.values():
|
||
|
for fxn_dict2 in l_dict.values():
|
||
|
validate_dict(fxn_dict2)
|
||
|
|
||
|
def resolve_all_calls(self) -> None:
|
||
|
def resolve_calls(fxn_dict2: CallNode) -> None:
|
||
|
fxn_dict2['r_calls'] = []
|
||
|
fxn_dict2['unresolved_calls'] = set()
|
||
|
|
||
|
for call in fxn_dict2['calls']:
|
||
|
call_dict = self.find_fxn(fxn_dict2['tu'], call)
|
||
|
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 self.globals.values():
|
||
|
resolve_calls(fxn_dict)
|
||
|
|
||
|
for l_dict in self.locals.values():
|
||
|
for fxn_dict in l_dict.values():
|
||
|
resolve_calls(fxn_dict)
|
||
|
|
||
|
def calc_all_wcs(self) -> None:
|
||
|
# Loop through every global and local function
|
||
|
# and resolve each call, save results in r_calls
|
||
|
for fxn_dict in self.globals.values():
|
||
|
calc_wcs(fxn_dict, [])
|
||
|
|
||
|
for l_dict in self.locals.values():
|
||
|
for fxn_dict in l_dict.values():
|
||
|
calc_wcs(fxn_dict, [])
|
||
|
|
||
|
def print_all_fxns(self) -> None:
|
||
|
|
||
|
def print_fxn(row_format: str, fxn_dict2: CallNode) -> None:
|
||
|
unresolved = fxn_dict2['unresolved_calls']
|
||
|
stack = str(fxn_dict2['wcs'])
|
||
|
if unresolved:
|
||
|
unresolved_str = f"({' ,'.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) -> int:
|
||
|
return 1 if val == 'unbounded' else -val
|
||
|
|
||
|
# Loop through every global and local function
|
||
|
# and resolve each call, save results in r_calls
|
||
|
d_list = []
|
||
|
for fxn_dict in self.globals.values():
|
||
|
d_list.append(fxn_dict)
|
||
|
|
||
|
for l_dict in self.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 read_symbols(file: str) -> List[Symbol]:
|
||
|
|
||
|
def to_symbol(read_elf_line: str) -> Symbol:
|
||
|
v = read_elf_line.split()
|
||
|
|
||
|
s2 = Symbol()
|
||
|
# s2.value = int(v[1], 16)
|
||
|
# if ('x' in v[2]):
|
||
|
# #raise Exception(f'Mixed symbol sizes in \'{v}\' ')
|
||
|
# s2.size=int(v[2].split('x')[1],16)
|
||
|
# else:
|
||
|
# s2.size = int(v[2])
|
||
|
s2.type = v[3]
|
||
|
s2.binding = v[4]
|
||
|
s2.name = v[7] if len(v) >= 8 else ""
|
||
|
|
||
|
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 find_rtl_ext() -> str:
|
||
|
# Find the rtl_extension
|
||
|
|
||
|
for _, _, 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 rtl_ext
|
||
|
|
||
|
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")
|
||
|
sys.exit(-1)
|
||
|
|
||
|
|
||
|
def find_files(rtl_ext: str) -> Tuple[List[str], List[str]]:
|
||
|
tu: List[str] = []
|
||
|
manual: List[str] = []
|
||
|
all_files: List[str] = []
|
||
|
for root, _, filenames in os.walk('.'):
|
||
|
for filename in filenames:
|
||
|
all_files.append(os.path.join(root, filename))
|
||
|
for f in [f for f in all_files if os.path.isfile(f) and f.endswith(rtl_ext)]:
|
||
|
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(f'Reading: {base}{rtl_ext}, {short_base}{su_ext}, {short_base}{obj_ext}')
|
||
|
|
||
|
for f in [f for f in all_files if os.path.isfile(f) and f.endswith(manual_ext)]:
|
||
|
manual.append(f)
|
||
|
print(f'Reading: {f}')
|
||
|
|
||
|
# Print some diagnostic messages
|
||
|
if not tu:
|
||
|
print("Could not find any translation units to analyse")
|
||
|
sys.exit(-1)
|
||
|
|
||
|
return tu, manual
|
||
|
|
||
|
|
||
|
def main() -> None:
|
||
|
# Find the appropriate RTL extension
|
||
|
rtl_ext = find_rtl_ext()
|
||
|
|
||
|
# Find all input files
|
||
|
call_graph: CallGraph = CallGraph()
|
||
|
tu_list, manual_list = find_files(rtl_ext)
|
||
|
|
||
|
# Read the input files
|
||
|
for tu in tu_list:
|
||
|
call_graph.read_obj(tu) # This must be first
|
||
|
|
||
|
for fxn in call_graph.weak.values():
|
||
|
if fxn['name'] not in call_graph.globals:
|
||
|
call_graph.globals[fxn['name']] = fxn
|
||
|
|
||
|
for tu in tu_list:
|
||
|
call_graph.read_rtl(tu, rtl_ext)
|
||
|
for tu in tu_list:
|
||
|
call_graph.read_su(tu)
|
||
|
|
||
|
# Read manual files
|
||
|
for m in manual_list:
|
||
|
call_graph.read_manual(m)
|
||
|
|
||
|
# Validate Data
|
||
|
call_graph.validate_all_data()
|
||
|
|
||
|
# Resolve All Function Calls
|
||
|
call_graph.resolve_all_calls()
|
||
|
|
||
|
# Calculate Worst Case Stack For Each Function
|
||
|
call_graph.calc_all_wcs()
|
||
|
|
||
|
# Print A Nice Message With Each Function and the WCS
|
||
|
call_graph.print_all_fxns()
|
||
|
|
||
|
|
||
|
main()
|