diff options
Diffstat (limited to 'build-aux/measurestack/app_plugins.py')
-rw-r--r-- | build-aux/measurestack/app_plugins.py | 915 |
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 {} |