Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 3 additions & 86 deletions LLDBPlugin/touchlab_kotlin_lldb/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import os
from typing import Optional

import lldb

Expand All @@ -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")
Expand All @@ -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")


Expand All @@ -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)

Expand Down Expand Up @@ -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__))
7 changes: 6 additions & 1 deletion LLDBPlugin/touchlab_kotlin_lldb/cache/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Optional
from typing import Dict, Optional, Set

import lldb

Expand Down Expand Up @@ -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 `^<module>\.` registered but ObjC prefix not yet
# readable; key -> attempt count.
self.pending_prefix: Dict[str, int] = {}
165 changes: 165 additions & 0 deletions LLDBPlugin/touchlab_kotlin_lldb/module_registration.py
Original file line number Diff line number Diff line change
@@ -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 `^<prefix>`. 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; '
'^<prefix> 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 `^<module>\\.` 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)
9 changes: 9 additions & 0 deletions LLDBPlugin/touchlab_kotlin_lldb/stepping/KonanHook.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
)
Expand Down