diff options
Diffstat (limited to 'build-aux')
-rwxr-xr-x | build-aux/embed-sources.h.gen | 2 | ||||
-rw-r--r-- | build-aux/requirements.txt | 9 | ||||
-rwxr-xr-x | build-aux/stack.c.gen | 274 | ||||
-rw-r--r-- | build-aux/stack.py | 184 | ||||
-rwxr-xr-x | build-aux/stack.sh | 2 |
5 files changed, 279 insertions, 192 deletions
diff --git a/build-aux/embed-sources.h.gen b/build-aux/embed-sources.h.gen index 0ba6457..d473094 100755 --- a/build-aux/embed-sources.h.gen +++ b/build-aux/embed-sources.h.gen @@ -1,5 +1,5 @@ #!/bin/sh -# embed-sources.h.gen - Generate C definitions for GNU `ld -r -b binary` files +# build-aux/embed-sources.h.gen - Generate C definitions for GNU `ld -r -b binary` files # # Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com> # SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/build-aux/requirements.txt b/build-aux/requirements.txt new file mode 100644 index 0000000..03c0b3d --- /dev/null +++ b/build-aux/requirements.txt @@ -0,0 +1,9 @@ +# build-aux/requirements.txt - List of Python dev requirements +# +# Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com> +# SPDX-License-Identifier: AGPL-3.0-or-later + +mypy +types-gdb +black +isort diff --git a/build-aux/stack.c.gen b/build-aux/stack.c.gen index 9325791..06612ac 100755 --- a/build-aux/stack.c.gen +++ b/build-aux/stack.c.gen @@ -1,9 +1,273 @@ -#!/usr/bin/env bash -# stack.c.gen - Analyze stack sizes for compiled objects +#!/usr/bin/env python3 +# build-aux/stack.c.gen - Analyze stack sizes for compiled objects # # Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com> # SPDX-License-Identifier: AGPL-3.0-or-later -for obj in "$@"; do - echo "// $obj" -done +import re +import sys +import typing + +################################################################################ +# +# Parse the "VCG" language +# +# https://www.rw.cdl.uni-saarland.de/people/sander/private/html/gsvcg1.html +# +# The formal syntax is found at +# ftp://ftp.cs.uni-sb.de/pub/graphics/vcg/vcg.tgz `doc/grammar.txt`. + + +class VCGElem: + typ: str + lineno: int + attrs: dict[str, str] + + +def parse_vcg(reader: typing.TextIO) -> typing.Iterator[VCGElem]: + re_beg = re.compile(r"(edge|node):\s*\{\s*") + _re_tok = r"[a-zA-Z_][a-zA-Z0-9_]*" + _re_str = r'"(?:[^\"]|\\.)*"' + re_attr = re.compile( + "(" + _re_tok + r")\s*:\s*(" + _re_tok + "|" + _re_str + r")\s*" + ) + re_end = re.compile(r"\}\s*$") + re_skip = re.compile(r"(graph:\s*\{\s*title\s*:\s*" + _re_str + r"\s*|\})\s*") + re_esc = re.compile(r"\\.") + + for lineno, line in enumerate(reader): + pos = 0 + + def _raise(msg: str) -> typing.NoReturn: + nonlocal lineno + nonlocal line + nonlocal pos + e = SyntaxError(msg) + e.lineno = lineno + e.offset = pos + e.text = line + raise e + + if re_skip.fullmatch(line): + continue + + elem = VCGElem() + elem.lineno = lineno + + m = re_beg.match(line, pos=pos) + if not m: + _raise("does not look like a VCG line") + elem.typ = m.group(1) + pos = m.end() + + elem.attrs = {} + while True: + if re_end.match(line, pos=pos): + break + m = re_attr.match(line, pos=pos) + if not m: + _raise("unexpected character") + k = m.group(1) + v = m.group(2) + if k in elem.attrs: + _raise(f"duplicate key: {repr(k)}") + if v.startswith('"'): + + def unesc(esc: re.Match[str]) -> str: + match esc.group(0)[1:]: + case "n": + return "\n" + case '"': + return '"' + case "\\": + return "\\" + case _: + _raise(f"invalid escape code {repr(esc.group(0))}") + + v = re_esc.sub(unesc, v[1:-1]) + elem.attrs[k] = v + pos = m.end() + + yield elem + + +################################################################################ +# Main application + + +class Node: + # from .title (`static` and `__weak` functions are prefixed with + # the compilation unit .c file. For static functions that's fine, + # but we'll have to handle it specially for __weak.). + funcname: str + # .label is "{funcname}\n{location}\n{nstatic} bytes (static}\n{ndynamic} dynamic objects" + location: str + nstatic: int + ndynamic: int + + # edges with .sourcename set to this node + calls: set[str] + + +re_location = re.compile(r"(?P<filename>.+):(?P<row>[0-9]+):(?P<col>[0-9]+)") + + +def read_source(location: str) -> str: + m = re_location.fullmatch(location) + if not m: + raise ValueError(f"unexpected label value {repr(location)}") + filename = m.group("filename") + row = int(m.group("row")) - 1 + col = int(m.group("col")) - 1 + with open(m.group("filename"), "r") as fh: + return fh.readlines()[row][col:].rstrip() + + +def main(ci_fnames: list[str]) -> None: + re_node_label = re.compile( + r"(?P<funcname>[^\n]+)\n" + + r"(?P<location>[^\n]+:[0-9]+:[0-9]+)\n" + + r"(?P<nstatic>[0-9]+) bytes \(static\)\n" + + r"(?P<ndynamic>[0-9]+) dynamic objects", + flags=re.MULTILINE, + ) + re_call_vcall = re.compile(r"VCALL\((?P<obj>[^,]+), (?P<meth>[^,)]+)[,)].*") + re_call_other = re.compile(r"(?P<func>[^(]+)\(.*") + + graph: dict[str, Node] = dict() + qualified: dict[str, set[str]] = dict() + + def handle_elem(elem: VCGElem) -> None: + match elem.typ: + case "node": + node = Node() + node.calls = set() + skip = False + for k, v in elem.attrs.items(): + match k: + case "title": + node.funcname = v + case "label": + if elem.attrs.get("shape", "") != "ellipse": + m = re_node_label.fullmatch(v) + if not m: + raise ValueError( + f"unexpected label value {repr(v)}" + ) + node.location = m.group("location") + node.nstatic = int(m.group("nstatic")) + node.ndynamic = int(m.group("ndynamic")) + case "shape": + if v != "ellipse": + raise ValueError(f"unexpected shape value {repr(v)}") + skip = True + case _: + raise ValueError(f"unknown edge key {repr(k)}") + if not skip: + if node.funcname in graph: + raise ValueError(f"duplicate node {repr(node.funcname)}") + graph[node.funcname] = node + if ":" in node.funcname: + _, shortname = node.funcname.rsplit(":", 1) + if shortname not in qualified: + qualified[shortname] = set() + qualified[shortname].add(node.funcname) + case "edge": + caller: str | None = None + callee: str | None = None + for k, v in elem.attrs.items(): + match k: + case "sourcename": + caller = v + case "targetname": + callee = v + case "label": + pass + case _: + raise ValueError(f"unknown edge key {repr(k)}") + if caller is None or callee is None: + raise ValueError(f"incomplete edge: {repr(elem.attrs)}") + if caller not in graph: + raise ValueError(f"unknown caller: {caller}") + if callee == "__indirect_call": + callstr = read_source(elem.attrs.get("label", "")) + if m := re_call_vcall.fullmatch(callstr): + callee += f":{m.group('obj')}->vtable->{m.group('meth')}" + elif m := re_call_other.fullmatch(callstr): + callee += f":{m.group('func')}" + else: + callee += f':{elem.attrs.get("label", "")}' + graph[caller].calls.add(callee) + case _: + raise ValueError(f"unknown elem type {repr(elem.typ)}") + + for ci_fname in ci_fnames: + with open(ci_fname, "r") as fh: + for elem in parse_vcg(fh): + handle_elem(elem) + + missing: set[str] = set() + cycles: set[str] = set() + + print("/*") + + dbg = False + + def nstatic(funcname: str, chain: list[str] = []) -> int: + nonlocal dbg + if funcname not in graph: + if f"__wrap_{funcname}" in graph: + # Handle `ld --wrap` functions + funcname = f"__wrap_{funcname}" + elif funcname in qualified and len(qualified[funcname]) == 1: + # Handle `__weak` functions + funcname = sorted(qualified[funcname])[0] + else: + missing.add(funcname) + return 0 + if funcname in chain: + if "__assert_msg_fail" in chain: + if funcname == "__wrap_printf": + return 0 + pass + else: + cycles.add(f"{chain[chain.index(funcname):] + [funcname]}") + return 9999999 + node = graph[funcname] + if dbg: + print(f"//dbg: {funcname}\t{node.nstatic}") + return node.nstatic + max( + [0, *[nstatic(call, chain + [funcname]) for call in node.calls]] + ) + + def thread_filter(name: str) -> bool: + return name.endswith("_cr") or name == "main" + + namelen = max(len(name) for name in graph if thread_filter(name)) + numlen = max(len(str(nstatic(name))) for name in graph if name.endswith("_cr")) + print(("=" * namelen) + " " + "=" * numlen) + + for funcname in graph: + if thread_filter(funcname): + # dbg = "dhcp" in funcname + print(f"{funcname.ljust(namelen)} {str(nstatic(funcname)).rjust(numlen)}") + + print(("=" * namelen) + " " + "=" * numlen) + + for funcname in sorted(missing): + print(f"warning: missing: {funcname}") + for cycle in sorted(cycles): + print(f"warning: cycle: {cycle}") + + print("*/") + + +if __name__ == "__main__": + re_suffix = re.compile(r"\.c\.o(bj)?$") + main( + [ + re_suffix.sub(".c.ci", fname) + for fname in sys.argv[1:] + if re_suffix.search(fname) + ] + ) diff --git a/build-aux/stack.py b/build-aux/stack.py deleted file mode 100644 index c1e36d3..0000000 --- a/build-aux/stack.py +++ /dev/null @@ -1,184 +0,0 @@ -#!/usr/bin/env python3 -import re -import sys -import typing - -# Parse the "VCG" language -# -# https://www.rw.cdl.uni-saarland.de/people/sander/private/html/gsvcg1.html -# -# The formal syntax is found at -# ftp://ftp.cs.uni-sb.de/pub/graphics/vcg/vcg.tgz `doc/grammar.txt`. - - -class VCGElem: - typ: str - lineno: int - attrs: dict[str, str] - - -def parse_vcg(reader: typing.TextIO) -> typing.Iterator[VCGElem]: - re_beg = re.compile(r"(edge|node):\s*\{\s*") - _re_tok = r"[a-zA-Z_][a-zA-Z0-9_]*" - _re_str = r'"(?:[^\"]|\\.)*"' - re_attr = re.compile( - "(" + _re_tok + r")\s*:\s*(" + _re_tok + "|" + _re_str + r")\s*" - ) - re_end = re.compile(r"\}\s*$") - re_skip = re.compile(r"(graph:\s*\{\s*title\s*:\s*" + _re_str + r"\s*|\})\s*") - re_esc = re.compile(r"\\.") - - for lineno, line in enumerate(reader): - pos = 0 - - def _raise(msg: str) -> typing.NoReturn: - nonlocal lineno - nonlocal line - nonlocal pos - e = SyntaxError(msg) - e.lineno = lineno - e.offset = pos - e.text = line - raise e - - if re_skip.fullmatch(line): - continue - - elem = VCGElem() - elem.lineno = lineno - - m = re_beg.match(line, pos=pos) - if not m: - _raise("does not look like a VCG line") - elem.typ = m.group(1) - pos = m.end() - - elem.attrs = {} - while True: - if re_end.match(line, pos=pos): - break - m = re_attr.match(line, pos=pos) - if not m: - _raise("unexpected character") - k = m.group(1) - v = m.group(2) - if k in elem.attrs: - _raise(f"duplicate key: {repr(k)}") - if v.startswith('"'): - - def unesc(esc: re.Match[str]) -> str: - match esc.group(0)[1:]: - case "n": - return "\n" - case '"': - return '"' - case "\\": - return "\\" - case _: - _raise(f"invalid escape code {repr(esc.group(0))}") - - v = re_esc.sub(unesc, v[1:-1]) - elem.attrs[k] = v - pos = m.end() - - yield elem - - -class Node: - # from .title (`static` functions are prefixed with the - # compilation unit .c file, which is fine, we'll just leave it). - funcname: str - # .label is "{funcname}\n{location}\n{nstatic} bytes (static}\n{ndynamic} dynamic objects" - location: str - nstatic: int - ndynamic: int - - # edges with .sourcename set to this node - calls: set[str] - -def main() -> None: - re_label = re.compile( - r"(?P<funcname>[^\n]+)\n" - + r"(?P<location>[^\n]+:[0-9]+:[0-9]+)\n" - + r"(?P<nstatic>[0-9]+) bytes \(static\)\n" - + r"(?P<ndynamic>[0-9]+) dynamic objects", - flags=re.MULTILINE, - ) - - graph: dict[str, Node] = dict() - - for elem in parse_vcg(sys.stdin): - match elem.typ: - case "node": - node = Node() - node.calls = set() - skip = False - for k, v in elem.attrs.items(): - match k: - case "title": - node.funcname = v - case "label": - if elem.attrs.get("shape", "") != "ellipse": - m = re_label.fullmatch(v) - if not m: - raise ValueError( - f"unexpected label value {repr(v)}" - ) - node.location = m.group("location") - node.nstatic = int(m.group("nstatic")) - node.ndynamic = int(m.group("ndynamic")) - case "shape": - if v != "ellipse": - raise ValueError(f"unexpected shape value {repr(v)}") - skip = True - case _: - raise ValueError(f"unknown edge key {repr(k)}") - if not skip: - if node.funcname in graph: - raise ValueError(f"duplicate node {repr(node.funcname)}") - graph[node.funcname] = node - case "edge": - caller: str | None = None - callee: str | None = None - for k, v in elem.attrs.items(): - match k: - case "sourcename": - caller = v - case "targetname": - callee = v - case "label": - pass - case _: - raise ValueError(f"unknown edge key {repr(k)}") - if caller is None or callee is None: - raise ValueError(f"incomplete edge: {repr(elem.attrs)}") - if caller not in graph: - raise ValueError(f"unknown caller: {caller}") - graph[caller].calls.add(callee) - case _: - raise ValueError(f"unknown elem type {repr(elem.typ)}") - - # x - - missing: set[str] = set() - def nstatic(funcname: str) -> int: - if funcname not in graph: - missing.add(funcname) - return 0 - node = graph[funcname] - return node.nstatic + max([0, *[nstatic(call) for call in node.calls]]) - - namelen = max(len(name) for name in graph if name.endswith("_cr")) - print(("="*namelen)+" =======") - - for funcname in graph: - if funcname.endswith("_cr"): - print(f"{funcname}\t{nstatic(funcname)}") - - print(("="*namelen)+" =======") - - for funcname in sorted(missing): - print(f"{funcname}\tmissing") - -if __name__ == "__main__": - main() diff --git a/build-aux/stack.sh b/build-aux/stack.sh deleted file mode 100755 index 4a8a12e..0000000 --- a/build-aux/stack.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -make >&2 && find build -name '*.ci' -exec cat -- {} + | python ./stack.py |