summaryrefslogtreecommitdiff
path: root/build-aux/measurestack/app_plugins.py
diff options
context:
space:
mode:
Diffstat (limited to 'build-aux/measurestack/app_plugins.py')
-rw-r--r--build-aux/measurestack/app_plugins.py915
1 files changed, 915 insertions, 0 deletions
diff --git a/build-aux/measurestack/app_plugins.py b/build-aux/measurestack/app_plugins.py
new file mode 100644
index 0000000..a921407
--- /dev/null
+++ b/build-aux/measurestack/app_plugins.py
@@ -0,0 +1,915 @@
+# build-aux/measurestack/app_plugins.py - Application-specific plugins for analyze.py
+#
+# Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+import re
+import subprocess
+import typing
+
+from . import analyze, util
+from .analyze import BaseName, Node, QName
+from .util import synthetic_node
+
+# pylint: disable=unused-variable
+__all__ = [
+ "CmdPlugin",
+ "LibHWPlugin",
+ "LibCRPlugin",
+ "LibCRIPCPlugin",
+ "Lib9PPlugin",
+ "LibMiscPlugin",
+ "PicoSDKPlugin",
+ "TinyUSBDevicePlugin",
+ "NewlibPlugin",
+ "LibGCCPlugin",
+]
+
+
+class CmdPlugin:
+ def is_intrhandler(self, name: QName) -> bool:
+ return False
+
+ def init_array(self) -> typing.Collection[QName]:
+ return []
+
+ def extra_includes(self) -> typing.Collection[BaseName]:
+ return []
+
+ def extra_nodes(self) -> typing.Collection[Node]:
+ return []
+
+ def mutate_node(self, node: Node) -> None:
+ pass
+
+ def indirect_callees(
+ self, loc: str, line: str
+ ) -> tuple[typing.Collection[QName], bool] | None:
+ if "/3rd-party/" in loc:
+ return None
+ if "srv->auth" in line:
+ return [QName("__indirect_call_with_null_check:srv->auth")], False
+ if "srv->rootdir" in line:
+ return [QName("get_root")], False
+ if "/ihex.c" in loc:
+ if "self->handle_data" in line:
+ return [QName("flash_handle_ihex_data")], False
+ if "self->handle_eof" in line:
+ return [QName("flash_handle_ihex_eof")], False
+ if "self->handle_set_exec_start_lin" in line:
+ return [
+ QName(
+ "__indirect_call_with_null_check:self->handle_set_exec_start_lin"
+ )
+ ], False
+ if "self->handle_set_exec_start_seg" in line:
+ return [
+ QName(
+ "__indirect_call_with_null_check:self->handle_set_exec_start_seg"
+ )
+ ], False
+ return None
+
+ def skipmodels(self) -> dict[BaseName, analyze.SkipModel]:
+ return {}
+
+
+class LibMiscPlugin:
+ re_comment = re.compile(r"/\*.*?\*/")
+ re_ws = re.compile(r"\s+")
+ re_lo_iface = re.compile(r"^\s*#\s*define\s+(?P<name>\S+)_LO_IFACE")
+ re_lo_func = re.compile(r"LO_FUNC *\([^,]*, *(?P<name>[^,) ]+) *[,)]")
+ re_lo_implementation = re.compile(
+ r"^LO_IMPLEMENTATION_(?P<vis>H|C|STATIC)\s*\("
+ r"\s*(?P<iface>[^, ]+)\s*,"
+ r"\s*(?P<impl_typ>[^,]+)\s*,"
+ r"\s*(?P<impl_name>[^, ]+)\s*\)"
+ )
+ re_lo_call = re.compile(r".*\bLO_CALL\((?P<obj>[^,]+), (?P<meth>[^,)]+)[,)].*")
+
+ objcalls: dict[str, set[QName]] # method_name => {method_impls}
+
+ def __init__(self, arg_c_fnames: typing.Collection[str]) -> None:
+ ifaces: dict[str, set[str]] = {} # iface_name => {method_names}
+ for fname in arg_c_fnames:
+ with open(fname, "r", encoding="utf-8") as fh:
+ while line := fh.readline():
+ if m := self.re_lo_iface.match(line):
+ iface_name = m.group("name")
+ if iface_name not in ifaces:
+ ifaces[iface_name] = set()
+ while line.endswith("\\\n"):
+ line += fh.readline()
+ line = line.replace("\\\n", " ")
+ line = self.re_comment.sub(" ", line)
+ line = self.re_ws.sub(" ", line)
+ for m2 in self.re_lo_func.finditer(line):
+ ifaces[iface_name].add(m2.group("name"))
+
+ implementations: dict[str, set[str]] = {} # iface_name => {impl_names}
+ for iface_name in ifaces:
+ implementations[iface_name] = set()
+ for fname in arg_c_fnames:
+ with open(fname, "r", encoding="utf-8") as fh:
+ for line in fh:
+ line = line.strip()
+ if m := self.re_lo_implementation.match(line):
+ implementations[m.group("iface")].add(m.group("impl_name"))
+
+ objcalls: dict[str, set[QName]] = {} # method_name => {method_impls}
+ for iface_name, iface in ifaces.items():
+ for method_name in iface:
+ if method_name not in objcalls:
+ objcalls[method_name] = set()
+ for impl_name in implementations[iface_name]:
+ objcalls[method_name].add(QName(impl_name + "_" + method_name))
+ self.objcalls = objcalls
+
+ def is_intrhandler(self, name: QName) -> bool:
+ return False
+
+ def init_array(self) -> typing.Collection[QName]:
+ return []
+
+ def extra_includes(self) -> typing.Collection[BaseName]:
+ return []
+
+ def extra_nodes(self) -> typing.Collection[Node]:
+ return []
+
+ def mutate_node(self, node: Node) -> None:
+ pass
+
+ def indirect_callees(
+ self, loc: str, line: str
+ ) -> tuple[typing.Collection[QName], bool] | None:
+ if "/3rd-party/" in loc:
+ return None
+ if m := self.re_lo_call.fullmatch(line):
+ meth = m.group("meth")
+ if meth in self.objcalls:
+ callees: typing.Collection[QName] = self.objcalls[meth]
+ if len(callees) == 0:
+ raise ValueError(f"{loc}: no implementors of {meth}")
+ if meth == "writev" and "lib9p/srv.c" in loc: # KLUDGE
+ callees = [
+ c for c in callees if c.base() != BaseName("rread_writev")
+ ]
+ return callees, False
+ return [
+ QName(f"__indirect_call:{m.group('obj')}.vtable->{m.group('meth')}")
+ ], False
+ return None
+
+ def skipmodels(self) -> dict[BaseName, analyze.SkipModel]:
+ return {}
+
+
+class LibHWPlugin:
+ pico_platform: str
+ libmisc: LibMiscPlugin
+
+ def __init__(self, arg_pico_platform: str, libmisc: LibMiscPlugin) -> None:
+ self.pico_platform = arg_pico_platform
+ self.libmisc = libmisc
+
+ def is_intrhandler(self, name: QName) -> bool:
+ return name.base() in [
+ BaseName("rp2040_hwtimer_intrhandler"),
+ BaseName("hostclock_handle_sig_alarm"),
+ BaseName("hostnet_handle_sig_io"),
+ BaseName("gpioirq_handler"),
+ BaseName("dmairq_handler"),
+ ]
+
+ def init_array(self) -> typing.Collection[QName]:
+ return []
+
+ def extra_includes(self) -> typing.Collection[BaseName]:
+ return []
+
+ def extra_nodes(self) -> typing.Collection[Node]:
+ return []
+
+ def mutate_node(self, node: Node) -> None:
+ pass
+
+ def indirect_callees(
+ self, loc: str, line: str
+ ) -> tuple[typing.Collection[QName], bool] | None:
+ if "/3rd-party/" in loc:
+ return None
+ for fn in [
+ "io_readv",
+ "io_writev",
+ "io_close",
+ "io_close_read",
+ "io_close_write",
+ "io_readwritev",
+ ]:
+ if f"{fn}(" in line:
+ return self.libmisc.indirect_callees(loc, f"LO_CALL(x, {fn[3:]})")
+ for fn in [
+ "io_read",
+ "io_write",
+ ]:
+ if f"{fn}(" in line:
+ # Like above, but add a "v" to the end.
+ return self.libmisc.indirect_callees(loc, f"LO_CALL(x, {fn[3:]}v)")
+ if "trigger->cb(trigger->cb_arg)" in line:
+ ret = [
+ QName("alarmclock_sleep_intrhandler"),
+ ]
+ if self.pico_platform == "rp2040":
+ ret += [
+ QName("w5500_tcp_alarm_handler"),
+ QName("w5500_udp_alarm_handler"),
+ ]
+ return ret, False
+ if "/rp2040_gpioirq.c:" in loc and "handler->fn" in line:
+ return [
+ QName("w5500_intrhandler"),
+ ], False
+ if "/rp2040_dma.c:" in loc and "handler->fn" in line:
+ return [
+ QName("rp2040_hwspi_intrhandler"),
+ ], False
+ return None
+
+ def skipmodels(self) -> dict[BaseName, analyze.SkipModel]:
+ return {}
+
+
+class LibCRPlugin:
+ def is_intrhandler(self, name: QName) -> bool:
+ return name.base() in [
+ BaseName("_cr_gdb_intrhandler"),
+ ]
+
+ def init_array(self) -> typing.Collection[QName]:
+ return []
+
+ def extra_includes(self) -> typing.Collection[BaseName]:
+ return []
+
+ def extra_nodes(self) -> typing.Collection[Node]:
+ return []
+
+ def mutate_node(self, node: Node) -> None:
+ pass
+
+ def indirect_callees(
+ self, loc: str, line: str
+ ) -> tuple[typing.Collection[QName], bool] | None:
+ return None
+
+ def skipmodels(self) -> dict[BaseName, analyze.SkipModel]:
+ return {}
+
+
+class LibCRIPCPlugin:
+ def is_intrhandler(self, name: QName) -> bool:
+ return False
+
+ def init_array(self) -> typing.Collection[QName]:
+ return []
+
+ def extra_includes(self) -> typing.Collection[BaseName]:
+ return []
+
+ def extra_nodes(self) -> typing.Collection[Node]:
+ return []
+
+ def mutate_node(self, node: Node) -> None:
+ pass
+
+ def indirect_callees(
+ self, loc: str, line: str
+ ) -> tuple[typing.Collection[QName], bool] | None:
+ if "/3rd-party/" in loc:
+ return None
+ if "/chan.c:" in loc and "front->dequeue(" in line:
+ return [
+ QName("cr_chan_dequeue"),
+ QName("cr_select_dequeue"),
+ ], False
+ return None
+
+ def skipmodels(self) -> dict[BaseName, analyze.SkipModel]:
+ return {}
+
+
+class Lib9PPlugin:
+ re_lib9p_msg_entry = re.compile(r"^\s*_MSG\((?P<typ>\S+)\),$")
+
+ lib9p_msgs: set[str]
+ _CONFIG_9P_MAX_CONNS: int | None
+ _CONFIG_9P_MAX_REQS: int | None
+
+ def __init__(
+ self,
+ arg_base_dir: str,
+ arg_c_fnames: typing.Collection[str],
+ ) -> None:
+ # Find filenames #######################################################
+
+ def _is_config_h(fname: str) -> bool:
+ if not fname.startswith(arg_base_dir + "/"):
+ return False
+ suffix = fname[len(arg_base_dir) + 1 :]
+ if suffix.startswith("3rd-party/"):
+ return False
+ return suffix.endswith("/config.h")
+
+ config_h_fname = util.get_zero_or_one(_is_config_h, arg_c_fnames)
+
+ lib9p_srv_c_fname = util.get_zero_or_one(
+ lambda fname: fname.endswith("lib9p/srv.c"), arg_c_fnames
+ )
+
+ lib9p_generated_c_fname = util.get_zero_or_one(
+ lambda fname: fname.endswith("lib9p/core_generated.c"), arg_c_fnames
+ )
+
+ # Read config ##########################################################
+
+ def config_h_get(varname: str) -> int | None:
+ if config_h_fname:
+ line = subprocess.run(
+ ["cpp"],
+ input=f'#include "{config_h_fname}"\n{varname}\n',
+ check=True,
+ capture_output=True,
+ encoding="utf-8",
+ ).stdout.split("\n")[-2]
+ return int(eval(line)) # pylint: disable=eval-used
+ return None
+
+ self._CONFIG_9P_MAX_CONNS = config_h_get("_CONFIG_9P_MAX_CONNS")
+ self._CONFIG_9P_MAX_REQS = config_h_get("_CONFIG_9P_MAX_REQS")
+
+ # Read sources #########################################################
+
+ lib9p_msgs: set[str] = set()
+ if lib9p_generated_c_fname:
+ with open(lib9p_generated_c_fname, "r", encoding="utf-8") as fh:
+ for line in fh:
+ line = line.rstrip()
+ if m := self.re_lib9p_msg_entry.fullmatch(line):
+ typ = m.group("typ")
+ lib9p_msgs.add(typ)
+ self.lib9p_msgs = lib9p_msgs
+
+ def thread_count(self, name: QName) -> int:
+ assert self._CONFIG_9P_MAX_CONNS
+ assert self._CONFIG_9P_MAX_REQS
+ if "read" in str(name.base()):
+ return self._CONFIG_9P_MAX_CONNS
+ if "write" in str(name.base()):
+ return self._CONFIG_9P_MAX_REQS
+ return 1
+
+ def is_intrhandler(self, name: QName) -> bool:
+ return False
+
+ def init_array(self) -> typing.Collection[QName]:
+ return []
+
+ def extra_includes(self) -> typing.Collection[BaseName]:
+ return []
+
+ def extra_nodes(self) -> typing.Collection[Node]:
+ return []
+
+ def mutate_node(self, node: Node) -> None:
+ pass
+
+ re_table_call = re.compile(
+ r"\s*_lib9p_(?P<meth>validate|unmarshal|marshal)\(.*(?P<grp>[RT])msg.*\);\s*"
+ )
+ re_print_call = re.compile(r".*lib9p_table_msg.*\.print\(.*")
+
+ def indirect_callees(
+ self, loc: str, line: str
+ ) -> tuple[typing.Collection[QName], bool] | None:
+ if "/3rd-party/" in loc:
+ return None
+ if self.lib9p_msgs and "lib9p/core.c:" in loc:
+ if m := self.re_table_call.fullmatch(line):
+ meth = m.group("meth")
+ grp = m.group("grp")
+ # Functions for disabled protocol extensions will be missing.
+ return [
+ QName(f"{meth}_{msg}")
+ for msg in self.lib9p_msgs
+ if msg.startswith(grp)
+ ], True
+ if self.re_print_call.fullmatch(line):
+ # Functions for disabled protocol extensions will be missing.
+ return [QName(f"fmt_print_{msg}") for msg in self.lib9p_msgs], True
+ if "lib9p/srv.c:" in loc:
+ if "srv->msglog(" in line:
+ # Actual ROMs shouldn't set this, and so will be missing on rp2040 builds.
+ return [QName("log_msg")], True
+ return None
+
+ def skipmodels(self) -> dict[BaseName, analyze.SkipModel]:
+ return {}
+
+
+class PicoSDKPlugin:
+ get_init_array: typing.Callable[[], typing.Collection[QName]]
+ app_init_array: typing.Collection[QName] | None
+ app_preinit_array: typing.Collection[QName]
+ _PICO_PANIC_FUNCTION: str | None
+
+ def __init__(
+ self,
+ *,
+ get_init_array: typing.Callable[[], typing.Collection[QName]],
+ PICO_PANIC_FUNCTION: str | None,
+ ) -> None:
+ # grep for '__attribute__((constructor))' / '[[gnu::constructor]]'.
+ self.get_init_array = get_init_array
+ self.app_init_array = None
+
+ # git grep '^PICO_RUNTIME_INIT_FUNC\S*('
+ self.app_preinit_array = [
+ # QName("runtime_init_mutex"), # pico_mutex
+ # QName("runtime_init_default_alarm_pool"), # pico_time
+ # QName("runtime_init_boot_locks_reset"), # hardware_boot_lock
+ QName("runtime_init_per_core_irq_priorities"), # hardware_irq
+ # QName("spinlock_set_extexclall"), # hardware_sync_spin_lock
+ QName("__aeabi_bits_init"), # pico_bit_ops
+ # QName("runtime_init_bootrom_locking_enable"), # pico_bootrom, rp2350-only
+ # QName("runtime_init_pre_core_tls_setup"), # pico_clib_interface, picolibc-only
+ # QName("__aeabi_double_init"), # pico_double
+ # QName("__aeabi_float_init"), # pico_float
+ QName("__aeabi_mem_init"), # pico_mem_ops
+ QName("first_per_core_initializer"), # pico_runtime
+ # pico_runtime_init
+ # QName("runtime_init_bootrom_reset"), # rp2350-only
+ # QName("runtime_init_per_core_bootrom_reset"), # rp2350-only
+ # QName("runtime_init_per_core_h3_irq_registers"), # rp2350-only
+ QName("runtime_init_early_resets"),
+ QName("runtime_init_usb_power_down"),
+ # QName("runtime_init_per_core_enable_coprocessors"), # PICO_RUNTIME_SKIP_INIT_PER_CORE_ENABLE_COPROCESSORS
+ QName("runtime_init_clocks"),
+ QName("runtime_init_post_clock_resets"),
+ QName("runtime_init_rp2040_gpio_ie_disable"),
+ QName("runtime_init_spin_locks_reset"),
+ QName("runtime_init_install_ram_vector_table"),
+ ]
+
+ self._PICO_PANIC_FUNCTION = PICO_PANIC_FUNCTION
+
+ def is_intrhandler(self, name: QName) -> bool:
+ return name.base() in [
+ BaseName("isr_invalid"),
+ BaseName("isr_nmi"),
+ BaseName("isr_hardfault"),
+ BaseName("isr_svcall"),
+ BaseName("isr_pendsv"),
+ BaseName("isr_systick"),
+ *[BaseName(f"isr_irq{n}") for n in range(32)],
+ ]
+
+ def init_array(self) -> typing.Collection[QName]:
+ return []
+
+ def extra_includes(self) -> typing.Collection[BaseName]:
+ return []
+
+ def indirect_callees(
+ self, loc: str, line: str
+ ) -> tuple[typing.Collection[QName], bool] | None:
+ if "/3rd-party/pico-sdk/" not in loc or "/3rd-party/pico-sdk/lib/" in loc:
+ return None
+ m = util.re_call_other.fullmatch(line)
+ call: str | None = m.group("func") if m else None
+
+ match call:
+ case "connect_internal_flash_func":
+ return [
+ QName("rom_func_lookup(ROM_FUNC_CONNECT_INTERNAL_FLASH)")
+ ], False
+ case "flash_exit_xip_func":
+ return [QName("rom_func_lookup(ROM_FUNC_FLASH_EXIT_XIP)")], False
+ case "flash_range_erase_func":
+ return [QName("rom_func_lookup(ROM_FUNC_FLASH_RANGE_ERASE)")], False
+ case "flash_flush_cache_func":
+ return [QName("rom_func_lookup(ROM_FUNC_FLASH_FLUSH_CACHE)")], False
+ case "flash_range_program_func":
+ return [QName("rom_func_lookup(ROM_FUNC_FLASH_RANGE_PROGRAM)")], False
+ case "rom_table_lookup":
+ return [QName("rom_hword_as_ptr(BOOTROM_TABLE_LOOKUP_OFFSET)")], False
+ if "/flash.c:" in loc and "boot2_copyout" in line:
+ return [QName("_stage2_boot")], False
+ if "/stdio.c:" in loc:
+ if call == "out_func":
+ return [
+ QName("stdio_out_chars_crlf"),
+ QName("stdio_out_chars_no_crlf"),
+ ], False
+ if call and (call.startswith("d->") or call.startswith("driver->")):
+ _, meth = call.split("->", 1)
+ match meth:
+ case "out_chars":
+ return [QName("stdio_uart_out_chars")], False
+ case "out_flush":
+ return [QName("stdio_uart_out_flush")], False
+ case "in_chars":
+ return [QName("stdio_uart_in_chars")], False
+ if "/newlib_interface.c:" in loc:
+ if line == "(*p)();":
+ if self.app_init_array is None:
+ self.app_init_array = self.get_init_array()
+ return self.app_init_array, False
+ if "/pico_runtime/runtime.c:" in loc:
+ return self.app_preinit_array, False
+ return None
+
+ def skipmodels(self) -> dict[BaseName, analyze.SkipModel]:
+ return {}
+
+ def extra_nodes(self) -> typing.Collection[Node]:
+ ret = []
+
+ # src/src/rp2_common/pico_crt0/crt0.S
+ for n in range(32):
+ ret += [synthetic_node(f"isr_irq{n}", 0, {"__unhandled_user_irq"})]
+ ret += [
+ synthetic_node("isr_invalid", 0, {"__unhandled_user_irq"}),
+ synthetic_node("isr_nmi", 0, {"__unhandled_user_irq"}),
+ synthetic_node("isr_hardfault", 0, {"__unhandled_user_irq"}),
+ synthetic_node("isr_svcall", 0, {"__unhandled_user_irq"}),
+ synthetic_node("isr_pendsv", 0, {"__unhandled_user_irq"}),
+ synthetic_node("isr_systick", 0, {"__unhandled_user_irq"}),
+ synthetic_node("__unhandled_user_irq", 0),
+ synthetic_node("_entry_point", 0, {"_reset_handler"}),
+ synthetic_node("_reset_handler", 0, {"runtime_init", "main", "exit"}),
+ ]
+
+ # src/rp2_common/pico_int64_ops/pico_int64_ops_aeabi.S
+ ret += [
+ synthetic_node("__wrap___aeabi_lmul", 4),
+ ]
+
+ # src/rp2_common/hardware_divider/include/hardware/divider_helper.S
+ save_div_state_and_lr = 5 * 4
+ # src/rp2_common/pico_divider/divider_hardware.S
+ save_div_state_and_lr_64 = 5 * 4
+ ret += [
+ # s32 aliases
+ synthetic_node("div_s32s32", 0, {"divmod_s32s32"}),
+ synthetic_node("__wrap___aeabi_idiv", 0, {"divmod_s32s32"}),
+ synthetic_node("__wrap___aeabi_idivmod", 0, {"divmod_s32s32"}),
+ # s32 impl
+ synthetic_node("divmod_s32s32", 0, {"divmod_s32s32_savestate"}),
+ synthetic_node(
+ "divmod_s32s32_savestate",
+ save_div_state_and_lr,
+ {"divmod_s32s32_unsafe"},
+ ),
+ synthetic_node("divmod_s32s32_unsafe", 2 * 4, {"__aeabi_idiv0"}),
+ # u32 aliases
+ synthetic_node("div_u32u32", 0, {"divmod_u32u32"}),
+ synthetic_node("__wrap___aeabi_uidiv", 0, {"divmod_u32u32"}),
+ synthetic_node("__wrap___aeabi_uidivmod", 0, {"divmod_u32u32"}),
+ # u32 impl
+ synthetic_node("divmod_u32u32", 0, {"divmod_u32u32_savestate"}),
+ synthetic_node(
+ "divmod_u32u32_savestate",
+ save_div_state_and_lr,
+ {"divmod_u32u32_unsafe"},
+ ),
+ synthetic_node("divmod_u32u32_unsafe", 2 * 4, {"__aeabi_idiv0"}),
+ # s64 aliases
+ synthetic_node("div_s64s64", 0, {"divmod_s64s64"}),
+ synthetic_node("__wrap___aeabi_ldiv", 0, {"divmod_s64s64"}),
+ synthetic_node("__wrap___aeabi_ldivmod", 0, {"divmod_s64s64"}),
+ # s64 impl
+ synthetic_node("divmod_s64s64", 0, {"divmod_s64s64_savestate"}),
+ synthetic_node(
+ "divmod_s64s64_savestate",
+ save_div_state_and_lr_64 + (2 * 4),
+ {"divmod_s64s64_unsafe"},
+ ),
+ synthetic_node(
+ "divmod_s64s64_unsafe", 4, {"divmod_u64u64_unsafe", "__aeabi_ldiv0"}
+ ),
+ # u64 aliases
+ synthetic_node("div_u64u64", 0, {"divmod_u64u64"}),
+ synthetic_node("__wrap___aeabi_uldivmod", 0, {"divmod_u64u64"}),
+ # u64 impl
+ synthetic_node("divmod_u64u64", 0, {"divmod_u64u64_savestate"}),
+ synthetic_node(
+ "divmod_u64u64_savestate",
+ save_div_state_and_lr_64 + (2 * 4),
+ {"divmod_u64u64_unsafe"},
+ ),
+ synthetic_node(
+ "divmod_u64u64_unsafe", (1 + 1 + 2 + 5 + 5 + 2) * 4, {"__aeabi_ldiv0"}
+ ),
+ # *_rem
+ synthetic_node("divod_s64s64_rem", 2 * 4, {"divmod_s64s64"}),
+ synthetic_node("divod_u64u64_rem", 2 * 4, {"divmod_u64u64"}),
+ ]
+
+ # src/rp2_common/pico_mem_ops/mem_ops_aeabi.S
+ ret += [
+ synthetic_node("__aeabi_mem_init", 0, {"rom_funcs_lookup"}),
+ synthetic_node(
+ "__wrap___aeabi_memset", 0, {"rom_func_lookup(ROM_FUNC_MEMSET)"}
+ ),
+ synthetic_node("__wrap___aeabi_memset4", 0, {"__wrap___aeabi_memset8"}),
+ synthetic_node(
+ "__wrap___aeabi_memset8", 0, {"rom_func_lookup(ROM_FUNC_MEMSET4)"}
+ ),
+ synthetic_node("__wrap___aeabi_memcpy4", 0, {"__wrap___aeabi_memcpy8"}),
+ synthetic_node(
+ "__wrap___aeabi_memcpy7", 0, {"rom_func_lookup(ROM_FUNC_MEMCPY4)"}
+ ),
+ synthetic_node("__wrap_memset", 0, {"rom_func_lookup(ROM_FUNC_MEMSET)"}),
+ synthetic_node("__wrap___aeabi_memcpy", 0, {"__wrap_memcpy"}),
+ synthetic_node("__wrap_memcpy", 0, {"rom_func_lookup(ROM_FUNC_MEMCPY)"}),
+ ]
+
+ # src/rp2_common/pico_bit_ops/bit_ops_aeabi.S
+ ret += [
+ synthetic_node("__aeabi_bits_init", 0, {"rom_funcs_lookup"}),
+ synthetic_node("__wrap___clz", 0, {"__wrap___clzsi2"}),
+ synthetic_node("__wrap___clzl", 0, {"__wrap___clzsi2"}),
+ synthetic_node("__wrap___clzsi2", 0, {"rom_func_lookup(ROM_FUNC_CLZ32)"}),
+ synthetic_node("__wrap___ctzsi2", 0, {"rom_func_lookup(ROM_FUNC_CTZ32)"}),
+ synthetic_node(
+ "__wrap___popcountsi2", 0, {"rom_func_lookup(ROM_FUNC_POPCOUNT32)"}
+ ),
+ synthetic_node("__wrap___clzll", 0, {"__wrap___clzdi2"}),
+ synthetic_node("__wrap___clzdi2", 4, {"rom_func_lookup(ROM_FUNC_CLZ32)"}),
+ synthetic_node("__wrap___ctzdi2", 4, {"rom_func_lookup(ROM_FUNC_CTZ32)"}),
+ synthetic_node(
+ "__wrap___popcountdi2", 3 * 4, {"rom_func_lookup(ROM_FUNC_POPCOUNT32)"}
+ ),
+ synthetic_node("__rev", 0, {"reverse32"}),
+ synthetic_node("__revl", 0, {"reverse32"}),
+ synthetic_node("reverse32", 0, {"rom_func_lookup(ROM_FUNC_REVERSE32)"}),
+ synthetic_node("__revll", 0, {"reverse64"}),
+ synthetic_node("reverse64", 3 * 4, {"rom_func_lookup(ROM_FUNC_REVERSE32)"}),
+ ]
+
+ # src/rp2040/boot_stage2/boot2_${name,,}.S for name=W25Q080,
+ # controlled by `#define PICO_BOOT_STAGE2_{name} 1` in
+ # src/boards/include/boards/pico.h
+ ret += [
+ # synthetic_node("_stage2_boot", 0), # TODO
+ ]
+
+ # https://github.com/raspberrypi/pico-bootrom-rp2040
+ ret += [
+ # synthetic_node("rom_func_lookup(ROM_FUNC_CONNECT_INTERNAL_FLASH)", 0), # TODO
+ # synthetic_node("rom_func_lookup(ROM_FUNC_FLASH_EXIT_XIP)", 0), # TODO
+ # synthetic_node("rom_func_lookup(ROM_FUNC_FLASH_FLUSH_CACHE)", 0), # TODO
+ # synthetic_node("rom_hword_as_ptr(BOOTROM_TABLE_LOOKUP_OFFSET)", 0), # TODO
+ ]
+
+ return ret
+
+ def mutate_node(self, node: Node) -> None:
+ if self._PICO_PANIC_FUNCTION and node.funcname.base() == BaseName("panic"):
+ # inline assembly from src/rp2_common/pico_platform_panic/panic.c
+ assert node.nstatic == 0
+ assert node.ndynamic == 0
+ assert len(node.calls) == 0
+ node.nstatic += 4
+ node.calls[QName(self._PICO_PANIC_FUNCTION)] = False
+
+
+class TinyUSBDevicePlugin:
+ re_tud_class = re.compile(
+ r"^\s*#\s*define\s+(?P<k>CFG_TUD_(?:\S{3}|AUDIO|VIDEO|MIDI|VENDOR|USBTMC|DFU_RUNTIME|ECM_RNDIS))\s+(?P<v>\S+).*"
+ )
+ re_tud_entry = re.compile(
+ r"^\s+\.(?P<meth>\S+)\s*=\s*(?P<impl>[a-zA-Z0-9_]+)(?:,.*)?"
+ )
+ re_tud_if1 = re.compile(r"^\s*#\s*if (\S+)\s*")
+ re_tud_if2 = re.compile(r"^\s*#\s*if (\S+)\s*\|\|\s*(\S+)\s*")
+ re_tud_endif = re.compile(r"^\s*#\s*endif\s*")
+
+ tud_drivers: dict[str, set[QName]] # method_name => {method_impls}
+
+ def __init__(self, arg_c_fnames: typing.Collection[str]) -> None:
+ usbd_c_fname = util.get_zero_or_one(
+ lambda fname: fname.endswith("/tinyusb/src/device/usbd.c"), arg_c_fnames
+ )
+
+ tusb_config_h_fname = util.get_zero_or_one(
+ lambda fname: fname.endswith("/tusb_config.h"), arg_c_fnames
+ )
+
+ if not usbd_c_fname:
+ self.tud_drivers = {}
+ return
+
+ assert tusb_config_h_fname
+ tusb_config: dict[str, bool] = {}
+ with open(tusb_config_h_fname, "r", encoding="utf-8") as fh:
+ in_table = False
+ for line in fh:
+ line = line.rstrip()
+ if m := self.re_tud_class.fullmatch(line):
+ k = m.group("k")
+ v = m.group("v")
+ tusb_config[k] = bool(int(v))
+
+ tud_drivers: dict[str, set[QName]] = {}
+ with open(usbd_c_fname, "r", encoding="utf-8") as fh:
+ in_table = False
+ enabled = True
+ for line in fh:
+ line = line.rstrip()
+ if in_table:
+ if m := self.re_tud_if1.fullmatch(line):
+ enabled = tusb_config[m.group(1)]
+ elif m := self.re_tud_if2.fullmatch(line):
+ enabled = tusb_config[m.group(1)] or tusb_config[m.group(2)]
+ elif self.re_tud_endif.fullmatch(line):
+ enabled = True
+ if m := self.re_tud_entry.fullmatch(line):
+ meth = m.group("meth")
+ impl = m.group("impl")
+ if meth == "name" or not enabled:
+ continue
+ if meth not in tud_drivers:
+ tud_drivers[meth] = set()
+ if impl != "NULL":
+ tud_drivers[meth].add(QName(impl))
+ if line.startswith("}"):
+ in_table = False
+ elif " _usbd_driver[] = {" in line:
+ in_table = True
+ self.tud_drivers = tud_drivers
+
+ def is_intrhandler(self, name: QName) -> bool:
+ return False
+
+ def init_array(self) -> typing.Collection[QName]:
+ return []
+
+ def extra_includes(self) -> typing.Collection[BaseName]:
+ return []
+
+ def extra_nodes(self) -> typing.Collection[Node]:
+ return []
+
+ def mutate_node(self, node: Node) -> None:
+ pass
+
+ def indirect_callees(
+ self, loc: str, line: str
+ ) -> tuple[typing.Collection[QName], bool] | None:
+ if "/tinyusb/" not in loc or "/tinyusb/src/host/" in loc or "_host.c:" in loc:
+ return None
+ m = util.re_call_other.fullmatch(line)
+ assert m
+ call = m.group("func")
+ if call == "_ctrl_xfer.complete_cb":
+ ret = {
+ # QName("process_test_mode_cb"),
+ QName("tud_vendor_control_xfer_cb"),
+ }
+ ret.update(self.tud_drivers["control_xfer_cb"])
+ return ret, False
+ if call.startswith("driver->"):
+ meth = call[len("driver->") :]
+ callees = self.tud_drivers[meth]
+ if len(callees) == 0:
+ if meth == "sof":
+ return [QName(f"__indirect_call_with_null_check:{call}")], False
+ raise ValueError(f"{loc}: no implementors of {meth}")
+ return callees, False
+ if call == "event.func_call.func":
+ # callback from usb_defer_func()
+ return [
+ QName("__indirect_call_with_null_check:event.func_call.func")
+ ], False
+
+ return None
+
+ def skipmodels(self) -> dict[BaseName, analyze.SkipModel]:
+ return {}
+
+
+class NewlibPlugin:
+ def is_intrhandler(self, name: QName) -> bool:
+ return False
+
+ def init_array(self) -> typing.Collection[QName]:
+ return [QName("register_fini")]
+
+ def extra_includes(self) -> typing.Collection[BaseName]:
+ return [
+ # register_fini() calls atexit(__libc_fini_array)
+ BaseName("__libc_fini_array"),
+ ]
+
+ def extra_nodes(self) -> typing.Collection[Node]:
+ ret = []
+
+ # This is accurate to
+ # /usr/arm-none-eabi/lib/thumb/v6-m/nofp/libg.a as of
+ # Parabola's arm-none-eabi-newlib 4.5.0.20241231-1.
+
+ # malloc
+ ret += [
+ synthetic_node("free", 8, {"_free_r"}),
+ synthetic_node("malloc", 8, {"_malloc_r"}),
+ synthetic_node("realloc", 8, {"_realloc_r"}),
+ synthetic_node("aligned_alloc", 8, {"_memalign_r"}),
+ synthetic_node("reallocarray", 24, {"realloc", "__errno"}),
+ # synthetic_node("_free_r", 0), # TODO
+ # synthetic_node("_malloc_r", 0), # TODO
+ # synthetic_node("_realloc_r", 0), # TODO
+ # synthetic_node("_memalign_r", 0), # TODO
+ ]
+
+ # execution
+ ret += [
+ synthetic_node("raise", 16, {"_getpid_r"}),
+ synthetic_node("abort", 8, {"raise", "_exit"}),
+ synthetic_node("longjmp", 0),
+ synthetic_node("setjmp", 0),
+ ]
+
+ # <strings.h>
+ ret += [
+ synthetic_node("memcmp", 12),
+ synthetic_node("strcmp", 16),
+ synthetic_node("strlen", 8),
+ synthetic_node("strncpy", 16),
+ synthetic_node("strnlen", 8),
+ ]
+
+ # other
+ ret += [
+ synthetic_node("__errno", 0),
+ synthetic_node("_getpid_r", 8, {"_getpid"}),
+ synthetic_node("random", 8),
+ synthetic_node("register_fini", 8, {"atexit"}),
+ synthetic_node("atexit", 8, {"__register_exitproc"}),
+ synthetic_node(
+ "__register_exitproc",
+ 32,
+ {
+ "__retarget_lock_acquire_recursive",
+ "__retarget_lock_release_recursive",
+ },
+ ),
+ synthetic_node("__libc_fini_array", 16, {"_fini"}),
+ ]
+
+ return ret
+
+ def mutate_node(self, node: Node) -> None:
+ pass
+
+ def indirect_callees(
+ self, loc: str, line: str
+ ) -> tuple[typing.Collection[QName], bool] | None:
+ return None
+
+ def skipmodels(self) -> dict[BaseName, analyze.SkipModel]:
+ return {}
+
+
+class LibGCCPlugin:
+ def is_intrhandler(self, name: QName) -> bool:
+ return False
+
+ def init_array(self) -> typing.Collection[QName]:
+ return []
+
+ def extra_includes(self) -> typing.Collection[BaseName]:
+ return []
+
+ def extra_nodes(self) -> typing.Collection[Node]:
+ # This is accurate to Parabola's arm-none-eabi-gcc 14.2.0-1.
+ return [
+ # /usr/lib/gcc/arm-none-eabi/14.2.0/thumb/v6-m/nofp/libgcc.a
+ synthetic_node("__aeabi_idiv0", 0),
+ synthetic_node("__aeabi_ldiv0", 0),
+ synthetic_node("__aeabi_llsr", 0),
+ # /usr/lib/gcc/arm-none-eabi/14.2.0/thumb/v6-m/nofp/crti.o
+ synthetic_node("_fini", 24),
+ ]
+
+ def mutate_node(self, node: Node) -> None:
+ pass
+
+ def indirect_callees(
+ self, loc: str, line: str
+ ) -> tuple[typing.Collection[QName], bool] | None:
+ return None
+
+ def skipmodels(self) -> dict[BaseName, analyze.SkipModel]:
+ return {}