blob: 255768aebce0341f0bb06d13973e653735aa5154 [file] [log] [blame]
#!/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()