diff options
Diffstat (limited to 'build-aux/measurestack/app_plugins.py')
-rw-r--r-- | build-aux/measurestack/app_plugins.py | 1006 |
1 files changed, 1006 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..36e661b --- /dev/null +++ b/build-aux/measurestack/app_plugins.py @@ -0,0 +1,1006 @@ +# 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 typing + +from . import analyze, util +from .analyze import BaseName, Node, QName +from .util import synthetic_node + +# pylint: disable=unused-variable +__all__ = [ + "CmdPlugin", + "LibObjPlugin", + "LibHWPlugin", + "LibCRPlugin", + "LibCRIPCPlugin", + "Lib9PPlugin", + "LibMiscPlugin", + "PicoFmtPlugin", + "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 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 [], False + if "srv->rootdir" in line: + return [QName("get_root")], False + return None + + def skipmodels(self) -> dict[BaseName, analyze.SkipModel]: + return {} + + +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_[HC]\s*\(\s*(?P<iface>[^, ]+)\s*,\s*(?P<impl_typ>[^,]+)\s*,\s*(?P<impl_name>[^, ]+)\s*[,)].*" +) +re_call_objcall = re.compile(r"LO_CALL\((?P<obj>[^,]+), (?P<meth>[^,)]+)[,)].*") + + +class LibObjPlugin: + 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 := 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 = re_comment.sub(" ", line) + line = re_ws.sub(" ", line) + for m2 in 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 := 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 indirect_callees( + self, loc: str, line: str + ) -> tuple[typing.Collection[QName], bool] | None: + + if "/3rd-party/" in loc: + return None + if m := re_call_objcall.fullmatch(line): + if m.group("meth") in self.objcalls: + return self.objcalls[m.group("meth")], 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 + libobj: LibObjPlugin + + def __init__(self, arg_pico_platform: str, libobj: LibObjPlugin) -> None: + self.pico_platform = arg_pico_platform + self.libobj = libobj + + 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 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.libobj.indirect_callees(loc, f"LO_CALL(x, {fn[3:]})") + if "io_read(" in line: + return self.libobj.indirect_callees(loc, "LO_CALL(x, readv)") + if "io_writev(" in line: + return self.libobj.indirect_callees(loc, "LO_CALL(x, writev)") + 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 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 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 {} + + +re_tmessage_handler = re.compile( + r"^\s*\[LIB9P_TYP_T[^]]+\]\s*=\s*\(tmessage_handler\)\s*(?P<handler>\S+),\s*$" +) +re_lib9p_msg_entry = re.compile(r"^\s*_MSG_(?:[A-Z]+)\((?P<typ>\S+)\),$") +re_lib9p_caller = re.compile( + r"^lib9p_(?P<grp>[TR])msg_(?P<meth>validate|unmarshal|marshal)$" +) +re_lib9p_callee = re.compile( + r"^(?P<meth>validate|unmarshal|marshal)_(?P<msg>(?P<grp>[TR]).*)$" +) + + +class Lib9PPlugin: + tmessage_handlers: set[QName] | None + lib9p_msgs: set[str] + _CONFIG_9P_NUM_SOCKS: int | None + CONFIG_9P_SRV_MAX_REQS: int | None + CONFIG_9P_SRV_MAX_DEPTH: int | None + formatters: typing.Collection[BaseName] + + def __init__( + self, + arg_base_dir: str, + arg_c_fnames: typing.Collection[str], + libobj_plugin: LibObjPlugin, + ) -> None: + self.formatters = { + x.base() + for x in libobj_plugin.objcalls["format"] + if str(x.base()).startswith("lib9p_") + } + + # 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/9p.generated.c"), arg_c_fnames + ) + + # Read config ########################################################## + + def config_h_get(varname: str) -> int | None: + if config_h_fname: + with open(config_h_fname, "r", encoding="utf-8") as fh: + for line in fh: + line = line.rstrip() + if line.startswith("#define"): + parts = line.split() + if parts[1] == varname: + return int(parts[2]) + return None + + self._CONFIG_9P_NUM_SOCKS = config_h_get("_CONFIG_9P_NUM_SOCKS") + self.CONFIG_9P_SRV_MAX_REQS = config_h_get("CONFIG_9P_SRV_MAX_REQS") + self.CONFIG_9P_SRV_MAX_DEPTH = config_h_get("CONFIG_9P_SRV_MAX_DEPTH") + + # Read sources ######################################################### + + tmessage_handlers: set[QName] | None = None + if lib9p_srv_c_fname: + tmessage_handlers = set() + with open(lib9p_srv_c_fname, "r", encoding="utf-8") as fh: + for line in fh: + line = line.rstrip() + if m := re_tmessage_handler.fullmatch(line): + tmessage_handlers.add(QName(m.group("handler"))) + self.tmessage_handlers = tmessage_handlers + + 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 := 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_NUM_SOCKS + assert self.CONFIG_9P_SRV_MAX_REQS + if "read" in str(name.base()): + return self._CONFIG_9P_NUM_SOCKS + if "write" in str(name.base()): + return self._CONFIG_9P_NUM_SOCKS * self.CONFIG_9P_SRV_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 indirect_callees( + self, loc: str, line: str + ) -> tuple[typing.Collection[QName], bool] | None: + if "/3rd-party/" in loc: + return None + if ( + self.tmessage_handlers + and "/srv.c:" in loc + and "tmessage_handlers[typ](" in line + ): + # Functions for disabled protocol extensions will be missing. + return self.tmessage_handlers, True + if self.lib9p_msgs and "/9p.c:" in loc: + for meth in ["validate", "unmarshal", "marshal"]: + if line.startswith(f"tentry.{meth}("): + # Functions for disabled protocol extensions will be missing. + return [QName(f"{meth}_{msg}") for msg in self.lib9p_msgs], True + return None + + def skipmodels(self) -> dict[BaseName, analyze.SkipModel]: + ret: dict[BaseName, analyze.SkipModel] = { + BaseName("_lib9p_validate"): analyze.SkipModel( + 2, + self._skipmodel__lib9p_validate_unmarshal_marshal, + ), + BaseName("_lib9p_unmarshal"): analyze.SkipModel( + 2, + self._skipmodel__lib9p_validate_unmarshal_marshal, + ), + BaseName("_lib9p_marshal"): analyze.SkipModel( + 2, + self._skipmodel__lib9p_validate_unmarshal_marshal, + ), + BaseName("_vfctprintf"): analyze.SkipModel( + self.formatters, self._skipmodel__vfctprintf + ), + } + if isinstance(self.CONFIG_9P_SRV_MAX_DEPTH, int): + ret[BaseName("srv_util_pathfree")] = analyze.SkipModel( + self.CONFIG_9P_SRV_MAX_DEPTH, + self._skipmodel_srv_util_pathfree, + ) + return ret + + def _skipmodel__lib9p_validate_unmarshal_marshal( + self, chain: typing.Sequence[QName], call: QName + ) -> bool: + m_caller = re_lib9p_caller.fullmatch(str(chain[-2].base())) + assert m_caller + + m_callee = re_lib9p_callee.fullmatch(str(call.base())) + if not m_callee: + return False + return m_caller.group("grp") != m_callee.group("grp") + + def _skipmodel_srv_util_pathfree( + self, chain: typing.Sequence[QName], call: QName + ) -> bool: + assert isinstance(self.CONFIG_9P_SRV_MAX_DEPTH, int) + if call.base() == BaseName("srv_util_pathfree"): + return len(chain) >= self.CONFIG_9P_SRV_MAX_DEPTH and all( + c.base() == BaseName("srv_util_pathfree") + for c in chain[-self.CONFIG_9P_SRV_MAX_DEPTH :] + ) + return False + + def _skipmodel__vfctprintf( + self, chain: typing.Sequence[QName], call: QName + ) -> bool: + if call.base() == BaseName("libfmt_conv_formatter"): + return any(c.base() in self.formatters for c in chain) + return False + + +class LibMiscPlugin: + 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 indirect_callees( + self, loc: str, line: str + ) -> tuple[typing.Collection[QName], bool] | None: + return None + + def skipmodels(self) -> dict[BaseName, analyze.SkipModel]: + return { + BaseName("__assert_msg_fail"): analyze.SkipModel( + {BaseName("__assert_msg_fail")}, self._skipmodel___assert_msg_fail + ), + } + + def _skipmodel___assert_msg_fail( + self, chain: typing.Sequence[QName], call: QName + ) -> bool: + if call.base() in [BaseName("__lm_printf"), BaseName("__lm_light_printf")]: + return any( + c.base() == BaseName("__assert_msg_fail") for c in reversed(chain[:-1]) + ) + return False + + +class PicoFmtPlugin: + known_fct: dict[BaseName, BaseName] + + def __init__(self, arg_pico_platform: str) -> None: + self.known_fct = { + # pico_fmt + BaseName("fmt_vsnprintf"): BaseName("_out_buffer"), + } + match arg_pico_platform: + case "rp2040": + self.known_fct.update( + { + # pico_stdio + BaseName("__wrap_vprintf"): BaseName("stdio_buffered_printer"), + BaseName("stdio_vprintf"): BaseName("stdio_buffered_printer"), + # libfmt + BaseName("__lm_light_printf"): BaseName("libfmt_light_fct"), + } + ) + case "host": + self.known_fct.update( + { + # libfmt + BaseName("__lm_printf"): BaseName("libfmt_libc_fct"), + BaseName("__lm_light_printf"): BaseName("libfmt_libc_fct"), + } + ) + + 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 indirect_callees( + self, loc: str, line: str + ) -> tuple[typing.Collection[QName], bool] | None: + if "/3rd-party/pico-fmt/" not in loc: + return None + if "/printf.c:" in loc: + m = util.re_call_other.fullmatch(line) + call: str | None = m.group("func") if m else None + if "->fct" in line: + return [x.as_qname() for x in self.known_fct.values()], False + if "specifier_table" in line: + return [ + # pico-fmt + QName("conv_sint"), + QName("conv_uint"), + # QName("conv_double"), + QName("conv_char"), + QName("conv_str"), + QName("conv_ptr"), + QName("conv_pct"), + # libfmt + QName("libfmt_conv_formatter"), + QName("libfmt_conv_quote"), + ], False + return None + + def skipmodels(self) -> dict[BaseName, analyze.SkipModel]: + ret: dict[BaseName, analyze.SkipModel] = { + BaseName("fmt_state_putchar"): analyze.SkipModel( + self.known_fct.keys(), self._skipmodel_fmt_state_putchar + ), + } + return ret + + def _skipmodel_fmt_state_putchar( + self, chain: typing.Sequence[QName], call: QName + ) -> bool: + if call.base() in self.known_fct.values(): + fct: BaseName | None = None + for pcall in reversed(chain): + if pcall.base() in self.known_fct: + fct = self.known_fct[pcall.base()] + return call.base() != fct + return True + return False + + +class PicoSDKPlugin: + get_init_array: typing.Callable[[], typing.Collection[QName]] + app_init_array: typing.Collection[QName] | None + app_preinit_array: typing.Collection[QName] + + def __init__( + self, + *, + get_init_array: typing.Callable[[], typing.Collection[QName]], + ) -> 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"), + ] + + 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 "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/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 + + # 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"}), + ] + + ret += [ + # src/rp2_common/pico_int64_ops/pico_int64_ops_aeabi.S + synthetic_node("__wrap___aeabi_lmul", 4), + # src/rp2_common/pico_divider/divider_hardware.S + # 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 + 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 + 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 + # synthetic_node("_stage2_boot", 0), # TODO + # https://github.com/raspberrypi/pico-bootrom-rp2040 + # 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 + + +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*") + + +class TinyUSBDevicePlugin: + 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 := 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 := re_tud_if1.fullmatch(line): + enabled = tusb_config[m.group(1)] + elif m := re_tud_if2.fullmatch(line): + enabled = tusb_config[m.group(1)] or tusb_config[m.group(2)] + elif re_tud_endif.fullmatch(line): + enabled = True + if m := 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 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->"): + return self.tud_drivers[call[len("driver->") :]], False + if call == "event.func_call.func": + # callback from usb_defer_func() + return [], 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]: + # 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. + return [ + # malloc + 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 + synthetic_node("raise", 16, {"_getpid_r"}), + synthetic_node("abort", 8, {"raise", "_exit"}), + synthetic_node("longjmp", 0), + synthetic_node("setjmp", 0), + # <strings.h> + synthetic_node("memcmp", 12), + synthetic_node("memcpy", 28), + synthetic_node("memset", 20), + synthetic_node("strcmp", 16), + synthetic_node("strlen", 8), + synthetic_node("strncpy", 16), + synthetic_node("strnlen", 8), + # other + 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"}), + ] + + 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 [ + QName("libfmt_install_formatter"), + QName("libfmt_install_quote"), + ] + + 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 indirect_callees( + self, loc: str, line: str + ) -> tuple[typing.Collection[QName], bool] | None: + return None + + def skipmodels(self) -> dict[BaseName, analyze.SkipModel]: + return {} |