| #!/usr/bin/env python | 
 | # Script that tries to find how much stack space each function in an | 
 | # object is using. | 
 | # | 
 | # Copyright (C) 2008-2015  Kevin O'Connor <kevin@koconnor.net> | 
 | # | 
 | # This file may be distributed under the terms of the GNU GPLv3 license. | 
 |  | 
 | # Usage: | 
 | #   objdump -m i386 -M i8086 -M suffix -d out/rom16.o | scripts/checkstack.py | 
 |  | 
 | import sys | 
 | import re | 
 |  | 
 | # Functions that change stacks | 
 | STACKHOP = ['stack_hop', 'stack_hop_back'] | 
 | # List of functions we can assume are never called. | 
 | #IGNORE = ['panic', '__dprintf'] | 
 | IGNORE = ['panic'] | 
 |  | 
 | OUTPUTDESC = """ | 
 | #funcname1[preamble_stack_usage,max_usage_with_callers]: | 
 | #    insn_addr:called_function [usage_at_call_point+caller_preamble,total_usage] | 
 | # | 
 | #funcname2[p,m,max_usage_to_yield_point]: | 
 | #    insn_addr:called_function [u+c,t,usage_to_yield_point] | 
 | """ | 
 |  | 
 | class function: | 
 |     def __init__(self, funcaddr, funcname): | 
 |         self.funcaddr = funcaddr | 
 |         self.funcname = funcname | 
 |         self.basic_stack_usage = 0 | 
 |         self.max_stack_usage = None | 
 |         self.yield_usage = -1 | 
 |         self.max_yield_usage = None | 
 |         self.total_calls = 0 | 
 |         # called_funcs = [(insnaddr, calladdr, stackusage), ...] | 
 |         self.called_funcs = [] | 
 |         self.subfuncs = {} | 
 |     # Update function info with a found "yield" point. | 
 |     def noteYield(self, stackusage): | 
 |         if self.yield_usage < stackusage: | 
 |             self.yield_usage = stackusage | 
 |     # Update function info with a found "call" point. | 
 |     def noteCall(self, insnaddr, calladdr, stackusage): | 
 |         if (calladdr, stackusage) in self.subfuncs: | 
 |             # Already noted a nearly identical call - ignore this one. | 
 |             return | 
 |         self.called_funcs.append((insnaddr, calladdr, stackusage)) | 
 |         self.subfuncs[(calladdr, stackusage)] = 1 | 
 |  | 
 | # Find out maximum stack usage for a function | 
 | def calcmaxstack(info, funcs): | 
 |     if info.max_stack_usage is not None: | 
 |         return | 
 |     info.max_stack_usage = max_stack_usage = info.basic_stack_usage | 
 |     info.max_yield_usage = max_yield_usage = info.yield_usage | 
 |     total_calls = 0 | 
 |     seenbefore = {} | 
 |     # Find max of all nested calls. | 
 |     for insnaddr, calladdr, usage in info.called_funcs: | 
 |         callinfo = funcs.get(calladdr) | 
 |         if callinfo is None: | 
 |             continue | 
 |         calcmaxstack(callinfo, funcs) | 
 |         if callinfo.funcname not in seenbefore: | 
 |             seenbefore[callinfo.funcname] = 1 | 
 |             total_calls += callinfo.total_calls + 1 | 
 |         funcnameroot = callinfo.funcname.split('.')[0] | 
 |         if funcnameroot in IGNORE: | 
 |             # This called function is ignored - don't contribute it to | 
 |             # the max stack. | 
 |             continue | 
 |         totusage = usage + callinfo.max_stack_usage | 
 |         totyieldusage = usage + callinfo.max_yield_usage | 
 |         if funcnameroot in STACKHOP: | 
 |             # Don't count children of this function | 
 |             totusage = totyieldusage = usage | 
 |         if totusage > max_stack_usage: | 
 |             max_stack_usage = totusage | 
 |         if callinfo.max_yield_usage >= 0 and totyieldusage > max_yield_usage: | 
 |             max_yield_usage = totyieldusage | 
 |     info.max_stack_usage = max_stack_usage | 
 |     info.max_yield_usage = max_yield_usage | 
 |     info.total_calls = total_calls | 
 |  | 
 | # Try to arrange output so that functions that call each other are | 
 | # near each other. | 
 | def orderfuncs(funcaddrs, availfuncs): | 
 |     l = [(availfuncs[funcaddr].total_calls | 
 |           , availfuncs[funcaddr].funcname, funcaddr) | 
 |          for funcaddr in funcaddrs if funcaddr in availfuncs] | 
 |     l.sort() | 
 |     l.reverse() | 
 |     out = [] | 
 |     while l: | 
 |         count, name, funcaddr = l.pop(0) | 
 |         info = availfuncs.get(funcaddr) | 
 |         if info is None: | 
 |             continue | 
 |         calladdrs = [calls[1] for calls in info.called_funcs] | 
 |         del availfuncs[funcaddr] | 
 |         out = out + orderfuncs(calladdrs, availfuncs) + [info] | 
 |     return out | 
 |  | 
 | hex_s = r'[0-9a-f]+' | 
 | re_func = re.compile(r'^(?P<funcaddr>' + hex_s + r') <(?P<func>.*)>:$') | 
 | re_asm = re.compile( | 
 |     r'^[ ]*(?P<insnaddr>' + hex_s | 
 |     + r'):\t.*\t(addr32 )?(?P<insn>.+?)[ ]*((?P<calladdr>' + hex_s | 
 |     + r') <(?P<ref>.*)>)?$') | 
 | re_usestack = re.compile( | 
 |     r'^(push[f]?[lw])|(sub.* [$](?P<num>0x' + hex_s + r'),%esp)$') | 
 |  | 
 | def main(): | 
 |     unknownfunc = function(None, "<unknown>") | 
 |     indirectfunc = function(-1, '<indirect>') | 
 |     unknownfunc.max_stack_usage = indirectfunc.max_stack_usage = 0 | 
 |     unknownfunc.max_yield_usage = indirectfunc.max_yield_usage = -1 | 
 |     funcs = {-1: indirectfunc} | 
 |     cur = None | 
 |     atstart = 0 | 
 |     stackusage = 0 | 
 |  | 
 |     # Parse input lines | 
 |     for line in sys.stdin.readlines(): | 
 |         m = re_func.match(line) | 
 |         if m is not None: | 
 |             # Found function | 
 |             funcaddr = int(m.group('funcaddr'), 16) | 
 |             funcs[funcaddr] = cur = function(funcaddr, m.group('func')) | 
 |             stackusage = 0 | 
 |             atstart = 1 | 
 |             continue | 
 |         m = re_asm.match(line) | 
 |         if m is None: | 
 |             #print("other", repr(line)) | 
 |             continue | 
 |         insn = m.group('insn') | 
 |  | 
 |         im = re_usestack.match(insn) | 
 |         if im is not None: | 
 |             if insn.startswith('pushl') or insn.startswith('pushfl'): | 
 |                 stackusage += 4 | 
 |                 continue | 
 |             elif insn.startswith('pushw') or insn.startswith('pushfw'): | 
 |                 stackusage += 2 | 
 |                 continue | 
 |             stackusage += int(im.group('num'), 16) | 
 |  | 
 |         if atstart: | 
 |             if '%esp' in insn or insn.startswith('leal'): | 
 |                 # Still part of initial header | 
 |                 continue | 
 |             if not stackusage and ( | 
 |                     insn.startswith('test') or insn.startswith('cmp') | 
 |                     or insn.startswith('j')): | 
 |                 # There may be conditional checks prior to stack frame | 
 |                 continue | 
 |             cur.basic_stack_usage = stackusage | 
 |             atstart = 0 | 
 |  | 
 |         insnaddr = m.group('insnaddr') | 
 |         calladdr = m.group('calladdr') | 
 |         if calladdr is None: | 
 |             if insn.startswith('lcallw'): | 
 |                 cur.noteCall(insnaddr, -1, stackusage + 4) | 
 |                 cur.noteYield(stackusage + 4) | 
 |             elif insn.startswith('int'): | 
 |                 cur.noteCall(insnaddr, -1, stackusage + 6) | 
 |                 cur.noteYield(stackusage + 6) | 
 |             elif insn.startswith('sti'): | 
 |                 cur.noteYield(stackusage) | 
 |             else: | 
 |                 # misc instruction | 
 |                 continue | 
 |         else: | 
 |             # Jump or call insn | 
 |             calladdr = int(calladdr, 16) | 
 |             ref = m.group('ref') | 
 |             if '+' in ref: | 
 |                 # Inter-function jump. | 
 |                 pass | 
 |             elif insn.startswith('j'): | 
 |                 # Tail call | 
 |                 cur.noteCall(insnaddr, calladdr, 0) | 
 |             elif insn.startswith('calll'): | 
 |                 cur.noteCall(insnaddr, calladdr, stackusage + 4) | 
 |             elif insn.startswith('callw'): | 
 |                 cur.noteCall(insnaddr, calladdr, stackusage + 2) | 
 |             else: | 
 |                 print("unknown call", ref) | 
 |                 cur.noteCall(insnaddr, calladdr, stackusage) | 
 |         # Reset stack usage to preamble usage | 
 |         stackusage = cur.basic_stack_usage | 
 |  | 
 |     # Calculate maxstackusage | 
 |     for info in funcs.values(): | 
 |         calcmaxstack(info, funcs) | 
 |  | 
 |     # Sort functions for output | 
 |     funcinfos = orderfuncs(funcs.keys(), funcs.copy()) | 
 |  | 
 |     # Show all functions | 
 |     print(OUTPUTDESC) | 
 |     for info in funcinfos: | 
 |         if info.max_stack_usage == 0 and info.max_yield_usage < 0: | 
 |             continue | 
 |         yieldstr = "" | 
 |         if info.max_yield_usage >= 0: | 
 |             yieldstr = ",%d" % info.max_yield_usage | 
 |         print("\n%s[%d,%d%s]:" % (info.funcname, info.basic_stack_usage | 
 |                                   , info.max_stack_usage, yieldstr)) | 
 |         for insnaddr, calladdr, stackusage in info.called_funcs: | 
 |             callinfo = funcs.get(calladdr, unknownfunc) | 
 |             yieldstr = "" | 
 |             if callinfo.max_yield_usage >= 0: | 
 |                 yieldstr = ",%d" % (stackusage + callinfo.max_yield_usage) | 
 |             print("    %04s:%-40s [%d+%d,%d%s]" % ( | 
 |                 insnaddr, callinfo.funcname, stackusage | 
 |                 , callinfo.basic_stack_usage | 
 |                 , stackusage+callinfo.max_stack_usage, yieldstr)) | 
 |  | 
 | if __name__ == '__main__': | 
 |     main() |