blob: 690fa50ccba84533abf17e5fd399bacaf200dd9b [file]
#!/usr/bin/python
##
# Copyright 2010-2025 JetBrains s.r.o.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
#
# (lldb) command script import llvmDebugInfoC/src/scripts/konan_lldb.py
# (lldb) p kotlin_variable
#
import io
import os
import re
import sys
import time
import logging
import lldb
_NULL = "null"
_BENCH_LOGGING = False
def _bench(start, msg):
if _BENCH_LOGGING:
print(f"{msg()}: {time.monotonic() - start}")
def _evaluate(expr):
target = lldb.debugger.GetSelectedTarget()
if not target:
raise DebuggerException("No target selected")
process = target.GetProcess()
if not process:
raise DebuggerException("No process found")
thread = process.GetSelectedThread()
if not thread:
# Try to select the first thread if none is selected
if process.GetNumThreads() > 0:
thread = process.GetThreadAtIndex(0)
process.SetSelectedThread(thread)
else:
raise DebuggerException("No threads available")
frame = thread.GetSelectedFrame()
if not frame:
# Try to select the first frame if none is selected
if thread.GetNumFrames() > 0:
frame = thread.GetFrameAtIndex(0)
thread.SetSelectedFrame(0)
else:
raise DebuggerException("No frames available")
# Store original frame information
original_frame = frame
original_frame_id = frame.GetFrameID()
result = frame.EvaluateExpression(expr)
# Try to find and restore the original frame
current_frame = thread.GetSelectedFrame()
if current_frame != original_frame:
logging.debug(
"Warning: Frame changed during evaluation from %s to %s",
original_frame,
current_frame,
)
# Try to find and restore the original frame
for idx in range(thread.GetNumFrames()):
frame = thread.GetFrameAtIndex(idx)
if frame.GetFrameID() == original_frame_id:
thread.SetSelectedFrame(idx)
logging.debug("Restored original frame")
break
err = result.GetError()
if not err.Success():
logging.debug(
"Expression evaluation failed: %s - %s", expr, err.description
)
raise EvaluateDebuggerException(expr, err)
logging.debug("%s => %s", expr, result)
return result
class DebuggerException(Exception):
pass
class EvaluateDebuggerException(DebuggerException):
def __init__(self, expression, error):
self.expression = expression
self.error = error
def __str__(self):
return (
f"Error evaluating `{self.expression}`: "
f"{self.error.description}"
)
def _max_children_count():
v = lldb.debugger.GetInternalVariableValue(
"target.max-children-count", lldb.debugger.GetInstanceName()
).GetStringAtIndex(0)
return int(v)
def _symbol_loaded_address(name, debugger=lldb.debugger):
target = debugger.GetSelectedTarget()
process = target.GetProcess()
thread = process.GetSelectedThread()
frame = thread.GetSelectedFrame()
candidates = frame.module.symbol[name]
# take first
for candidate in candidates:
address = candidate.GetStartAddress().GetLoadAddress(target)
logging.debug("%s %s", name, _hex(address))
return address
return 0
def _type_info_by_address(address, debugger=lldb.debugger):
target = debugger.GetSelectedTarget()
process = target.GetProcess()
thread = process.GetSelectedThread()
frame = thread.GetSelectedFrame()
candidates = list(
filter(
lambda x: x.GetStartAddress().GetLoadAddress(target) == address,
frame.module.symbols,
)
)
return candidates
def _is_string_or_array(value):
start = time.monotonic()
value_str = f"{_hex(value.unsigned)}"
string_addr = _symbol_loaded_address("kclass:kotlin.String")
expr = (
f"(int)Konan_DebugIsInstance({value_str}, {_hex(string_addr)}) ? 1 "
f": ((int)Konan_DebugIsArray({value_str})) ? 2 : 0)"
)
soa = _evaluate(expr).unsigned
logging.debug("%s: %s", value_str, soa)
_bench(start, lambda: f"is_string_or_array({value_str}) = {soa}")
return soa
def _type_info(value):
"""
This method checks self-referencing of pointer of first member of TypeInfo
including a case when an object has a meta-object pointed by TypeInfo.
Two lower bits are reserved for memory management needs,
see runtime/src/main/cpp/Memory.h.
"""
value_str = f"{_hex(value.unsigned)}"
logging.debug("%s: %s", value_str, value.GetTypeName())
if value.GetTypeName() != "ObjHeader *":
return None
result = _evaluate(
(
f"*(void **)((uintptr_t)(*(void**){value_str}) & ~0x3)"
f" == "
f"**(void***)((uintptr_t)(*(void**){value_str}) & ~0x3)"
f" ? "
f"*(void **)((uintptr_t)(*(void**){value_str}) & ~0x3)"
f" : "
f"(void *)0"
)
)
return (
result.unsigned if result.IsValid() and result.unsigned != 0 else None
)
_FACTORY = {}
# Cache type info pointer to [ChildMetaInfo]
_SYNTHETIC_OBJECT_LAYOUT_CACHE = {}
_TO_STRING_DEPTH = 2
_ARRAY_TO_STRING_LIMIT = 10
_TOTAL_MEMBERS_LIMIT = 50
_TYPE_CONVERSION = [
lambda obj, value, address, name: value.CreateValueFromExpression(
name, f"(void *){_hex(address)}"
),
lambda obj, value, address, name: value.CreateValueFromAddress(
name, address, value.type
),
lambda obj, value, address, name: value.CreateValueFromExpression(
name, f"(int8_t *){_hex(address)}"
).deref,
lambda obj, value, address, name: value.CreateValueFromExpression(
name, f"(int16_t *){_hex(address)}"
).deref,
lambda obj, value, address, name: value.CreateValueFromExpression(
name, f"(int32_t *){_hex(address)}"
).deref,
lambda obj, value, address, name: value.CreateValueFromExpression(
name, f"(int64_t *){_hex(address)}"
).deref,
lambda obj, value, address, name: value.CreateValueFromExpression(
name, f"(float *){_hex(address)}"
).deref,
lambda obj, value, address, name: value.CreateValueFromExpression(
name, f"(double *){_hex(address)}"
).deref,
lambda obj, value, address, name: value.CreateValueFromExpression(
name, f"(void **){_hex(address)}"
),
lambda obj, value, address, name: value.CreateValueFromExpression(
name, f"(bool *){_hex(address)}"
).deref,
lambda obj, value, address, name: None,
]
_TYPES = [
lambda x: x.GetType().GetBasicType(lldb.eBasicTypeVoid).GetPointerType(),
lambda x: x.GetType(),
lambda x: x.GetType().GetBasicType(lldb.eBasicTypeChar),
lambda x: x.GetType().GetBasicType(lldb.eBasicTypeShort),
lambda x: x.GetType().GetBasicType(lldb.eBasicTypeInt),
lambda x: x.GetType().GetBasicType(lldb.eBasicTypeLongLong),
lambda x: x.GetType().GetBasicType(lldb.eBasicTypeFloat),
lambda x: x.GetType().GetBasicType(lldb.eBasicTypeDouble),
lambda x: x.GetType().GetBasicType(lldb.eBasicTypeVoid).GetPointerType(),
lambda x: x.GetType().GetBasicType(lldb.eBasicTypeBool),
]
def _read_string(addr, size):
error = lldb.SBError()
s = (
lldb.debugger.GetSelectedTarget()
.GetProcess()
.ReadCStringFromMemory(int(addr), int(size), error)
)
if not error.Success():
raise DebuggerException()
return s
def _render_object(addr):
buff_addr = _evaluate("(void *)Konan_DebugBuffer()").unsigned
buff_len = _evaluate(
(
f"(int)Konan_DebugObjectToUtf8Array("
f"{_hex(addr)}, "
f"(void *){_hex(buff_addr)}, "
f"(int)Konan_DebugBufferSize()"
f");"
)
).signed
return _read_string(buff_addr, buff_len)
def kotlin_object_type_summary(lldb_val, _):
"""
Hook that is run by lldb to display a Kotlin object.
"""
logging.debug("%s: %s", _hex(lldb_val.unsigned), lldb_val.type.name)
if lldb_val.GetTypeName() != "ObjHeader *":
if lldb_val.GetValue() is None:
return _NULL
return lldb_val.value
if lldb_val.unsigned == 0:
return _NULL
return _render_object(lldb_val.unsigned)
def _select_provider(lldb_val, tip, internal_dict):
start = time.monotonic()
value_str = f"{_hex(lldb_val.unsigned)}"
logging.debug("%s name:%s tip:%s", value_str, lldb_val.name, _hex(tip))
soa = _is_string_or_array(lldb_val)
logging.debug("%s soa: %s", value_str, soa)
ret = (
_FACTORY["string"](lldb_val, tip, internal_dict)
if soa == 1
else (
_FACTORY["array"](lldb_val, tip, internal_dict)
if soa == 2
else _FACTORY["object"](lldb_val, tip, internal_dict)
)
)
logging.debug("%s = %s", value_str, ret)
_bench(start, lambda: f"select_provider({value_str})")
return ret
# noinspection PyUnresolvedReferences
class KonanHelperProvider(lldb.SBSyntheticValueProvider):
def __init__(self, valobj, am_string, type_name, _):
super().__init__(valobj)
self._log = logging.getLogger(self.__class__.__name__)
self._target = lldb.debugger.GetSelectedTarget()
self._process = self._target.GetProcess()
self._valobj = valobj
self._type_name = type_name
if am_string:
return
if self._children_count == 0:
value_str = f"{_hex(self._valobj.unsigned)}"
children_count = _evaluate(
f"(int)Konan_DebugGetFieldCount({value_str})"
).signed
self._log.debug(
"(int)[%s].Konan_DebugGetFieldCount(%s) = %s",
self._valobj.name,
value_str,
children_count,
)
self._children_count = children_count
def _read_string(self, expr, error):
return self._process.ReadCStringFromMemory(
_evaluate(expr).unsigned, 0x1000, error
)
def _read_value(self, index):
value_type = self._field_type(index)
address = self._field_address(index)
self._log.debug(
"[%s, type:%s, address:%s]", index, value_type, _hex(address)
)
return _TYPE_CONVERSION[int(value_type)](
self, self._valobj, address, str(self._field_name(index))
)
def _read_type(self, index):
obj_type = _TYPES[self._field_type(index)](self._valobj)
child = self._valobj.unsigned + self._children[index].offset()
self._log.debug(
"type:%s of %s of %s",
obj_type,
_hex(self._valobj.unsigned),
_hex(child),
)
return obj_type
def _field_address(self, index):
return _evaluate(
(
f"(void *)Konan_DebugGetFieldAddress("
f"{_hex(self._valobj.unsigned)}, {index}"
f")"
)
).unsigned
def _field_type(self, index):
return _evaluate(
(
f"(int)Konan_DebugGetFieldType("
f"{_hex(self._valobj.unsigned)}, {index}"
f")"
)
).unsigned
def _render_string(self, representation):
writer = io.StringIO()
max_children_count = _max_children_count()
limit = min(self._children_count, max_children_count)
for i in range(limit):
writer.write(representation(i))
if i != limit - 1:
writer.write(", ")
if max_children_count < self._children_count:
writer.write(", ...")
return f"[{writer.getvalue()}]"
# noinspection PyUnresolvedReferences
class KonanStringSyntheticProvider(KonanHelperProvider):
def __init__(self, valobj):
self._log = logging.getLogger(self.__class__.__name__)
self._log.debug("%s name:%s", _hex(valobj.unsigned), valobj.name)
self._children_count = 0
super(KonanStringSyntheticProvider, self).__init__(
valobj, True, "StringProvider", {}
)
fallback = valobj.GetValue()
buff_addr = _evaluate("(void *)Konan_DebugBuffer()").unsigned
buff_len = _evaluate(
(
f"(int)Konan_DebugObjectToUtf8Array("
f"{_hex(self._valobj.unsigned)}, "
f"(void *){_hex(buff_addr)}, "
f"(int)Konan_DebugBufferSize()"
f");"
)
).signed
if not buff_len:
self._representation = fallback
return
error = lldb.SBError()
s = self._process.ReadCStringFromMemory(
int(buff_addr), int(buff_len), error
)
if not error.Success():
raise DebuggerException()
self._representation = s if error.Success() else fallback
self._logger = lldb.formatters.Logger.Logger()
def update(self):
pass
def num_children(self):
return 0
def has_children(self):
return False
def get_child_index(self, _):
return None
def get_child_at_index(self, _):
return None
class KonanObjectSyntheticProvider(KonanHelperProvider):
def __init__(self, valobj, _, internal_dict):
self._log = logging.getLogger(self.__class__.__name__)
self._log.debug(_hex(valobj.unsigned))
self._children_count = 0
super(KonanObjectSyntheticProvider, self).__init__(
valobj, False, "ObjectProvider", internal_dict
)
self._children = [
self._field_name(i) for i in range(self._children_count)
]
self._log.debug(
"%s _children: %s", _hex(self._valobj.unsigned), self._children
)
def _field_name(self, index):
self._log.debug("%s, %s", _hex(self._valobj.unsigned), index)
error = lldb.SBError()
name = self._read_string(
(
f"(char *)Konan_DebugGetFieldName("
f"{_hex(self._valobj.unsigned)}, (int){index}"
f")"
),
error,
)
if not error.Success():
raise DebuggerException()
logging.debug(
"KonanObjectSyntheticProvider (%s, %s) = %s",
_hex(self._valobj.unsigned),
index,
name,
)
return name
def num_children(self):
self._log.debug(
"%s = %s", _hex(self._valobj.unsigned), self._children_count
)
return self._children_count
def has_children(self):
self._log.debug(
"%s = %s",
_hex(self._valobj.unsigned),
self._children_count > 0,
)
return self._children_count > 0
def get_child_index(self, name):
value_str = _hex(self._valobj.unsigned)
self._log.debug("%s, %s", value_str, name)
index = self._children.index(name)
self._log.debug("%s index=%s", value_str, name)
return index
def get_child_at_index(self, index):
self._log.debug("%s, %s", _hex(self._valobj.unsigned), index)
return self._read_value(index)
class KonanArraySyntheticProvider(KonanHelperProvider):
def __init__(self, valobj, internal_dict):
self._log = logging.getLogger(self.__class__.__name__)
self._children_count = 0
super(KonanArraySyntheticProvider, self).__init__(
valobj, False, "ArrayProvider", internal_dict
)
self._log.debug("valobj: %s", _hex(valobj.unsigned))
if self._valobj is None:
return
valobj.SetSyntheticChildrenGenerated(True)
def num_children(self):
self._log.debug(
"(%s) = %s", _hex(self._valobj.unsigned), self._children_count
)
return self._children_count
def has_children(self):
self._log.debug(
"(%s) = %s",
_hex(self._valobj.unsigned),
self._children_count > 0,
)
return self._children_count > 0
def get_child_index(self, name):
self._log.debug("%s, %s", _hex(self._valobj.unsigned), name)
index = int(name)
return index if (0 <= index < self._children_count) else -1
def get_child_at_index(self, index):
self._log.debug("%s, %s", _hex(self._valobj.unsigned), index)
return self._read_value(index)
def _field_name(self, index):
self._log.debug("%s, %s", _hex(self._valobj.unsigned), index)
return str(index)
class KonanZerroSyntheticProvider(lldb.SBSyntheticValueProvider):
def __init__(self, valobj):
super().__init__(valobj)
self._log = logging.getLogger(self.__class__.__name__)
logging.debug(valobj.name)
def num_children(self):
self._log.debug("")
return 0
def has_children(self):
self._log.debug("")
return False
def get_child_index(self, name):
self._log.debug("")
return 0
def get_child_at_index(self, index):
self._log.debug("")
return None
def __getattr__(self, item):
pass
class KonanNullSyntheticProvider(KonanZerroSyntheticProvider):
def __init__(self, valobj):
super(KonanNullSyntheticProvider, self).__init__(valobj)
class KonanNotInitializedObjectSyntheticProvider(KonanZerroSyntheticProvider):
def __init__(self, valobj):
super(KonanNotInitializedObjectSyntheticProvider, self).__init__(
valobj
)
class KonanProxyTypeProvider:
def __init__(self, valobj, internal_dict):
self._log = logging.getLogger(self.__class__.__name__)
start = time.monotonic()
value_str = _hex(valobj.unsigned)
value_name = valobj.name
self._log.debug("%s, name: %s", value_str, value_name)
if valobj.unsigned == 0:
self._log.debug(
"%s, name: %s NULL syntectic %s",
value_str,
value_name,
valobj.IsValid(),
)
_bench(start, lambda: f"KonanProxyTypeProvider({value_str})")
self._proxy = KonanNullSyntheticProvider(valobj)
return
tip = _type_info(valobj)
if not tip:
self._log.debug(
"%s, name: %s NULL syntectic %s",
value_str,
value_name,
valobj.IsValid(),
)
_bench(start, lambda: f"KonanProxyTypeProvider({value_str})")
self._proxy = KonanNotInitializedObjectSyntheticProvider(valobj)
return
self._log.debug("%s tip: %s", value_str, _hex(tip))
self._proxy = _select_provider(valobj, tip, internal_dict)
_bench(start, lambda: f"KonanProxyTypeProvider({value_str})")
self._log.debug(
"%s _proxy: %s", value_str, self._proxy.__class__.__name__
)
def __getattr__(self, item):
return getattr(self._proxy, item)
def _get_runtime_type(variable):
type_name = _evaluate(
f"(char *)Konan_DebugGetTypeName({_hex(variable.unsigned)})"
).summary
return "" if type_name is None else type_name.strip('"')
def field_type_command(_, field_address, exe_ctx, result, internal_dict):
"""
Returns runtime type of foo.bar.baz field in the form of
'(foo.bar.baz <TYPE_NAME>)'. If requested field could not be traced,
then '<NO_FIELD_FOUND>' plug is used for type name.
"""
fields = field_address.split(".")
variable = exe_ctx.GetFrame().FindVariable(fields[0])
for field_name in fields[1:]:
if variable is not None:
provider = KonanProxyTypeProvider(variable, internal_dict)
field_index = provider.get_child_index(field_name)
variable = provider.get_child_at_index(field_index)
else:
break
desc = "<NO_FIELD_FOUND>"
if variable is not None:
rt = _get_runtime_type(variable)
if len(rt) > 0:
desc = rt
result.write(f"{desc}")
_KONAN_VARIABLE = re.compile("kvar:(.*)#internal")
_KONAN_VARIABLE_TYPE = re.compile("^kfun:<get-(.*)>\\(\\)(.*)$")
_TYPES_KONAN_TO_C = {
"kotlin.Byte": ("int8_t", lambda v: v.signed),
"kotlin.Short": ("short", lambda v: v.signed),
"kotlin.Int": ("int", lambda v: v.signed),
"kotlin.Long": ("long", lambda v: v.signed),
"kotlin.UByte": ("int8_t", lambda v: v.unsigned),
"kotlin.UShort": ("short", lambda v: v.unsigned),
"kotlin.UInt": ("int", lambda v: v.unsigned),
"kotlin.ULong": ("long", lambda v: v.unsigned),
"kotlin.Char": ("short", lambda v: v.signed),
"kotlin.Boolean": ("bool", lambda v: v.signed),
"kotlin.Float": ("float", lambda v: v.value),
"kotlin.Double": ("double", lambda v: v.value),
}
def type_by_address_command(debugger, command, result, _):
result.AppendMessage(f"DEBUG: {command}")
tokens = command.split()
target = debugger.GetSelectedTarget()
types = _type_info_by_address(tokens[0])
result.AppendMessage(f"DEBUG: {types}")
for t in types:
result.AppendMessage(
f"{t.name}: {_hex(t.GetStartAddress().GetLoadAddress(target))}"
)
def symbol_by_name_command(debugger, command, result):
target = debugger.GetSelectedTarget()
process = target.GetProcess()
thread = process.GetSelectedThread()
frame = thread.GetSelectedFrame()
tokens = command.split()
mask = re.compile(tokens[0])
symbols = list(
filter(lambda v: mask.match(v.name), frame.GetModule().symbols)
)
visited = list()
for symbol in symbols:
name = symbol.name
if name in visited:
continue
visited.append(name)
result.AppendMessage(
f"{name}: {symbol.GetStartAddress().GetLoadAddress(target)}"
)
def konan_globals_command(debugger, _, result, __):
target = debugger.GetSelectedTarget()
process = target.GetProcess()
thread = process.GetSelectedThread()
frame = thread.GetSelectedFrame()
konan_variable_symbols = list(
filter(
lambda v: _KONAN_VARIABLE.match(v.name),
frame.GetModule().symbols,
)
)
visited = list()
for symbol in konan_variable_symbols:
name = _KONAN_VARIABLE.search(symbol.name).group(1)
if name in visited:
continue
visited.append(name)
def match(v):
return re.match(f"^kfun:<get-{name}>\\(\\).*$", v.name)
getters = list(filter(match, frame.module.symbols))
if not getters:
result.AppendMessage(f"storage not found for name:{name}")
continue
getter_functions = frame.module.FindFunctions(getters[0].name)
if not getter_functions:
continue
address = (
getter_functions[0]
.function.GetStartAddress()
.GetLoadAddress(target)
)
t = _KONAN_VARIABLE_TYPE.search(getters[0].name).group(2)
(c_type, extractor) = (
_TYPES_KONAN_TO_C[t]
if t in _TYPES_KONAN_TO_C.keys()
else ("ObjHeader *", lambda v: kotlin_object_type_summary(v, {}))
)
value = _evaluate(f"(({c_type} (*)()){_hex(address)})()")
str_value = extractor(value)
result.AppendMessage(f"{t} {name}: {str_value}")
def _hex(value):
return f"0x{value:x}"
_LOGGING = False
def _init_logger():
formatter = logging.Formatter(
"%(levelname)s - %(name)s - %(funcName)s: %(message)s"
)
# Same as in LLDBFrontend
if os.getenv("GLOG_log_dir") is not None:
handler = logging.FileHandler(
filename=os.getenv("GLOG_log_dir", "") + "/konan_lldb.log"
)
handler.setFormatter(formatter)
logging.getLogger().addHandler(handler)
logging.getLogger().setLevel(logging.DEBUG)
if _LOGGING:
handler = logging.StreamHandler(stream=sys.stderr)
handler.setFormatter(formatter)
logging.getLogger().addHandler(handler)
logging.getLogger().setLevel(logging.DEBUG)
def __lldb_init_module(debugger, _):
_init_logger()
logging.debug("init start")
_FACTORY["object"] = lambda x, y, z: KonanObjectSyntheticProvider(x, y, z)
_FACTORY["array"] = lambda x, y, z: KonanArraySyntheticProvider(x, z)
_FACTORY["string"] = lambda x, y, _: KonanStringSyntheticProvider(x)
debugger.HandleCommand(
(
"type summary add "
"--no-value "
"--python-function konan_lldb.kotlin_object_type_summary "
"ObjHeader *"
"--category Kotlin"
)
)
debugger.HandleCommand(
(
"type synthetic add "
"--python-class konan_lldb.KonanProxyTypeProvider "
"ObjHeader *"
"--category Kotlin"
)
)
debugger.HandleCommand("type category enable Kotlin")
debugger.HandleCommand(
f"command script add -f {__name__}.field_type_command field_type"
)
debugger.HandleCommand(
(
f"command script add -f "
f"{__name__}.type_by_address_command type_by_address"
)
)
debugger.HandleCommand(
(
f"command script add -f "
f"{__name__}.symbol_by_name_command symbol_by_name"
)
)
# Avoid Kotlin/Native runtime
debugger.HandleCommand(
"settings set target.process.thread.step-avoid-regexp ^::Kotlin_"
)
logging.debug("init end")