diff --git a/bsp/stm32/stm32l475-atk-pandora/rtconfig.py b/bsp/stm32/stm32l475-atk-pandora/rtconfig.py index 573c1f7492e78800782e5d1fc1d94419f7db295f..c29aca2346696abb66556214e17040ad3b493d4b 100644 --- a/bsp/stm32/stm32l475-atk-pandora/rtconfig.py +++ b/bsp/stm32/stm32l475-atk-pandora/rtconfig.py @@ -44,7 +44,7 @@ if PLATFORM == 'gcc': OBJCPY = PREFIX + 'objcopy' DEVICE = ' -mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections' - CFLAGS = DEVICE + ' -Dgcc' + CFLAGS = DEVICE + ' -Dgcc -fstack-usage' 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' diff --git a/tools/building.py b/tools/building.py index 2ddeb9669b8a4fda7d0a8dc1c860785f4a351d35..ff388346296e01774302016f4885939ed6b0b1b4 100644 --- a/tools/building.py +++ b/tools/building.py @@ -209,6 +209,11 @@ def PrepareBuilding(env, root_directory, has_libcpu=False, remove_components = [ dest = 'target', type = 'string', 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', dest = 'genconfig', action = 'store_true', @@ -363,6 +368,11 @@ def PrepareBuilding(env, root_directory, has_libcpu=False, remove_components = [ genconfig() exit(0) + if GetOption('stackanalysis'): + from stackanalysis import ThreadStackStaticAnalysis + ThreadStackStaticAnalysis(Env) + exit(0) + if env['PLATFORM'] != 'win32': AddOption('--menuconfig', dest = 'menuconfig', diff --git a/tools/stack.pl b/tools/stack.pl new file mode 100644 index 0000000000000000000000000000000000000000..fa297ae7e4bacff2d26f16ec9d4fada00f735322 --- /dev/null +++ b/tools/stack.pl @@ -0,0 +1,251 @@ +#!/usr/bin/perl -w +# avstack.pl: AVR stack checker +# Copyright (C) 2013 Daniel Beer +# +# Permission to use, copy, modify, and/or distribute this software for +# any purpose with or without fee is hereby granted, provided that the +# above copyright notice and this permission notice appear in all +# copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +# WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +# AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL +# DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR +# PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +# PERFORMANCE OF THIS SOFTWARE. +# +# Usage +# ----- +# +# This script requires that you compile your code with -fstack-usage. +# This results in GCC generating a .su file for each .o file. Once you +# have these, do: +# +# ./avstack.pl +# +# This will disassemble .o files to construct a call graph, and read +# frame size information from .su. The call graph is traced to find, for +# each function: +# +# - Call height: the maximum call height of any callee, plus 1 +# (defined to be 1 for any function which has no callees). +# +# - Inherited frame: the maximum *inherited* frame of any callee, plus +# the GCC-calculated frame size of the function in question. +# +# Using these two pieces of information, we calculate a cost (estimated +# peak stack usage) for calling the function. Functions are then listed +# on stdout in decreasing order of cost. +# +# Functions which are recursive are marked with an 'R' to the left of +# them. Their cost is calculated for a single level of recursion. +# +# The peak stack usage of your entire program can usually be estimated +# as the stack cost of "main", plus the maximum stack cost of any +# interrupt handler which might execute. + +use strict; + +# Configuration: set these as appropriate for your architecture/project. + +my $objdump = "E:/RT_Thread_Env/tools/gnu_gcc/arm_gcc/mingw/bin/arm-none-eabi-objdump"; +my $call_cost = 4; + +# First, we need to read all object and corresponding .su files. We're +# gathering a mapping of functions to callees and functions to frame +# sizes. We're just parsing at this stage -- callee name resolution +# comes later. + +my %frame_size; # "func@file" -> size +my %call_graph; # "func@file" -> {callees} +my %addresses; # "addr@file" -> "func@file" + +my %global_name; # "func" -> "func@file" +my %ambiguous; # "func" -> 1 + +foreach (@ARGV) { + # Disassemble this object file to obtain a callees. Sources in the + # call graph are named "func@file". Targets in the call graph are + # named either "offset@file" or "funcname". We also keep a list of + # the addresses and names of each function we encounter. + my $objfile = $_; + my $source; + + open(DISASSEMBLY, "$objdump -dr $objfile|") || + die "Can't disassemble $objfile"; + while () { + chomp; + + if (/^([0-9a-fA-F]+) <(.*)>:/) { + my $a = $1; + my $name = $2; + + $source = "$name\@$objfile"; + $call_graph{$source} = {}; + $ambiguous{$name} = 1 if defined($global_name{$name}); + $global_name{$name} = "$name\@$objfile"; + + $a =~ s/^0*//; + $addresses{"$a\@$objfile"} = "$name\@$objfile"; + } + + if (/: R_[A-Za-z0-9_]+_CALL[ \t]+(.*)/) { + my $t = $1; + + if ($t eq ".text") { + $t = "\@$objfile"; + } elsif ($t =~ /^\.text\+0x(.*)$/) { + $t = "$1\@$objfile"; + } + + $call_graph{$source}->{$t} = 1; + } + } + close(DISASSEMBLY); + + # Extract frame sizes from the corresponding .su file. + if ($objfile =~ /^(.*).o$/) { + my $sufile = "$1.su"; + + open(SUFILE, "<$sufile") || die "Can't open $sufile"; + while () { + $frame_size{"$1\@$objfile"} = $2 + $call_cost + if /^.*:([^\t ]+)[ \t]+([0-9]+)/; + } + close(SUFILE); + } +} + +# In this step, we enumerate each list of callees in the call graph and +# try to resolve the symbols. We omit ones we can't resolve, but keep a +# set of them anyway. + +my %unresolved; + +foreach (keys %call_graph) { + my $from = $_; + my $callees = $call_graph{$from}; + my %resolved; + + foreach (keys %$callees) { + my $t = $_; + + if (defined($addresses{$t})) { + $resolved{$addresses{$t}} = 1; + } elsif (defined($global_name{$t})) { + $resolved{$global_name{$t}} = 1; + warn "Ambiguous resolution: $t" if defined ($ambiguous{$t}); + } elsif (defined($call_graph{$t})) { + $resolved{$t} = 1; + } else { + $unresolved{$t} = 1; + } + } + + $call_graph{$from} = \%resolved; +} + +# Create fake edges and nodes to account for dynamic behaviour. +$call_graph{"INTERRUPT"} = {}; + +foreach (keys %call_graph) { + $call_graph{"INTERRUPT"}->{$_} = 1 if /Handler/; +} + +# Trace the call graph and calculate, for each function: +# +# - inherited frames: maximum inherited frame of callees, plus own +# frame size. +# - height: maximum height of callees, plus one. +# - recursion: is the function called recursively (including indirect +# recursion)? + +my %has_caller; +my %visited; +my %total_cost; +my %call_depth; + +sub trace { + my $f = shift; + + if ($visited{$f}) { + $visited{$f} = "R" if $visited{$f} eq "?"; + return; + } + + $visited{$f} = "?"; + + my $max_depth = 0; + my $max_frame = 0; + + my $targets = $call_graph{$f} || die "Unknown function: $f"; + if (defined($targets)) { + foreach (keys %$targets) { + my $t = $_; + + $has_caller{$t} = 1; + trace($t); + + my $is = $total_cost{$t}; + my $d = $call_depth{$t}; + + $max_frame = $is if $is > $max_frame; + $max_depth = $d if $d > $max_depth; + } + } + + $call_depth{$f} = $max_depth + 1; + $total_cost{$f} = $max_frame + ($frame_size{$f} || 0); + $visited{$f} = " " if $visited{$f} eq "?"; +} + +foreach (keys %call_graph) { trace $_; } + +# Now, print results in a nice table. +printf " %-30s %8s %8s %8s\n", + "Func", "Cost", "Frame", "Height"; +print "------------------------------------"; +print "------------------------------------\n"; + +my $max_iv = 0; +my $main = 0; + +foreach (sort { $total_cost{$b} <=> $total_cost{$a} } keys %visited) { + my $name = $_; + + if (/^(.*)@(.*)$/) { + $name = $1 unless $ambiguous{$name}; + } + + my $tag = $visited{$_}; + my $cost = $total_cost{$_}; + + $name = $_ if $ambiguous{$name}; + $tag = ">" unless $has_caller{$_}; + + if (/^__vector_/) { + $max_iv = $cost if $cost > $max_iv; + } elsif (/^main@/) { + $main = $cost; + } + + if ($ambiguous{$name}) { $name = $_; } + + printf "%s %-30s %8d %8d %8d\n", $tag, $name, $cost, + $frame_size{$_} || 0, $call_depth{$_}; +} + +print "\n"; + +print "Peak execution estimate (main + worst-case IV):\n"; +printf " main = %d, worst IV = %d, total = %d\n", + $total_cost{$global_name{"main"}}, + $total_cost{"INTERRUPT"}, + $total_cost{$global_name{"main"}} + $total_cost{"INTERRUPT"}; + +print "\n"; + +print "The following functions were not resolved:\n"; +foreach (keys %unresolved) { print " $_\n"; } \ No newline at end of file diff --git a/tools/stackanalysis.py b/tools/stackanalysis.py new file mode 100644 index 0000000000000000000000000000000000000000..6f33d52ce891f7670a9b35884c9bf7d961dc339e --- /dev/null +++ b/tools/stackanalysis.py @@ -0,0 +1,30 @@ +import os + +ofile = [] + +def searchofile(root): + global ofile + items = os.listdir(root) + for item in items: + path = os.path.join(root, item) + if os.path.isdir(path): + searchofile(path) + elif path.split('/')[-1].endswith('.o'): + if os.path.exists(path.replace('.o','.su')): + ofile.append(path) + else: + pass + + +def ThreadStackStaticAnalysis(env): + print('Start thread stack static analysis...') + + global ofile + file_dir = env['BSP_ROOT']+'/build' + searchofile(file_dir) + cmd = 'perl ' + env['RTT_ROOT'].replace(env['BSP_ROOT'],'.') + '/tools/stack.pl ' + ' '.join(ofile) + os.system(cmd) + + print('\n\ncmd:\n' + cmd) + print('\nThread stack static analysis done!') + return