!29 【开源贡献活动】 增加线程栈静态分析功能(python版本) https://gitee.com/rtthread/rt-thread/issues/I1I3UR

Merge pull request !29 from 来日方长/stackchecker_WorstCaseStack_giteemaster
This commit is contained in:
RT-Thread 2020-06-24 19:28:00 +08:00 committed by Gitee
commit fe67df3408
3 changed files with 451 additions and 1 deletions

View File

@ -44,7 +44,7 @@ if PLATFORM == 'gcc':
OBJCPY = PREFIX + 'objcopy' OBJCPY = PREFIX + 'objcopy'
DEVICE = ' -mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections' DEVICE = ' -mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections'
CFLAGS = DEVICE + ' -Dgcc' CFLAGS = DEVICE + ' -Dgcc -fstack-usage -fdump-rtl-dfinish'
AFLAGS = ' -c' + DEVICE + ' -x assembler-with-cpp -Wa,-mimplicit-it=thumb ' AFLAGS = ' -c' + DEVICE + ' -x assembler-with-cpp -Wa,-mimplicit-it=thumb '
LFLAGS = DEVICE + ' -Wl,--gc-sections,-Map=rt-thread.map,-cref,-u,Reset_Handler -T board/linker_scripts/link.lds' LFLAGS = DEVICE + ' -Wl,--gc-sections,-Map=rt-thread.map,-cref,-u,Reset_Handler -T board/linker_scripts/link.lds'

440
tools/WCS.py Normal file
View File

@ -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

View File

@ -209,6 +209,11 @@ def PrepareBuilding(env, root_directory, has_libcpu=False, remove_components = [
dest = 'target', dest = 'target',
type = 'string', type = 'string',
help = 'set target project: mdk/mdk4/mdk5/iar/vs/vsc/ua/cdk/ses/makefile/eclipse') help = 'set target project: mdk/mdk4/mdk5/iar/vs/vsc/ua/cdk/ses/makefile/eclipse')
AddOption('--stackanalysis',
dest = 'stackanalysis',
action = 'store_true',
default = False,
help = 'thread stack static analysis')
AddOption('--genconfig', AddOption('--genconfig',
dest = 'genconfig', dest = 'genconfig',
action = 'store_true', action = 'store_true',
@ -363,6 +368,11 @@ def PrepareBuilding(env, root_directory, has_libcpu=False, remove_components = [
genconfig() genconfig()
exit(0) exit(0)
if GetOption('stackanalysis'):
from WCS import ThreadStackStaticAnalysis
ThreadStackStaticAnalysis(Env)
exit(0)
if env['PLATFORM'] != 'win32': if env['PLATFORM'] != 'win32':
AddOption('--menuconfig', AddOption('--menuconfig',
dest = 'menuconfig', dest = 'menuconfig',