415 lines
13 KiB
Python
415 lines
13 KiB
Python
from typing import NamedTuple, List, Mapping, Optional, Tuple, Set, DefaultDict
|
|
from collections import defaultdict
|
|
import pickle
|
|
import argparse
|
|
|
|
import os
|
|
import io
|
|
import re
|
|
|
|
import pcpp
|
|
from pycparser import c_ast, CParser
|
|
|
|
g_failed = False
|
|
|
|
def verbose(msg):
|
|
print(msg, flush=True)
|
|
|
|
def error(msg):
|
|
global g_failed
|
|
print(f"\n{msg}", flush=True)
|
|
g_failed = True
|
|
|
|
fake_includes = { }
|
|
|
|
fake_includes["stdint.h"] = """
|
|
#pragma once
|
|
typedef unsigned char uint8_t;
|
|
typedef unsigned short uint16_t;
|
|
typedef unsigned int uint32_t;
|
|
typedef unsigned long uint64_t;
|
|
typedef signed char int8_t;
|
|
typedef signed short int16_t;
|
|
typedef signed int int32_t;
|
|
typedef signed long int64_t;
|
|
"""
|
|
|
|
fake_includes["stdbool.h"] = """
|
|
#pragma once
|
|
#define bool _Bool
|
|
#define true 1
|
|
#define false 0
|
|
"""
|
|
|
|
fake_includes["stddef.h"] = """
|
|
typedef unsigned long size_t;
|
|
typedef unsigned long uintptr_t;
|
|
typedef long ptrdiff_t;
|
|
"""
|
|
|
|
fake_includes["stdarg.h"] = """
|
|
typedef int va_list;
|
|
"""
|
|
|
|
fake_includes["stdio.h"] = """
|
|
typedef int FILE;
|
|
typedef unsigned long fpos_t;
|
|
"""
|
|
|
|
fake_includes["string.h"] = ""
|
|
fake_includes["stdlib.h"] = ""
|
|
fake_includes["locale.h"] = ""
|
|
fake_includes["math.h"] = ""
|
|
fake_includes["assert.h"] = ""
|
|
|
|
class Preprocessor(pcpp.Preprocessor):
|
|
def __init__(self):
|
|
super().__init__()
|
|
|
|
def on_file_open(self, is_system_include, includepath):
|
|
if is_system_include and os.path.basename(includepath) in fake_includes:
|
|
return io.StringIO(fake_includes[os.path.basename(includepath)])
|
|
return super().on_file_open(is_system_include, includepath)
|
|
|
|
class ValuePreprocessor(pcpp.Preprocessor):
|
|
def __init__(self, func: str, expr: str):
|
|
super().__init__()
|
|
self.func = func
|
|
self.expr = expr
|
|
|
|
def on_unknown_macro_in_expr(self,tok):
|
|
raise RuntimeError(f"Bad recursion bound on function {self.func}: '{self.expr}'")
|
|
|
|
class FuncInfo:
|
|
stack_usage: int
|
|
max_recursion: Optional[int]
|
|
calls: Set[str]
|
|
defined: bool
|
|
recursion_stack: List[str]
|
|
|
|
def __init__(self):
|
|
self.stack_usage = 0
|
|
self.max_recursion = None
|
|
self.calls = set()
|
|
self.defined = False
|
|
self.recursion_stack = []
|
|
self.largest_call = ""
|
|
|
|
class StackUsage(NamedTuple):
|
|
usage: int
|
|
largest_call: str
|
|
|
|
class File:
|
|
functions: DefaultDict[str, FuncInfo]
|
|
stack_usage: Mapping[str, StackUsage]
|
|
addresses: Set[str]
|
|
max_dynamic_usage: int
|
|
recursion_errors: Mapping[str, List[str]]
|
|
|
|
def __init__(self):
|
|
self.functions = defaultdict(FuncInfo)
|
|
self.stack_usage = {}
|
|
self.addresses = set()
|
|
self.max_dynamic_usage = 0
|
|
self.recursion_errors = {}
|
|
|
|
class AstVisitor(c_ast.NodeVisitor):
|
|
file: File
|
|
current_func: str
|
|
|
|
def __init__(self, file: File):
|
|
self.file = file
|
|
self.current_func = None
|
|
|
|
def visit_FuncDef(self, node: c_ast.FuncDecl):
|
|
if node.body:
|
|
func = node.decl.name
|
|
if not (func.startswith("ufbx_") or func.startswith("ufbxi_")):
|
|
error(f"Bad function definition: {func}")
|
|
self.file.functions[func].defined = True
|
|
|
|
self.current_func = node.decl.name
|
|
self.visit(node.body)
|
|
self.current_func = None
|
|
|
|
def visit_UnaryOp(self, node: c_ast.UnaryOp):
|
|
if node.op == "&" and isinstance(node.expr, c_ast.ID):
|
|
self.file.addresses.add(node.expr.name)
|
|
self.visit(node.expr)
|
|
|
|
def visit_FuncCall(self, node: c_ast.FuncCall):
|
|
src = self.current_func
|
|
if src:
|
|
dst = node.name.name
|
|
if (isinstance(dst, str) and (dst.startswith("ufbxi_") or dst.startswith("ufbx_"))):
|
|
self.file.functions[src].calls.add(dst)
|
|
if node.args:
|
|
self.visit(node.args)
|
|
|
|
def get_stack_usage_to(file: File, func: str, target: str, seen: Set[str] = set()) -> Optional[Tuple[int, List[str]]]:
|
|
if func in seen:
|
|
return (0, []) if func == target else None
|
|
info = file.functions.get(func)
|
|
if not info:
|
|
raise RuntimeError(f"Function not found: {func}")
|
|
|
|
seen = seen | { func }
|
|
|
|
max_path = None
|
|
for call in info.calls:
|
|
path = get_stack_usage_to(file, call, target, seen)
|
|
if path is not None:
|
|
if max_path:
|
|
max_path = max(max_path, path)
|
|
else:
|
|
max_path = path
|
|
|
|
if max_path:
|
|
max_usage, stack = max_path
|
|
usage = info.stack_usage + max_usage
|
|
return (usage, [func] + stack)
|
|
else:
|
|
return None
|
|
|
|
def add_ignore(ignores: str, ignore: str) -> str:
|
|
if not ignores: ignores = "/"
|
|
parts = ignores[1:].split(",") + [ignore]
|
|
return "/" + ",".join(sorted(p for p in parts if p))
|
|
|
|
def is_ignored(ignores: str, func: str) -> bool:
|
|
return ignores and func in ignores[1:].split(",")
|
|
|
|
def get_stack_usage(file: File, func: str, ignores: str = "", stack: List[str] = []) -> StackUsage:
|
|
if is_ignored(ignores, func):
|
|
return StackUsage(-1, "")
|
|
|
|
key = f"{func}{ignores}" if ignores else func
|
|
existing = file.stack_usage.get(key)
|
|
if existing is not None: return existing
|
|
|
|
info = file.functions.get(func)
|
|
if not info:
|
|
raise RuntimeError(f"Function not found: {func}")
|
|
|
|
if info.max_recursion:
|
|
rec_path = get_stack_usage_to(file, func, func)
|
|
if rec_path is None:
|
|
error(f"Unnecessary recursion tag in {func}()\nContains ufbxi_assert_max_recursion() but could not find recursive path to itself")
|
|
rec_path = (0, [])
|
|
|
|
rec_usage, rec_stack = rec_path
|
|
info.recursion_stack = rec_stack
|
|
self_usage = rec_usage * (info.max_recursion - 1) + info.stack_usage
|
|
child_ignores = add_ignore(ignores, func)
|
|
stack = []
|
|
else:
|
|
self_usage = info.stack_usage
|
|
child_ignores = ignores
|
|
|
|
if func in stack:
|
|
pos = stack.index(func)
|
|
error_stack = stack[pos:] + [func]
|
|
prev_error = file.recursion_errors.get(func)
|
|
if not prev_error or len(prev_error) > len(error_stack):
|
|
file.recursion_errors[func] = error_stack
|
|
return StackUsage(0, "")
|
|
|
|
stack = stack + [func]
|
|
|
|
max_usage = StackUsage(0, "")
|
|
for call in info.calls:
|
|
usage = get_stack_usage(file, call, child_ignores, stack).usage
|
|
max_usage = max(max_usage, StackUsage(usage, f"{call}{child_ignores}"))
|
|
|
|
usage = StackUsage(self_usage + max_usage.usage, max_usage.largest_call)
|
|
file.stack_usage[key] = usage
|
|
return usage
|
|
|
|
def parse_file(c_path: str, su_path: str, cache_path: Optional[str]) -> File:
|
|
pp = Preprocessor()
|
|
pp.define("UFBX_STANDARD_C")
|
|
pp.define("UFBXI_ANALYSIS_PARSER")
|
|
pp.define("UFBXI_ANALYSIS_RECURSIVE")
|
|
|
|
if cache_path and os.path.exists(cache_path) and os.path.getmtime(cache_path) > os.path.getmtime(c_path):
|
|
verbose(f"Loading AST cache: {cache_path}")
|
|
with open(cache_path, "rb") as f:
|
|
ast, max_recursions = pickle.load(f)
|
|
else:
|
|
max_recursions = { }
|
|
|
|
verbose(f"Preprocessing C file: {c_path}")
|
|
pp_stream = io.StringIO()
|
|
with open(c_path) as f:
|
|
pp.parse(f.read(), "ufbx.c")
|
|
pp.write(pp_stream)
|
|
pp_source = pp_stream.getvalue()
|
|
|
|
re_recursive_function = re.compile(r"UFBXI_RECURSIVE_FUNCTION\s*\(\s*(\w+),\s*(.+)\s*\);")
|
|
for line in pp_source.splitlines():
|
|
m = re_recursive_function.search(line)
|
|
if m:
|
|
name, rec_expr = m.groups()
|
|
lit_pp = ValuePreprocessor(name, rec_expr)
|
|
toks = lit_pp.tokenize(rec_expr)
|
|
rec_value, _ = lit_pp.evalexpr(toks)
|
|
if not rec_value:
|
|
raise RuntimeError(f"Bad recursion bound on function {name}: '{rec_expr}'")
|
|
max_recursions[name] = rec_value
|
|
|
|
pp_source = re_recursive_function.sub("", pp_source)
|
|
|
|
verbose("Parsing C file")
|
|
parser = CParser()
|
|
ast = parser.parse(pp_source)
|
|
|
|
if cache_path:
|
|
verbose(f"Writing AST cache: {cache_path}")
|
|
with open(cache_path, "wb") as f:
|
|
pickle.dump((ast, max_recursions), f)
|
|
|
|
verbose("Visiting AST")
|
|
file = File()
|
|
visitor = AstVisitor(file)
|
|
visitor.visit(ast)
|
|
|
|
# Gather maximum recursion from file
|
|
for func, rec in max_recursions.items():
|
|
file.functions[func].max_recursion = rec
|
|
|
|
if su_path:
|
|
verbose(f"Reading stack usage file: {su_path}")
|
|
with open(su_path) as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if not line: continue
|
|
m = re.match(r".*:\d+:(?:\d+:)?(\w+)(?:\.[a-z0-9\.]+)?\s+(\d+)\s+([a-z,]+)", line)
|
|
if not m:
|
|
raise RuntimeError(f"Bad .su line: {line}")
|
|
func, stack, usage = m.groups()
|
|
assert usage in ("static", "dynamic,bounded")
|
|
file.functions[func].stack_usage = int(stack)
|
|
|
|
return file
|
|
|
|
def get_max_dynamic_usage(file: File) -> Tuple[int, str]:
|
|
max_dynamic_usage = (0, "")
|
|
|
|
addr_funcs = file.addresses & set(file.functions.keys())
|
|
for func in addr_funcs:
|
|
usage = get_stack_usage(file, func).usage
|
|
max_dynamic_usage = max(max_dynamic_usage, (usage, func))
|
|
|
|
return max_dynamic_usage
|
|
|
|
def dump_largest_stack(file: File, func: str) -> List[str]:
|
|
usage = file.stack_usage[func]
|
|
print(f"{func}() {usage.usage} bytes")
|
|
index = 0
|
|
while True:
|
|
if "/" in func:
|
|
raw_func, _ = func.split("/")
|
|
else:
|
|
raw_func = func
|
|
info = file.functions.get(raw_func)
|
|
stack_usage = file.stack_usage.get(func)
|
|
if not info: break
|
|
|
|
if info.recursion_stack:
|
|
rec_usage = 0
|
|
for ix, frame in enumerate(info.recursion_stack):
|
|
rec_info = file.functions[frame]
|
|
ch = "|" if ix > 0 else ">"
|
|
fn = f"{frame}()"
|
|
usage = f"+{rec_info.stack_usage} bytes"
|
|
rec_usage += rec_info.stack_usage
|
|
print(f"{ch}{index:3} {fn:<40}{usage:>14}")
|
|
index += 1
|
|
|
|
rec_extra = info.max_recursion - 1
|
|
usage = f"(+{rec_usage * rec_extra} bytes)"
|
|
prefix = f"(recursion {info.max_recursion} times)"
|
|
index += rec_extra * len(info.recursion_stack)
|
|
dots = "." * len(str(index - 1))
|
|
print(f"|{dots:>3} {prefix:<39}{usage:>16}")
|
|
|
|
fn = f"{raw_func}()"
|
|
usage = f"+{info.stack_usage} bytes"
|
|
print(f"{index:4} {fn:<40}{usage:>14}")
|
|
func = stack_usage.largest_call
|
|
index += 1
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser("analyze_stack.py")
|
|
parser.add_argument("su", nargs="?", help="Stack usage .su file")
|
|
parser.add_argument("--source", help="Path to ufbx.c")
|
|
parser.add_argument("--cache", help="Cache file to use")
|
|
parser.add_argument("--limit", help="Maximum stack usage in bytes")
|
|
parser.add_argument("--no-su", action="store_true", help="Allow running with no .su file")
|
|
argv = parser.parse_args()
|
|
|
|
if argv.su:
|
|
su_path = argv.su
|
|
elif not argv.no_su:
|
|
raise RuntimeError("Expected an .su file as a positional argument")
|
|
else:
|
|
su_path = ""
|
|
|
|
if argv.source:
|
|
c_path = argv.source
|
|
else:
|
|
c_path = os.path.relpath(os.path.join(os.path.dirname(__file__), "..", "ufbx.c"))
|
|
|
|
file = parse_file(c_path, su_path, argv.cache)
|
|
|
|
if su_path:
|
|
file.max_dynamic_usage = get_max_dynamic_usage(file)
|
|
|
|
max_usage = (0, "")
|
|
for func in file.functions:
|
|
usage = get_stack_usage(file, func)
|
|
max_usage = max(max_usage, (usage.usage, func))
|
|
|
|
if argv.limit:
|
|
limit = int(argv.limit, base=0)
|
|
total = max_usage[0] + file.max_dynamic_usage[0]
|
|
if total >= limit:
|
|
error(f"Stack overflow in {max_usage[1]}: {max_usage[0]} bytes + {file.max_dynamic_usage[0]} dynamic\noverflows limit of {limit} bytes")
|
|
print("\nLargest stack:")
|
|
dump_largest_stack(file, max_usage[1])
|
|
print("\nLargest dynamic stack:")
|
|
dump_largest_stack(file, file.max_dynamic_usage[1])
|
|
|
|
for func, stack in file.recursion_errors.items():
|
|
stack_str = "\n".join(f"{ix:3}: {s}()" for ix, s in enumerate(stack))
|
|
error(f"Unbounded recursion in {func}()\nStack trace:\n{stack_str}")
|
|
|
|
if not g_failed:
|
|
interesting_functions = [
|
|
"ufbx_load_file",
|
|
"ufbx_evaluate_scene",
|
|
"ufbx_subdivide_mesh",
|
|
"ufbx_tessellate_nurbs_surface",
|
|
"ufbx_tessellate_nurbs_curve",
|
|
"ufbx_evaluate_transform",
|
|
"ufbx_generate_indices",
|
|
"ufbx_inflate",
|
|
"ufbx_triangulate_face",
|
|
]
|
|
|
|
for func in interesting_functions:
|
|
print()
|
|
dump_largest_stack(file, func)
|
|
|
|
print()
|
|
print("Largest potentially dynamically called stack:")
|
|
dump_largest_stack(file, file.max_dynamic_usage[1])
|
|
else:
|
|
print("Skipping further tests due to no .su file specified")
|
|
|
|
if g_failed:
|
|
exit(1)
|
|
else:
|
|
print()
|
|
print("Success!")
|