new file mode 120000
@@ -0,0 +1 @@
+spl-stackusage.rb
\ No newline at end of file
new file mode 100755
@@ -0,0 +1,471 @@
+#!/usr/bin/env ruby
+#
+# Copyright © 2015 Siarhei Siamashka <siarhei.siamashka@gmail.com>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+###############################################################################
+# This is a u-boot SPL stack usage analysis script. Needs to be run #
+# from the root of the u-boot source tree after a successful SPL build. #
+# #
+# Produces 'spl-stackusage.png' file as the output. #
+###############################################################################
+
+require 'find'
+
+###############################################################################
+# Process command line arguments
+#
+# add_node[node_name, stack_usage]
+# remove_node[node_name, stack_usage]
+# add_arrow[node_name1, node_name2]
+# remove_arrow[node_name1, node_name2]
+#
+###############################################################################
+
+$removed_arrows = {}
+
+ARGV.each {|a|
+ a.split(";").each {|a|
+ if a =~ /remove_arrow[\(\[]\s*(\S+)\s*\,\s*(\S+)\s*[\)\]]/
+ $removed_arrows[[$1, $2]] = 1
+ end
+ }
+}
+
+###############################################################################
+
+if !File.directory?("spl")
+ abort("Error: can't find the 'spl' directory.\n" +
+ "Please ensure that you are in the root of the u-boot source tree.\n")
+end
+
+def tool_exists(tool_name)
+ `which #{tool_name} > /dev/null 2>&1`
+ return $?.to_i == 0
+end
+
+if !tool_exists("dot")
+ abort("Please install the graphviz dot tool.\n")
+end
+
+###############################################################################
+# Get objdump log #
+###############################################################################
+
+# Get the information from the environment
+$toolchain = ENV["CROSS_COMPILE"]
+
+# Try to guess the ARM toolchain automatically
+if !$toolchain && `file spl/u-boot-spl` =~ / ARM, /
+ toolchain_list = [
+ "arm-none-linux-gnueabi-",
+ "armv7a-hardfloat-linux-gnueabi-",
+ "arm-linux-gnueabihf-"
+ ]
+ toolchain_list.each {|toolchain|
+ if tool_exists("#{toolchain}objdump")
+ $toolchain = toolchain
+ break
+ end
+ }
+end
+
+if !$toolchain
+ abort("Error: can't find a usable toolchain.\n" +
+ "Try to set the CROSS_COMPILE environment variable.\n")
+end
+
+$objdump_dis_log = `#{$toolchain}objdump -d -M reg-names-raw spl/u-boot-spl`
+$objdump_sym_log = `#{$toolchain}objdump -t spl/u-boot-spl`
+$size_log = `#{$toolchain}size spl/u-boot-spl`
+
+File.open("spl-stackusage.dis", "w").write($objdump_dis_log)
+File.open("spl-stackusage.sym", "w").write($objdump_sym_log)
+
+###############################################################################
+# Read the *.su files (generated by GCC with '-fstack-usage' option) #
+###############################################################################
+
+$dynamic_stack_size = {}
+$self_stack_size = {}
+$total_stack_size = {}
+
+Find.find("spl") { |filename|
+ next unless filename =~ /.*\.su$/
+ File.open(filename).each_line {|l|
+ if l =~ /\:([^\:\s]+)\s+(\d+)\s+(\S+)$/
+ case $3
+ when "dynamic"
+ $dynamic_stack_size[$1] = true
+ when "static"
+ $self_stack_size[$1] = [$2.to_i, ($self_stack_size[$1] || 0)].max
+ else
+ abort("Unsupported stack usage type #{$3} in #{filename}\n")
+ end
+ end
+ }
+}
+
+if $self_stack_size.size == 0
+ abort("Error: could not load anything from *.su files in 'spl' directory.\n" +
+ "Please ensure that you are using '-fstack-usage' GCC option.\n")
+end
+
+###############################################################################
+# Parse the objdump log #
+###############################################################################
+
+$detected_cycles = {}
+$labels = {}
+$arrows = {}
+$functions = {}
+$func_code = {}
+$datastructs = {}
+$refs = {}
+$xrefs = {}
+$max_stack_size = 0
+$critical_path = {}
+$shortest_cycle = nil
+$indirect_targets = {}
+$indirect_sources = {}
+$blue_arrows = {}
+
+# Stack usage, based on inspecting disassembly listing in objdump log
+def guess_func_stack_usage(func_name)
+ func_code = $func_code[func_name]
+ if !func_code
+ printf("No function code for '%s'\n", func_name)
+ return nil
+ end
+ stack_usage = 0
+ func_code.each_line { |l|
+ # 'push {reglist}' instruction
+ if l =~ /push\s+\{\s*(.*?)\s*\}/
+ reglist = $1.split(",")
+ abort("Register ranges are not supported") if $1 =~ /\-/
+ stack_usage += reglist.size * 4
+ end
+ # 'sub sp, sp, #imm' instruction
+ if l =~ /sub\s+r13\s*\,\s*(r13\s*)\,\s*\#(\d+)/
+ stack_usage += $2.to_i
+ end
+ }
+ return stack_usage
+end
+
+# Stack usage, based on parsing *.su files
+def get_func_stack_usage(func_name)
+ stack_usage = $self_stack_size[func_name]
+ stack_usage = guess_func_stack_usage(func_name) if !stack_usage
+ return stack_usage
+end
+
+$objdump_sym_log.each_line {|l|
+ # 000034c8 l F .text 000000c8 twsi_i2c_read
+ next unless l =~ /^(\h+)\s+\S+\s+(\S+)\s+(\.\S+)\s+\h+.*?\s+(\S+)\s*$/
+ addr = $1
+ type = $2
+ sect = $3
+ name = $4
+ if type == "F" && sect == ".text"
+ $functions[name] = addr.to_i(16)
+ else
+ $datastructs[name] = addr.to_i(16)
+ end
+}
+
+$functions.each { |func, _|
+ if $dynamic_stack_size.has_key?(func)
+ abort("dynamic stack size in #{func}")
+ end
+}
+
+Find.find("spl") { |filename|
+ next unless filename =~ /.*\.o$/
+ skipflag = false
+ current_section = ""
+ `#{$toolchain}objdump -r #{filename}`.each_line {|l|
+ if l =~ /RELOCATION RECORDS FOR/
+ if l =~ /\[\.text\.(.*?)\]/
+ skipflag = !$functions.has_key?($1)
+ elsif l =~ /\[\.(data|rodata)\.(.*?)\]/
+ skipflag = !$datastructs.has_key?($2)
+ else
+ skipflag = false
+ end
+ end
+
+ next if skipflag
+
+ if l =~ /(\S+)\s+(\S+)\s+(\S+)/
+ reltype = $2
+ name = $3
+ next if !$functions.has_key?(name)
+ next if reltype == "R_ARM_CALL" || reltype == "R_ARM_JUMP24"
+ next if reltype == "R_ARM_THM_CALL" || reltype == "R_ARM_THM_JUMP24"
+ $indirect_targets[name] = 1
+ end
+ }
+}
+
+def add_ref(current_func, func_name)
+ return if $removed_arrows.has_key?([current_func, func_name])
+
+ $arrows[[current_func, func_name]] = 1
+ $refs[current_func] = {} if !$refs.has_key?(current_func)
+ $refs[func_name] = {} if !$refs.has_key?(func_name)
+ $refs[current_func][func_name] = true
+
+ $xrefs[current_func] = {} if !$xrefs.has_key?(current_func)
+ $xrefs[func_name] = {} if !$xrefs.has_key?(func_name)
+ $xrefs[func_name][current_func] = true
+end
+
+# First pass, collect local aliases
+current_func = "entry"
+current_label = "entry"
+$label_to_func_mapping = {}
+$objdump_dis_log.each_line {|l|
+ current_label = $1 if l =~ /^\h+\s+\<(.*?)>\:$/
+ current_func = $1 if l =~ /^\h+\s+\<(.*?)>\:$/ && $functions.has_key?($1)
+ $label_to_func_mapping[current_label] = current_func
+}
+
+current_func = "entry"
+$objdump_dis_log.each_line {|l|
+ current_func = $1 if l =~ /^\h+\s+\<(.*?)>\:$/ && $functions.has_key?($1)
+ if l =~ /(.*?)\<(.*?)(\+0x\h+)?\>/
+ target_func = $label_to_func_mapping[$2]
+ add_ref(current_func, target_func) if target_func != current_func
+ end
+ if l =~ /\s+blx?\s+r/
+ $indirect_sources[current_func] = 1
+ end
+ if l =~ /\s+bx?\s+r/ && !(l =~ /\s+bx?\s+r14/)
+ $indirect_sources[current_func] = 1
+ end
+ $func_code[current_func] = "" unless $func_code.has_key?(current_func)
+ $func_code[current_func] << sprintf("%s\n", l.strip)
+}
+
+###############################################################################
+# Handle indirect calls #
+###############################################################################
+
+$functions["<indirect-call>"] = 1
+$self_stack_size["<indirect-call>"] = 0
+
+$multiplexed_indirect_sources = $indirect_sources.clone
+$multiplexed_indirect_targets = $indirect_targets.clone
+
+# A function can't indirectly call itself (that would be a recursion)
+$indirect_sources.each {|src, _|
+ $removed_arrows[[src, src]] = 1
+}
+# A function can't indirectly call its caller (that would be a recursion)
+$refs.each {|src, dst_list|
+ dst_list.each {|dst, _|
+ $removed_arrows[[dst, src]] = 1
+ }
+}
+
+def impossible_indirect_call(bad_src, bad_dst)
+ return unless $multiplexed_indirect_sources.has_key?(bad_src)
+ return unless $multiplexed_indirect_targets.has_key?(bad_dst)
+
+ if $multiplexed_indirect_sources.size < $multiplexed_indirect_targets.size
+ $multiplexed_indirect_sources.delete(bad_src)
+ $multiplexed_indirect_targets.each {|dst, _|
+ next if dst == bad_dst
+ newarrow = [bad_src, dst]
+ if !($removed_arrows.has_key?(newarrow))
+ add_ref(newarrow[0], newarrow[1])
+ $blue_arrows[newarrow] = 1
+ end
+ }
+ else
+ $multiplexed_indirect_targets.delete(bad_dst)
+ $multiplexed_indirect_sources.each {|src, _|
+ next if src == bad_src
+ newarrow = [src, bad_dst]
+ if !($removed_arrows.has_key?(newarrow))
+ add_ref(newarrow[0], newarrow[1])
+ $blue_arrows[newarrow] = 1
+ end
+ }
+ end
+end
+
+$removed_arrows.each {|k, _|
+ impossible_indirect_call(k[0], k[1])
+}
+
+$multiplexed_indirect_sources.each {|src, _|
+ newarrow = [src, "<indirect-call>"]
+ add_ref(newarrow[0], newarrow[1])
+ $blue_arrows[newarrow] = 1
+}
+
+$multiplexed_indirect_targets.each {|dst, _|
+ newarrow = ["<indirect-call>", dst]
+ add_ref(newarrow[0], newarrow[1])
+ $blue_arrows[newarrow] = 1
+}
+
+###############################################################################
+
+def recursive_walk(node, visited_nodes, depth, total_stack)
+ $total_stack_size[node] = [total_stack, ($total_stack_size[node] || 0)].max
+ if $total_stack_size[node] > $max_stack_size
+ $critical_path = visited_nodes.clone
+ $max_stack_size = $total_stack_size[node]
+ end
+ $refs[node].each {|next_node, _|
+ func_stack = get_func_stack_usage(next_node)
+ if visited_nodes.has_key?(next_node) then
+ # Find shortest cycle
+ tmp = visited_nodes.sort {|a, b| a[1] <=> b[1]}.map {|a| a[0]}
+ idx = tmp.find_index(next_node)
+ tmp = tmp.slice(idx, tmp.size - idx)
+ $shortest_cycle = tmp if !$shortest_cycle
+ $shortest_cycle = tmp if tmp.size < $shortest_cycle.size
+ $detected_cycles[[node, next_node]] = 1
+ next
+ end
+ visited_nodes[next_node] = depth if next_node != "<indirect-call>"
+ recursive_walk(next_node, visited_nodes, depth + 1,
+ total_stack + func_stack)
+ visited_nodes.delete(next_node)
+ }
+end
+
+# Find the 'root' nodes and recursively traverse the graph starting from them
+$xrefs.each {|func_name, nodes|
+ next if nodes.size != 0
+ visited_nodes = {}
+ depth = 0
+ visited_nodes[func_name] = depth
+ recursive_walk(func_name, visited_nodes, depth + 1,
+ get_func_stack_usage(func_name))
+}
+
+###############################################################################
+# Generate .dot file for graphviz #
+###############################################################################
+
+$legend = ""
+$max_stack_size = $total_stack_size.map { |k, v| v }.max
+
+def format_path_arr(path_arr)
+ result = ""
+ (path_arr + [path_arr[0]]).each_cons(2) { |a|
+ normal_call = $refs[a[0]].has_key?(a[1]) &&
+ !$blue_arrows.has_key?([a[0], a[1]])
+ indirect_call = ($refs[a[0]].has_key?("<indirect-call>") &&
+ $refs["<indirect-call>"].has_key?(a[1])) ||
+ $blue_arrows.has_key?([a[0], a[1]])
+ if normal_call
+ result << sprintf("%s > ", a[0])
+ elsif indirect_call
+ result << sprintf("%s ? ", a[0])
+ else
+ result << sprintf("%s ! ", a[0])
+ end
+ }
+ return result + path_arr[0]
+end
+
+if $size_log =~ /(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\h+)\s+spl\/u\-boot\-spl/
+ text_plus_data_size = $1.to_i + $2.to_i
+ bss_size = $3.to_i
+
+ $legend << sprintf("------------ u-boot SPL ------------\\l")
+ $legend << sprintf("text+data = %5d bytes\\l", text_plus_data_size)
+ $legend << sprintf("bss = %5d bytes\\l", bss_size)
+ $legend << sprintf("stack usage = %5d bytes\\l", $max_stack_size)
+end
+
+if $detected_cycles.size != 0
+ $legend << sprintf("--------------------------------------\\l")
+ $legend << sprintf("Warning, recursion suspected:\\l\\\"%s\\\"\\l",
+ format_path_arr($shortest_cycle))
+ $legend << sprintf("\\lWhere:")
+ $legend << sprintf("\\l '>' - means normal function call")
+ $legend << sprintf("\\l '?' - indirect call (via a pointer)\\l")
+ $legend << sprintf("\\lBlue arrows highlight possible indirect calls")
+ $legend << sprintf("\\lgoing through a fake multiplexer node. Red")
+ $legend << sprintf("\\larrows highlight possible cases of recursion")
+ $legend << sprintf("\\lon the callgraph. Recursion is bad because it")
+ $legend << sprintf("\\lmakes stack usage estimation unpredictable.\\l")
+ $legend << sprintf("\\lThe recursion warnings can be eliminated by:")
+ $legend << sprintf("\\l 1) Fixing the SPL code if it is really broken.")
+ $legend << sprintf("\\l 2) Incorrectly guessed indirect calls can")
+ $legend << sprintf("\\l be suppressed by using one or more")
+ $legend << sprintf("\\l 'remove_arrow[caller,callee]' command")
+ $legend << sprintf("\\l line arguments passed to the script.\\l")
+ $legend << sprintf("\\lThe nodes on the execution path with maximized")
+ $legend << sprintf("\\lstack usage are orange colored.\\l")
+end
+
+fh = File.open("spl-stackusage.dot", "w")
+
+$refs.each {|func_name, nodes|
+ $labels[func_name] = sprintf("%s\\n[total=%d, self=%d]",
+ func_name,
+ $total_stack_size[func_name],
+ get_func_stack_usage(func_name))
+}
+
+fh.printf("digraph graphname {\n")
+fh.printf("rankdir=LR;\n")
+fh.printf("node [shape=box,fontname=helvetica];\n")
+
+$total_stack_size.each { |k, v|
+ extra = ""
+ shape = "box"
+ shape = "octagon" if $indirect_sources.has_key?(k)
+ shape = "doubleoctagon" if $indirect_targets.has_key?(k)
+ if $critical_path.has_key?(k)
+ extra = ",style=filled,fillcolor=orange,group=main"
+ end
+ if k == "<indirect-call>"
+ shape = "oval"
+ extra = ",style=filled,fillcolor=lightblue"
+ end
+ fh.printf("\"%s\" [shape=#{shape}#{extra}];\n", $labels[k])
+}
+
+$arrows.each {|k, v|
+ if $detected_cycles.has_key?(k) then
+ fh.printf("\"%s\" -> \"%s\" [color=\"red\",penwidth=3];\n",
+ $labels[k[0]], $labels[k[1]])
+ elsif $blue_arrows.has_key?(k) then
+ fh.printf("\"%s\" -> \"%s\" [color=blue];\n",
+ $labels[k[0]], $labels[k[1]])
+ else
+ fh.printf("\"%s\" -> \"%s\";\n", $labels[k[0]], $labels[k[1]])
+ end
+}
+fh.printf("legend [shape=box,style=filled,fontname=monospace,fontsize=24," +
+ "color=\"lightgray\",label=\"#{$legend}\"];\n")
+fh.printf("}\n")
+fh.close
+
+printf("Execution path with maximized stack usage (%d bytes):\n%s\n",
+ $max_stack_size,
+ format_path_arr($critical_path.invert.sort.map { |a| a[1] }))
+
+`dot -Tpng -o spl-stackusage.png spl-stackusage.dot`