diff --git a/LLDBPlugin/touchlab_kotlin_lldb/__init__.py b/LLDBPlugin/touchlab_kotlin_lldb/__init__.py index 8196b49..58d67d1 100644 --- a/LLDBPlugin/touchlab_kotlin_lldb/__init__.py +++ b/LLDBPlugin/touchlab_kotlin_lldb/__init__.py @@ -1,5 +1,4 @@ import os -from typing import Optional import lldb @@ -8,16 +7,13 @@ from .util.log import log from .commands import FieldTypeCommand, SymbolByNameCommand, TypeByAddressCommand, GCCollectCommand -from .types.summary import kotlin_object_type_summary, kotlin_objc_class_summary -from .types.proxy import KonanProxyTypeProvider, KonanObjcProxyTypeProvider +from .types.summary import kotlin_object_type_summary +from .types.proxy import KonanProxyTypeProvider from .cache import LLDBCache os.environ['CLIENT_TYPE'] = 'Xcode' -KONAN_INIT_PREFIX = '_Konan_init_' -KONAN_INIT_MODULE_NAME = '[0-9a-zA-Z_]+' -KONAN_INIT_SUFFIX = '_kexe' def __lldb_init_module(debugger: lldb.SBDebugger, _): log(lambda: "init start") @@ -27,8 +23,6 @@ def __lldb_init_module(debugger: lldb.SBDebugger, _): register_commands(debugger) register_hooks(debugger) - configure_objc_types_init(debugger) - log(lambda: "init end") @@ -38,78 +32,6 @@ def reset_cache(): LLDBCache.reset() -def configure_objc_types_init(debugger: lldb.SBDebugger): - target = debugger.GetDummyTarget() - breakpoint = target.BreakpointCreateByRegex( - "^{}({})({})?$".format(KONAN_INIT_PREFIX, KONAN_INIT_MODULE_NAME, KONAN_INIT_SUFFIX) - ) - breakpoint.SetOneShot(True) - breakpoint.SetAutoContinue(True) - breakpoint.SetScriptCallbackFunction('{}.{}'.format(__name__, configure_objc_types_breakpoint.__name__)) - - -def configure_objc_types_breakpoint(frame: lldb.SBFrame, bp_loc: lldb.SBBreakpointLocation, internal_dict): - process = frame.thread.process - target = process.target - - symbols = target.FindSymbols('_OBJC_CLASS_RO_$_KotlinBase') - - base_class_name: Optional[str] = None - for symbol_context in symbols: - error = lldb.SBError() - name_addr = process.ReadPointerFromMemory(symbol_context.symbol.addr.GetLoadAddress(target) + 6 * 4, error) - # TODO: Log error? - if not error.success: - continue - base_class_name = process.ReadCStringFromMemory(name_addr, 128, error) - # TODO: Log error? - if not error.success: - continue - - break - - module_name = frame.symbol.name.removeprefix(KONAN_INIT_PREFIX).removesuffix(KONAN_INIT_SUFFIX) - if module_name == "stdlib": - return False - - specifiers_to_register = [ - lldb.SBTypeNameSpecifier( - '^{}\\.'.format(module_name), - lldb.eMatchTypeRegex, - ), - ] - - if base_class_name is not None: - objc_class_prefix = base_class_name.removesuffix("Base") - specifiers_to_register.append( - lldb.SBTypeNameSpecifier( - '^{}'.format(objc_class_prefix), - lldb.eMatchTypeRegex, - ) - ) - - category = target.debugger.GetCategory(KOTLIN_CATEGORY) - - for type_specifier in specifiers_to_register: - category.AddTypeSummary( - type_specifier, - lldb.SBTypeSummary.CreateWithFunctionName( - '{}.{}'.format(__name__, kotlin_objc_class_summary.__name__), - lldb.eTypeOptionHideValue, - ) - ) - category.AddTypeSynthetic( - type_specifier, - lldb.SBTypeSynthetic.CreateWithClassName( - '{}.{}'.format(__name__, KonanObjcProxyTypeProvider.__name__), - ) - ) - - bp_loc.GetBreakpoint().SetEnabled(False) - - return False - - def configure_types(debugger: lldb.SBDebugger): category = debugger.CreateCategory(KOTLIN_CATEGORY) @@ -154,9 +76,4 @@ def register_hooks(debugger: lldb.SBDebugger): # Avoid Kotlin/Native runtime debugger.HandleCommand('settings set target.process.thread.step-avoid-regexp ^::Kotlin_') - hooks_to_register = [ - KonanHook, - ] - - for hook in hooks_to_register: - debugger.HandleCommand('target stop-hook add -P {}.{}'.format(__name__, hook.__name__)) + debugger.HandleCommand('target stop-hook add -P {}.{}'.format(__name__, KonanHook.__name__)) diff --git a/LLDBPlugin/touchlab_kotlin_lldb/cache/__init__.py b/LLDBPlugin/touchlab_kotlin_lldb/cache/__init__.py index f9b41a7..f0a8c1c 100644 --- a/LLDBPlugin/touchlab_kotlin_lldb/cache/__init__.py +++ b/LLDBPlugin/touchlab_kotlin_lldb/cache/__init__.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Dict, Optional, Set import lldb @@ -28,3 +28,8 @@ def __init__(self): self._array_header_type: Optional[lldb.SBType] = None self._runtime_type_size: Optional[lldb.value] = None self._runtime_type_alignment: Optional[lldb.value] = None + # Modules fully handled (registered or ruled out). + self.registered_module_keys: Set[str] = set() + # Kotlin modules with `^\.` registered but ObjC prefix not yet + # readable; key -> attempt count. + self.pending_prefix: Dict[str, int] = {} diff --git a/LLDBPlugin/touchlab_kotlin_lldb/module_registration.py b/LLDBPlugin/touchlab_kotlin_lldb/module_registration.py new file mode 100644 index 0000000..039e28d --- /dev/null +++ b/LLDBPlugin/touchlab_kotlin_lldb/module_registration.py @@ -0,0 +1,165 @@ +import re +from typing import List, Optional + +import lldb + +from .types.base import KOTLIN_CATEGORY +from .types.summary import kotlin_objc_class_summary +from .types.proxy import KonanObjcProxyTypeProvider +from .cache import LLDBCache +from .util.log import log + +# `_Konan_init_MyMod` (framework) or `_Konan_init_MyApp_kexe` (executable); group 1 = module name. +_KONAN_INIT_RE = re.compile(r'^_Konan_init_([0-9a-zA-Z_]+?)(_kexe)?$') +_KOTLIN_RUNTIME_MARKER = 'Kotlin_initRuntimeIfNeeded' +_KOTLIN_BASE_OBJC_SYMBOL = '_OBJC_CLASS_RO_$_KotlinBase' +# `name` pointer offset in the 64-bit ObjC class_ro_t; all current Apple targets are 64-bit. +_OBJC_CLASS_RO_NAME_OFFSET = 6 * 4 +# Skipping these by PATH ALONE — before any symbol API realizes their symtab — is the launch-time win. +_SYSTEM_PATH_MARKERS = ('/usr/lib', '/System/') +# Cap ObjC-prefix retries: the read fails until dyld rebases the class_ro_t name pointer. +_MAX_PREFIX_ATTEMPTS = 16 + + +def _module_key(module: lldb.SBModule) -> str: + # Fall back to path so two no-UUID modules don't collide on one key. + return module.GetUUIDString() or module.GetFileSpec().fullpath or '' + + +def _is_candidate_module(module: lldb.SBModule) -> bool: + directory = module.GetFileSpec().GetDirectory() or '' + return not any(marker in directory for marker in _SYSTEM_PATH_MARKERS) + + +def _is_kotlin_module(module: lldb.SBModule) -> bool: + return len(module.FindSymbols(_KOTLIN_RUNTIME_MARKER)) > 0 + + +def _kotlin_module_names(module: lldb.SBModule) -> List[str]: + names: List[str] = [] + for symbol in module.symbols: + match = _KONAN_INIT_RE.match(symbol.name or '') + if match is None: + continue + module_name = match.group(1) + if module_name == 'stdlib': + continue + names.append(module_name) + return names + + +def _read_objc_class_prefix( + target: lldb.SBTarget, + process: lldb.SBProcess, + base_symbols: lldb.SBSymbolContextList, +) -> Optional[str]: + for symbol_context in base_symbols: + error = lldb.SBError() + symbol_addr = symbol_context.symbol.addr.GetLoadAddress(target) + name_addr = process.ReadPointerFromMemory(symbol_addr + _OBJC_CLASS_RO_NAME_OFFSET, error) + if not error.success: + continue + base_class_name = process.ReadCStringFromMemory(name_addr, 128, error) + if not error.success or not base_class_name: + continue + # `or None`: an empty prefix would build `^`, matching every type. + return base_class_name.removesuffix('Base') or None + return None + + +def _register_specifiers(target: lldb.SBTarget, specifiers: List[lldb.SBTypeNameSpecifier]): + category = target.debugger.GetCategory(KOTLIN_CATEGORY) + for type_specifier in specifiers: + category.AddTypeSummary( + type_specifier, + lldb.SBTypeSummary.CreateWithFunctionName( + '{}.{}'.format(kotlin_objc_class_summary.__module__, kotlin_objc_class_summary.__name__), + lldb.eTypeOptionHideValue, + ), + ) + category.AddTypeSynthetic( + type_specifier, + lldb.SBTypeSynthetic.CreateWithClassName( + '{}.{}'.format(KonanObjcProxyTypeProvider.__module__, KonanObjcProxyTypeProvider.__name__), + ), + ) + + +def _try_register_objc_prefix( + target: lldb.SBTarget, + process: lldb.SBProcess, + cache: 'LLDBCache', + module: lldb.SBModule, + key: str, +) -> bool: + """Register `^`. True when handled (registered / no base class / cap hit), False to retry.""" + base_symbols = module.FindSymbols(_KOTLIN_BASE_OBJC_SYMBOL) + if not base_symbols: + return True # no exported ObjC base class; module-name formatters suffice + + prefix = _read_objc_class_prefix(target, process, base_symbols) + if prefix: + _register_specifiers(target, [lldb.SBTypeNameSpecifier('^{}'.format(prefix), lldb.eMatchTypeRegex)]) + log(lambda: 'Registered ObjC prefix ^{} for {}.'.format(prefix, module.GetFileSpec().GetFilename())) + return True + + attempts = cache.pending_prefix.get(key, 0) + 1 + cache.pending_prefix[key] = attempts + if attempts >= _MAX_PREFIX_ATTEMPTS: + log(lambda: 'Gave up reading ObjC prefix for {} after {} stops; ' + '^ formatting unavailable.'.format(module.GetFileSpec().GetFilename(), attempts)) + return True + return False + + +def _register_kotlin_module( + target: lldb.SBTarget, + process: lldb.SBProcess, + cache: 'LLDBCache', + module: lldb.SBModule, + key: str, +) -> bool: + """First sight of a Kotlin module: register `^\\.` now, then attempt the ObjC prefix.""" + names = _kotlin_module_names(module) + if not names: + log(lambda: 'Kotlin marker present but no module names for {}; skipping.'.format( + module.GetFileSpec().GetFilename())) + return True + _register_specifiers(target, [ + lldb.SBTypeNameSpecifier('^{}\\.'.format(name), lldb.eMatchTypeRegex) for name in names + ]) + return _try_register_objc_prefix(target, process, cache, module, key) + + +def scan_and_register_modules(execution_context: lldb.SBExecutionContext): + """Lazily register Kotlin formatters at stop time. Replaces the old global `_Konan_init_*` + regex breakpoint that realized every module's symtab at launch. Side effect only — never + influences whether the process stops.""" + target = execution_context.target + if not target.IsValid(): + return + process = target.GetProcess() + if not process.IsValid(): + return + + cache = LLDBCache.instance() + for i in range(target.GetNumModules()): + module = target.GetModuleAtIndex(i) + key = _module_key(module) + + if key in cache.registered_module_keys: + continue + + if key in cache.pending_prefix: # known Kotlin module, prefix still settling + if _try_register_objc_prefix(target, process, cache, module, key): + cache.pending_prefix.pop(key, None) + cache.registered_module_keys.add(key) + continue + + if not _is_candidate_module(module) or not _is_kotlin_module(module): + cache.registered_module_keys.add(key) + continue + + if _register_kotlin_module(target, process, cache, module, key): + cache.pending_prefix.pop(key, None) + cache.registered_module_keys.add(key) diff --git a/LLDBPlugin/touchlab_kotlin_lldb/stepping/KonanHook.py b/LLDBPlugin/touchlab_kotlin_lldb/stepping/KonanHook.py index 8bbb5e7..21683e1 100644 --- a/LLDBPlugin/touchlab_kotlin_lldb/stepping/KonanHook.py +++ b/LLDBPlugin/touchlab_kotlin_lldb/stepping/KonanHook.py @@ -3,6 +3,8 @@ from .KonanStepIn import KonanStepIn from .KonanStepOut import KonanStepOut from .KonanStepOver import KonanStepOver +from ..module_registration import scan_and_register_modules +from ..util.log import log KONAN_LLDB_DONT_SKIP_BRIDGING_FUNCTIONS = 'KONAN_LLDB_DONT_SKIP_BRIDGING_FUNCTIONS' MAX_SIZE_FOR_STOP_REASON = 20 @@ -18,6 +20,13 @@ def __init__(self, target: lldb.SBTarget, extra_args, _): pass def handle_stop(self, execution_context: lldb.SBExecutionContext, stream: lldb.SBStream) -> bool: + # Side-effect only: try/except so registration can never alter the + # stop/continue decision below. + try: + scan_and_register_modules(execution_context) + except Exception as e: + log(lambda: 'Kotlin module registration error: {}'.format(e)) + is_bridging_functions_skip_enabled = not execution_context.target.GetEnvironment().Get( KONAN_LLDB_DONT_SKIP_BRIDGING_FUNCTIONS )