Browse Source

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

RT-Thread 4 years ago
parent
commit
fe67df3408
3 changed files with 451 additions and 1 deletions
  1. 1 1
      bsp/stm32/stm32l475-atk-pandora/rtconfig.py
  2. 440 0
      tools/WCS.py
  3. 10 0
      tools/building.py

+ 1 - 1
bsp/stm32/stm32l475-atk-pandora/rtconfig.py

@@ -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 - 0
tools/WCS.py

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

+ 10 - 0
tools/building.py

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