Files

1632 lines
68 KiB
Python

#
# Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved.
# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
#
# This code is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 2 only, as
# published by the Free Software Foundation. Oracle designates this
# particular file as subject to the "Classpath" exception as provided
# by Oracle in the LICENSE file that accompanied this code.
#
# This code is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
# version 2 for more details (a copy is included in the LICENSE file that
# accompanied this code).
#
# You should have received a copy of the GNU General Public License version
# 2 along with this work; if not, write to the Free Software Foundation,
# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
# or visit www.oracle.com if you need additional information or have any
# questions.
#
from typing import Iterable
import sys
import os
import re
import gdb
import gdb.types
import gdb.printing
import gdb.unwinder
from gdb.FrameDecorator import FrameDecorator
if sys.version_info.major < 3:
pyversion = '.'.join(str(v) for v in sys.version_info[:3])
message = (
'Cannot load SubstrateVM debugging assistance for GDB from ' + os.path.basename(__file__)
+ ': it requires at least Python 3.x. You are running GDB with Python ' + pyversion
+ ' from ' + sys.executable + '.'
)
raise AssertionError(message)
if int(gdb.VERSION.split('.')[0]) < 14:
message = (
'Cannot load SubstrateVM debugging assistance for GDB from ' + os.path.basename(__file__)
+ ': it requires at least GDB 14.x. You are running GDB ' + gdb.VERSION + '.'
)
raise AssertionError(message)
# check for a symbol that exists only in native image debug info
if gdb.lookup_global_symbol("com.oracle.svm.core.Isolates") is None:
message = (
'Cannot load SubstrateVM debugging assistance without a loaded java native image or native shared library,'
+ 'the script requires java types for initialization.'
)
raise AssertionError(message)
def trace(msg: str) -> None:
if svm_debug_tracing.tracefile:
svm_debug_tracing.tracefile.write(f'trace: {msg}\n'.encode(encoding='utf-8', errors='strict'))
svm_debug_tracing.tracefile.flush()
def adr(obj: gdb.Value) -> int:
# use null as fallback if we cannot find the address value
adr_val = 0
if obj.type.code == gdb.TYPE_CODE_PTR:
if int(obj) != 0:
adr_val = int(obj.dereference().address)
elif obj.address is not None:
adr_val = int(obj.address)
return adr_val
def try_or_else(success, failure, *exceptions):
try:
return success()
except exceptions or Exception:
return failure() if callable(failure) else failure
class Function:
"""A more complete representation of gdb function symbols."""
def __init__(self, static: bool, name: str, gdb_sym: gdb.Symbol):
self.static = static
self.name = name
self.sym = gdb_sym
class SVMUtil:
pretty_printer_name = "SubstrateVM"
hub_field_name = "hub"
compressed_ref_prefix = '_z_.'
use_heap_base = try_or_else(lambda: bool(gdb.parse_and_eval('(int)__svm_use_heap_base')), True, gdb.error)
compression_shift = try_or_else(lambda: int(gdb.parse_and_eval('(int)__svm_compression_shift')), 0, gdb.error)
reserved_bits_mask = try_or_else(lambda: int(gdb.parse_and_eval('(int)__svm_reserved_bits_mask')), 0, gdb.error)
object_alignment = try_or_else(lambda: int(gdb.parse_and_eval('(int)__svm_object_alignment')), 0, gdb.error)
heap_base_regnum = try_or_else(lambda: int(gdb.parse_and_eval('(int)__svm_heap_base_regnum')), 0, gdb.error)
string_type = gdb.lookup_type("java.lang.String")
enum_type = gdb.lookup_type("java.lang.Enum")
object_type = gdb.lookup_type("java.lang.Object")
object_header_type = gdb.lookup_type("_objhdr")
hub_type = gdb.lookup_type("Encoded$Dynamic$Hub")
null = gdb.Value(0).cast(object_type.pointer())
classloader_type = gdb.lookup_type("java.lang.ClassLoader")
wrapper_types = [gdb.lookup_type(f'java.lang.{x}') for x in
["Byte", "Short", "Integer", "Long", "Float", "Double", "Boolean", "Character"]
if gdb.lookup_global_symbol(f'java.lang.{x}') is not None]
pretty_print_objfiles = set()
current_print_depth = 0
parents = dict()
selfref_cycles = set()
hlreps = dict()
deopt_stub_adr = 0
@classmethod
def get_heap_base(cls) -> gdb.Value:
try:
return gdb.selected_frame().read_register(cls.heap_base_regnum)
except gdb.error:
# no frame available
return cls.null
@classmethod
def is_null(cls, obj: gdb.Value) -> bool:
return adr(obj) == 0 or (cls.use_heap_base and adr(obj) == int(cls.get_heap_base()))
@classmethod
def get_uncompressed_type(cls, t: gdb.Type) -> gdb.Type:
# compressed types only exist for java type which are either struct or union
if t.code != gdb.TYPE_CODE_STRUCT and t.code != gdb.TYPE_CODE_UNION:
return t
result = cls.get_base_class(t) if (cls.is_compressed(t) and t != cls.hub_type) else t
trace(f'<SVMUtil> - get_uncompressed_type({t}) = {result}')
return result
@classmethod
def get_compressed_type(cls, t: gdb.Type) -> gdb.Type:
t = cls.get_basic_type(t)
# compressed types only exist for java types which are either struct or union
# do not compress types that already have the compressed prefix
if not cls.is_java_type(t) or cls.is_compressed(t):
return t
type_name = t.name
# java types only contain '::' if there is a classloader namespace
if '::' in type_name:
loader_namespace, _, type_name = type_name.partition('::')
type_name = loader_namespace + '::' + cls.compressed_ref_prefix + type_name
else:
type_name = cls.compressed_ref_prefix + type_name
trace(f'<SVMUtil> - get_compressed_type({t}) = {type_name}')
return gdb.lookup_type(type_name)
@classmethod
def get_compressed_oop(cls, obj: gdb.Value) -> int:
# use compressed ref if available - only compute it if necessary
if obj.type.code == gdb.TYPE_CODE_PTR and cls.is_compressed(obj.type):
return int(obj)
obj_adr = adr(obj)
if obj_adr == 0:
return obj_adr
# recreate compressed oop from the object address
# this reverses the uncompress expression from
# com.oracle.objectfile.elf.dwarf.DwarfInfoSectionImpl#writeIndirectOopConversionExpression
is_hub = cls.get_basic_type(obj.type) == cls.hub_type
compression_shift = cls.compression_shift
num_reserved_bits = int.bit_count(cls.reserved_bits_mask)
num_alignment_bits = int.bit_count(cls.object_alignment - 1)
compressed_oop = obj_adr
if cls.use_heap_base:
compressed_oop -= int(SVMUtil.get_heap_base())
assert compression_shift >= 0
compressed_oop = compressed_oop >> compression_shift
if is_hub and num_reserved_bits != 0:
assert compression_shift >= 0
compressed_oop = compressed_oop << compression_shift
assert num_alignment_bits >= 0
compressed_oop = compressed_oop >> num_alignment_bits
assert num_reserved_bits >= 0
compressed_oop = compressed_oop << num_reserved_bits
return compressed_oop
@classmethod
def get_unqualified_type_name(cls, qualified_type_name: str) -> str:
result = qualified_type_name.split('.')[-1]
result = result.split('$')[-1]
trace(f'<SVMUtil> - get_unqualified_type_name({qualified_type_name}) = {result}')
return result
@classmethod
def is_compressed(cls, t: gdb.Type) -> bool:
# for the hub type we always want handle it as compressed as there is no clear distinction in debug info for
# the hub field, and it may always have an expression in the type's data_location attribute
if cls.get_basic_type(t) == cls.hub_type:
return True
type_name = cls.get_basic_type(t).name
if type_name is None:
# fallback to the GDB type printer for t
type_name = str(t)
# compressed types from a different classLoader have the format <loader_name>::_z_.<type_name>
result = type_name.startswith(cls.compressed_ref_prefix) or ('::' + cls.compressed_ref_prefix) in type_name
trace(f'<SVMUtil> - is_compressed({type_name}) = {result}')
return result
@classmethod
def adr_str(cls, obj: gdb.Value) -> str:
if not svm_print_address.absolute_adr and cls.is_compressed(obj.type):
result = f' @z({hex(cls.get_compressed_oop(obj))})'
else:
result = f' @({hex(adr(obj))})'
trace(f'<SVMUtil> - adr_str({hex(adr(obj))}) = {result}')
return result
@classmethod
def prompt_hook(cls, current_prompt: str = None):
cls.current_print_depth = 0
cls.parents.clear()
cls.selfref_cycles.clear()
SVMCommandPrint.cache.clear()
@classmethod
def is_selfref(cls, obj: gdb.Value) -> bool:
result = (svm_check_selfref.value and
not cls.is_primitive(obj.type) and
adr(obj) in cls.selfref_cycles)
trace(f'<SVMUtil> - is_selfref({hex(adr(obj))}) = {result}')
return result
@classmethod
def add_selfref(cls, parent: gdb.Value, child: gdb.Value) -> gdb.Value:
# filter out null references and primitives
if (child.type.code == gdb.TYPE_CODE_PTR and cls.is_null(child)) or cls.is_primitive(child.type):
return child
child_adr = adr(child)
parent_adr = adr(parent)
trace(f'<SVMUtil> - add_selfref(parent={hex(parent_adr)}, child={hex(child_adr)})')
if svm_check_selfref.value and cls.is_reachable(child_adr, parent_adr):
trace(f' <add selfref {child_adr}>')
cls.selfref_cycles.add(child_adr)
else:
trace(f' <add {hex(child_adr)} --> {hex(parent_adr)}>')
if child_adr in cls.parents:
cls.parents[child_adr].append(parent_adr)
else:
cls.parents[child_adr] = [parent_adr]
return child
@classmethod
# checks if node this is reachable from node other (this node is parent of other node)
def is_reachable(cls, this: hex, other: hex) -> bool:
test_nodes = [other]
trace(f'<SVMUtil> - is_reachable(this={this}, other={other}')
while True:
if len(test_nodes) == 0:
return False
if any(this == node for node in test_nodes):
return True
# create a flat list of all ancestors of each tested node
test_nodes = [parent for node in test_nodes for parent in cls.parents.get(node, [])]
@classmethod
def get_java_string(cls, obj: gdb.Value, gdb_output_string: bool = False) -> str:
if cls.is_null(obj):
return ""
trace(f'<SVMUtil> - get_java_string({hex(adr(obj))})')
coder = cls.get_int_field(obj, 'coder', None)
if coder is None:
codec = 'utf-16' # Java 8 has a char[] with utf-16
bytes_per_char = 2
else:
trace(f'<SVMUtil> - get_java_string: coder = {coder}')
# From Java 9 on, value is byte[] with latin_1 or utf-16_le
codec = {
0: 'latin_1',
1: 'utf-16_le',
}.get(coder)
bytes_per_char = 1
value = cls.get_obj_field(obj, 'value')
if cls.is_null(value):
return ""
value_content = cls.get_obj_field(value, 'data')
value_length = cls.get_int_field(value, 'len')
if cls.is_null(value_content) or value_length == 0:
return ""
string_data = bytearray()
for index in range(min(svm_print_string_limit.value,
value_length) if gdb_output_string and svm_print_string_limit.value >= 0 else value_length):
mask = (1 << 8 * bytes_per_char) - 1
code_unit = int(value_content[index] & mask)
code_unit_as_bytes = code_unit.to_bytes(bytes_per_char, byteorder='little')
string_data.extend(code_unit_as_bytes)
result = string_data.decode(codec).replace("\x00", r"\0")
if gdb_output_string and 0 < svm_print_string_limit.value < value_length:
result += "..."
trace(f'<SVMUtil> - get_java_string({hex(adr(obj))}) = {result}')
return result
@classmethod
def get_obj_field(cls, obj: gdb.Value, field_name: str, default: gdb.Value = null) -> gdb.Value:
try:
return obj[field_name]
except gdb.error:
return default
@classmethod
def get_int_field(cls, obj: gdb.Value, field_name: str, default: int = 0) -> int:
field = cls.get_obj_field(obj, field_name)
try:
return int(field)
except (gdb.error, TypeError): # TypeError if field is None already
return default
@classmethod
def get_classloader_namespace(cls, obj: gdb.Value) -> str:
try:
hub = cls.get_obj_field(obj, cls.hub_field_name)
if cls.is_null(hub):
return ""
hub_companion = cls.get_obj_field(hub, 'companion')
if cls.is_null(hub_companion):
return ""
loader = cls.get_obj_field(hub_companion, 'classLoader')
if cls.is_null(loader):
return ""
loader = cls.cast_to(loader, cls.classloader_type)
loader_name = cls.get_obj_field(loader, 'nameAndId')
if cls.is_null(loader_name):
return ""
loader_namespace = cls.get_java_string(loader_name)
trace(f'<SVMUtil> - get_classloader_namespace loader_namespace: {loader_namespace}')
# replicate steps in 'com.oracle.svm.hosted.image.NativeImageBFDNameProvider::uniqueShortLoaderName'
# for recreating the loader name stored in the DWARF debuginfo
loader_namespace = cls.get_unqualified_type_name(loader_namespace)
loader_namespace = loader_namespace.replace(' @', '_').replace("'", '').replace('"', '')
return loader_namespace
except gdb.error:
pass # ignore gdb errors here and try to continue with no classLoader
return ""
@classmethod
def get_rtt(cls, obj: gdb.Value) -> gdb.Type:
static_type = cls.get_basic_type(obj.type)
if static_type == cls.hub_type:
return cls.hub_type
# check for interfaces and cast them to Object to make the hub accessible
if cls.get_uncompressed_type(cls.get_basic_type(obj.type)).code == gdb.TYPE_CODE_UNION:
obj = cls.cast_to(obj, cls.object_type)
hub = cls.get_obj_field(obj, cls.hub_field_name)
if cls.is_null(hub):
return static_type
name_field = cls.get_obj_field(hub, 'name')
if cls.is_null(name_field):
return static_type
rtt_name = cls.get_java_string(name_field)
if rtt_name.startswith('['):
array_dimension = rtt_name.count('[')
if array_dimension > 0:
rtt_name = rtt_name[array_dimension:]
if rtt_name[0] == 'L':
classname_end = rtt_name.find(';')
rtt_name = rtt_name[1:classname_end]
else:
rtt_name = {
'Z': 'boolean',
'B': 'byte',
'C': 'char',
'D': 'double',
'F': 'float',
'I': 'int',
'J': 'long',
'S': 'short',
}.get(rtt_name, rtt_name)
for _ in range(array_dimension):
rtt_name += '[]'
loader_namespace = cls.get_classloader_namespace(obj)
if loader_namespace != "":
try:
# try to apply loader namespace
rtt = gdb.lookup_type(loader_namespace + '::' + rtt_name)
except gdb.error:
rtt = gdb.lookup_type(rtt_name) # found a loader namespace that is ignored (e.g. 'app')
else:
rtt = gdb.lookup_type(rtt_name)
if cls.is_compressed(obj.type) and not cls.is_compressed(rtt):
rtt = cls.get_compressed_type(rtt)
trace(f'<SVMUtil> - get_rtt({hex(adr(obj))}) = {rtt_name}')
return rtt
@classmethod
def cast_to(cls, obj: gdb.Value, t: gdb.Type) -> gdb.Value:
if t is None:
return obj
# get objects address, take care of compressed oops
if cls.is_compressed(t):
obj_oop = cls.get_compressed_oop(obj)
else:
obj_oop = adr(obj)
trace(f'<SVMUtil> - cast_to({hex(adr(obj))}, {t})')
if t.code != gdb.TYPE_CODE_PTR:
t = t.pointer()
trace(f'<SVMUtil> - cast_to({hex(adr(obj))}, {t}) returned')
# just use the raw pointer value and cast it instead the obj
# casting the obj directly results in issues with compressed oops
return obj if t == obj.type else gdb.Value(obj_oop).cast(t)
@classmethod
def get_symbol_adr(cls, symbol: str) -> int:
trace(f'<SVMUtil> - get_symbol_adr({symbol})')
return gdb.parse_and_eval(symbol).address
@classmethod
def execout(cls, cmd: str) -> str:
trace(f'<SVMUtil> - execout({cmd})')
return gdb.execute(cmd, False, True)
@classmethod
def get_basic_type(cls, t: gdb.Type) -> gdb.Type:
trace(f'<SVMUtil> - get_basic_type({t})')
while t.code == gdb.TYPE_CODE_PTR:
t = t.target()
return t
@classmethod
def is_primitive(cls, t: gdb.Type) -> bool:
result = cls.get_basic_type(t).is_scalar
trace(f'<SVMUtil> - is_primitive({t}) = {result}')
return result
@classmethod
def is_primitive_wrapper(cls, t: gdb.Type) -> bool:
result = t in cls.wrapper_types
trace(f'<SVMUtil> - is_primitive_wrapper({t}) = {result}')
return result
@classmethod
def is_enum_type(cls, t: gdb.Type) -> bool:
return cls.get_base_class(t) == cls.enum_type
@classmethod
def get_base_class(cls, t: gdb.Type) -> gdb.Type:
return t if t == cls.object_type else \
next((f.type for f in t.fields() if f.is_base_class), cls.object_type)
@classmethod
def find_shared_types(cls, type_list: list, t: gdb.Type) -> list: # list[gdb.Type]
if len(type_list) == 0:
while t != cls.object_type:
type_list += [t]
t = cls.get_base_class(t)
return type_list
else:
while t != cls.object_type:
if t in type_list:
return type_list[type_list.index(t):]
t = cls.get_base_class(t)
return [cls.object_type]
@classmethod
def get_all_fields(cls, t: gdb.Type, include_static: bool) -> list: # list[gdb.Field]:
t = cls.get_basic_type(t)
while t.code == gdb.TYPE_CODE_TYPEDEF:
t = t.target()
t = cls.get_basic_type(t)
if t.code != gdb.TYPE_CODE_STRUCT and t.code != gdb.TYPE_CODE_UNION:
return []
for f in t.fields():
if not include_static:
try:
f.bitpos # bitpos attribute is not available for static fields
except AttributeError: # use bitpos access exception to skip static fields
continue
if f.is_base_class:
yield from cls.get_all_fields(f.type, include_static)
else:
yield f
@classmethod
def get_all_member_functions(cls, t: gdb.Type, include_static: bool, include_constructor: bool) -> set: # set[Function]:
syms = set()
try:
basic_type = cls.get_basic_type(t)
type_name = basic_type.name
members = SVMUtil.execout(f"ptype '{type_name}'")
for member in members.split('\n'):
parts = member.strip().split(' ')
is_static = parts[0] == 'static'
if not include_static and is_static:
continue
for part in parts:
if '(' in part:
func_name = part[:part.find('(')]
if include_constructor or func_name != cls.get_unqualified_type_name(type_name):
sym = gdb.lookup_global_symbol(f"{type_name}::{func_name}")
# check if symbol exists and is a function
if sym is not None and sym.type.code == gdb.TYPE_CODE_FUNC:
syms.add(Function(is_static, func_name, sym))
break
for f in basic_type.fields():
if f.is_base_class:
syms = syms.union(cls.get_all_member_functions(f.type, include_static, include_constructor))
except Exception as ex:
trace(f'<SVMUtil> - get_all_member_function_names({t}) exception: {ex}')
return syms
@classmethod
def is_java_type(cls, t: gdb.Type) -> bool:
t = cls.get_uncompressed_type(cls.get_basic_type(t))
# Check for existing ".class" symbol (which exists for every java type in a native image)
# a java class is represented by a struct, interfaces are represented by a union
# only structs contain a "hub" field, thus just checking for a hub field does not work for interfaces
result = ((t.code == gdb.TYPE_CODE_UNION and gdb.lookup_global_symbol(t.name + '.class', gdb.SYMBOL_VAR_DOMAIN) is not None) or
(t.code == gdb.TYPE_CODE_STRUCT and gdb.types.has_field(t, SVMUtil.hub_field_name)))
trace(f'<SVMUtil> - is_java_obj({t}) = {result}')
return result
class SVMPPString:
def __init__(self, obj: gdb.Value, java: bool = True):
trace(f'<SVMPPString> - __init__({hex(adr(obj))})')
self.__obj = obj
self.__java = java
def to_string(self) -> str:
trace('<SVMPPString> - to_string')
if self.__java:
try:
value = '"' + SVMUtil.get_java_string(self.__obj, True) + '"'
except gdb.error:
return SVMPPConst(None)
else:
value = str(self.__obj)
value = value[value.index('"'):]
if svm_print_address.with_adr:
value += SVMUtil.adr_str(self.__obj)
trace(f'<SVMPPString> - to_string = {value}')
return value
class SVMPPArray:
def __init__(self, obj: gdb.Value, java_array: bool = True):
trace(f'<SVMPPArray> - __init__(obj={obj.type} @ {hex(adr(obj))}, java_array={java_array})')
if java_array:
self.__length = SVMUtil.get_int_field(obj, 'len')
self.__array = SVMUtil.get_obj_field(obj, 'data', None)
if SVMUtil.is_null(self.__array):
self.__array = None
else:
self.__length = obj.type.range()[-1] + 1
self.__array = obj
self.__obj = obj
self.__java_array = java_array
self.__skip_children = SVMUtil.is_selfref(obj) or 0 <= svm_print_depth_limit.value <= SVMUtil.current_print_depth
if not self.__skip_children:
SVMUtil.current_print_depth += 1
def display_hint(self) -> str:
trace('<SVMPPArray> - display_hint = array')
return 'array'
def to_string(self) -> str:
trace('<SVMPPArray> - to_string')
if self.__java_array:
rtt = SVMUtil.get_rtt(self.__obj)
value = str(SVMUtil.get_uncompressed_type(rtt))
value = value.replace('[]', f'[{self.__length}]')
else:
value = str(self.__obj.type)
if self.__skip_children:
value += ' = {...}'
if svm_print_address.with_adr:
value += SVMUtil.adr_str(self.__obj)
trace(f'<SVMPPArray> - to_string = {value}')
return value
def __iter__(self):
trace('<SVMPPArray> - __iter__')
if self.__array is not None:
for i in range(self.__length):
yield self.__array[i]
def children(self) -> Iterable[object]:
trace('<SVMPPArray> - children')
if self.__skip_children:
return
for index, elem in enumerate(self):
# apply custom limit only for java arrays
if self.__java_array and 0 <= svm_print_element_limit.value <= index:
yield str(index), '...'
return
trace(f'<SVMPPArray> - children[{index}]')
yield str(index), SVMUtil.add_selfref(self.__obj, elem)
SVMUtil.current_print_depth -= 1
class SVMPPClass:
def __init__(self, obj: gdb.Value, java_class: bool = True):
trace(f'<SVMPPClass> - __init__({obj.type} @ {hex(adr(obj))})')
self.__obj = obj
self.__java_class = java_class
self.__skip_children = SVMUtil.is_selfref(obj) or 0 <= svm_print_depth_limit.value <= SVMUtil.current_print_depth
if not self.__skip_children:
SVMUtil.current_print_depth += 1
def __getitem__(self, key: str) -> gdb.Value:
trace(f'<SVMPPClass> - __getitem__({key})')
item = SVMUtil.get_obj_field(self.__obj, key, None)
if item is None:
return None
pp_item = gdb.default_visualizer(item)
return item if pp_item is None else pp_item
def to_string(self) -> str:
trace('<SVMPPClass> - to_string')
try:
if self.__java_class:
rtt = SVMUtil.get_rtt(self.__obj)
result = SVMUtil.get_uncompressed_type(rtt).name
else:
result = "object" if self.__obj.type.name is None else self.__obj.type.name
if self.__skip_children:
result += ' = {...}'
if svm_print_address.with_adr:
result += SVMUtil.adr_str(self.__obj)
trace(f'<SVMPPClass> - to_string = {result}')
return result
except gdb.error as ex:
trace(f"<SVMPPClass> - to_string error - SVMPPClass: {ex}")
return 'object'
def children(self) -> Iterable[object]:
trace('<SVMPPClass> - children (class field iterator)')
if self.__skip_children:
return
# hide fields from the object header
fields = [str(f.name) for f in SVMUtil.get_all_fields(self.__obj.type, svm_print_static_fields.value) if f.parent_type != SVMUtil.object_header_type]
for index, f in enumerate(fields):
trace(f'<SVMPPClass> - children: field "{f}"')
# apply custom limit only for java objects
if self.__java_class and 0 <= svm_print_field_limit.value <= index:
yield f, '...'
return
yield f, SVMUtil.add_selfref(self.__obj, self.__obj[f])
SVMUtil.current_print_depth -= 1
class SVMPPEnum:
def __init__(self, obj: gdb.Value):
trace(f'<SVMPPEnum> - __init__({hex(adr(obj))})')
self.__obj = obj
self.__name = SVMUtil.get_obj_field(self.__obj, 'name', "")
self.__ordinal = SVMUtil.get_int_field(self.__obj, 'ordinal', None)
def to_string(self) -> str:
result = SVMUtil.get_java_string(self.__name) + f"({self.__ordinal})"
if svm_print_address.with_adr:
result += SVMUtil.adr_str(self.__obj)
trace(f'<SVMPPEnum> - to_string = {result}')
return result
class SVMPPBoxedPrimitive:
def __init__(self, obj: gdb.Value):
trace(f'<SVMPPBoxedPrimitive> - __init__({obj.type} @ {hex(adr(obj))})')
self.__obj = obj
self.__value = SVMUtil.get_obj_field(self.__obj, 'value', obj.type.name)
def to_string(self) -> str:
result = str(self.__value)
if svm_print_address.with_adr:
result += SVMUtil.adr_str(self.__obj)
trace(f'<SVMPPBoxedPrimitive> - to_string = {result}')
return result
class SVMPPConst:
def __init__(self, val: str):
trace('<SVMPPConst> - __init__')
self.__val = val
def to_string(self) -> str:
result = "null" if self.__val is None else self.__val
trace(f'<SVMPPConst> - to_string = {result}')
return result
class SVMPrettyPrinter(gdb.printing.PrettyPrinter):
def __init__(self):
super().__init__(SVMUtil.pretty_printer_name)
def __call__(self, obj: gdb.Value):
trace(f'<SVMPrettyPrinter> - __call__({obj.type} @ {hex(adr(obj))})')
if not SVMUtil.is_primitive(obj.type) and SVMUtil.is_java_type(obj.type):
# Filter out references to the null literal
if SVMUtil.is_null(obj):
return SVMPPConst(None)
rtt = SVMUtil.get_rtt(obj)
uncompressed_rtt = SVMUtil.get_uncompressed_type(rtt)
obj = SVMUtil.cast_to(obj, rtt)
# filter for primitive wrappers
if SVMUtil.is_primitive_wrapper(uncompressed_rtt):
return SVMPPBoxedPrimitive(obj)
# filter for strings
if uncompressed_rtt == SVMUtil.string_type:
return SVMPPString(obj)
# filter for arrays
if uncompressed_rtt.name.endswith("[]"):
return SVMPPArray(obj)
# filter for enum values
if SVMUtil.is_enum_type(uncompressed_rtt):
return SVMPPEnum(obj)
# Any other Class ...
if svm_use_hlrep.value:
pp = make_high_level_object(obj, uncompressed_rtt.name)
else:
pp = SVMPPClass(obj)
return pp
# no complex java type -> handle foreign types for selfref checks
elif obj.type.code == gdb.TYPE_CODE_PTR and obj.type.target().code != gdb.TYPE_CODE_VOID:
# Filter out references to the null literal
if SVMUtil.is_null(obj):
return SVMPPConst(None)
return self.__call__(obj.dereference())
elif obj.type.code == gdb.TYPE_CODE_ARRAY:
return SVMPPArray(obj, False)
elif obj.type.code == gdb.TYPE_CODE_TYPEDEF:
# try to expand foreign c structs
try:
obj = obj.dereference()
return self.__call__(obj)
except gdb.error as err:
return None
elif obj.type.code == gdb.TYPE_CODE_STRUCT:
return SVMPPClass(obj, False)
elif SVMUtil.is_primitive(obj.type):
if obj.type.name == "char" and obj.type.sizeof == 2:
return SVMPPConst(repr(chr(obj)))
elif obj.type.name == "byte":
return SVMPPConst(str(int(obj)))
else:
return None
else:
return None
def HLRep(original_class):
try:
SVMUtil.hlreps[original_class.target_type] = original_class
except Exception as ex:
trace(f'<@HLRep registration exception: {ex}>')
return original_class
@HLRep
class ArrayList:
target_type = 'java.util.ArrayList'
def __init__(self, obj: gdb.Value):
trace(f'<ArrayList> - __init__({obj.type} @ {hex(adr(obj))})')
self.__size = SVMUtil.get_int_field(obj, 'size')
element_data = SVMUtil.get_obj_field(obj, 'elementData')
if SVMUtil.is_null(element_data):
self.__data = None
else:
self.__data = SVMUtil.get_obj_field(element_data, 'data', None)
if self.__data is not None and SVMUtil.is_null(self.__data):
self.__data = None
self.__obj = obj
self.__skip_children = SVMUtil.is_selfref(obj) or 0 <= svm_print_depth_limit.value <= SVMUtil.current_print_depth
if not self.__skip_children:
SVMUtil.current_print_depth += 1
def to_string(self) -> str:
trace('<ArrayList> - to_string')
res = 'java.util.ArrayList'
if svm_infer_generics.value != 0:
elem_type = self.infer_generic_types()
if elem_type is not None:
res += f'<{elem_type}>'
res += f'({self.__size})'
if self.__skip_children:
res += ' = {...}'
if svm_print_address.with_adr:
res += SVMUtil.adr_str(self.__obj)
trace(f'<ArrayList> - to_string = {res}')
return res
def infer_generic_types(self) -> str:
elem_type: list = [] # list[gdb.Type]
for i, elem in enumerate(self, 1):
if not SVMUtil.is_null(elem): # check for null values
elem_type = SVMUtil.find_shared_types(elem_type, SVMUtil.get_rtt(elem))
if (len(elem_type) > 0 and elem_type[0] == SVMUtil.object_type) or (0 <= svm_infer_generics.value <= i):
break
return None if len(elem_type) == 0 else SVMUtil.get_unqualified_type_name(elem_type[0].name)
def display_hint(self) -> str:
trace('<ArrayList> - display_hint = array')
return 'array'
def __iter__(self) -> gdb.Value:
trace('<ArrayList> - __iter__')
if self.__data is not None:
for i in range(self.__size):
yield self.__data[i]
def children(self) -> Iterable[object]:
trace(f'<ArrayList> - children({self.__obj.type} @ {hex(adr(self.__obj))})')
if self.__skip_children:
return
for index, elem in enumerate(self):
if 0 <= svm_print_element_limit.value <= index:
yield str(index), '...'
return
trace(f'<ArrayList> - children({self.__obj.type} @ {hex(adr(self.__obj))})[{index}]')
yield str(index), SVMUtil.add_selfref(self.__obj, elem)
SVMUtil.current_print_depth -= 1
@HLRep
class HashMap:
target_type = 'java.util.HashMap'
def __init__(self, obj: gdb.Value):
trace(f'<HashMap> - __init__({obj.type} @ {hex(adr(obj))})')
self.__size = SVMUtil.get_int_field(obj, 'size')
table = SVMUtil.get_obj_field(obj, 'table')
if SVMUtil.is_null(table):
self.__data = None
self.__table_len = 0
else:
self.__data = SVMUtil.get_obj_field(table, 'data', None)
if self.__data is not None and SVMUtil.is_null(self.__data):
self.__data = None
self.__table_len = SVMUtil.get_int_field(table, 'len')
self.__obj = obj
self.__skip_children = SVMUtil.is_selfref(obj) or 0 <= svm_print_depth_limit.value <= SVMUtil.current_print_depth
if not self.__skip_children:
SVMUtil.current_print_depth += 1
def to_string(self) -> str:
trace('<HashMap> - to_string')
res = 'java.util.HashMap'
if svm_infer_generics.value != 0:
key_type, value_type = self.infer_generic_types()
res += f"<{key_type}, {value_type}>"
res += f'({self.__size})'
if self.__skip_children:
res += ' = {...}'
if svm_print_address.with_adr:
res += SVMUtil.adr_str(self.__obj)
trace(f'<HashMap> - to_string = {res}')
return res
def infer_generic_types(self) -> tuple: # (str, str):
key_type: list = [] # list[gdb.Type]
value_type: list = [] # list[gdb.Type]
for i, kv in enumerate(self, 1):
key, value = kv
# if len(*_type) = 1 we could just infer the type java.lang.Object, ignore null values
if not SVMUtil.is_null(key) and (len(key_type) == 0 or key_type[0] != SVMUtil.object_type):
key_type = SVMUtil.find_shared_types(key_type, SVMUtil.get_rtt(key))
if not SVMUtil.is_null(value) and (len(value_type) == 0 or value_type[0] != SVMUtil.object_type):
value_type = SVMUtil.find_shared_types(value_type, SVMUtil.get_rtt(value))
if (0 <= svm_infer_generics.value <= i) or (len(key_type) > 0 and key_type[0] == SVMUtil.object_type and
len(value_type) > 0 and value_type[0] == SVMUtil.object_type):
break
key_type_name = '?' if len(key_type) == 0 else SVMUtil.get_unqualified_type_name(key_type[0].name)
value_type_name = '?' if len(value_type) == 0 else SVMUtil.get_unqualified_type_name(value_type[0].name)
return key_type_name, value_type_name
def display_hint(self) -> str:
trace('<HashMap> - display_hint = map')
return "map"
def __iter__(self) -> tuple: # (gdb.Value, gdb.Value):
trace('<HashMap> - __iter__')
for i in range(self.__table_len):
obj = self.__data[i]
while not SVMUtil.is_null(obj):
key = SVMUtil.get_obj_field(obj, 'key')
value = SVMUtil.get_obj_field(obj, 'value')
yield key, value
obj = SVMUtil.get_obj_field(obj, 'next')
def children(self) -> Iterable[object]:
trace(f'<HashMap> - children({self.__obj.type} @ {hex(adr(self.__obj))})')
if self.__skip_children:
return
for index, (key, value) in enumerate(self):
if 0 <= svm_print_element_limit.value <= index:
yield str(index), '...'
return
trace(f'<HashMap> - children({self.__obj.type} @ {hex(adr(self.__obj))})[{index}]')
yield f"key{index}", SVMUtil.add_selfref(self.__obj, key)
yield f"value{index}", SVMUtil.add_selfref(self.__obj, value)
SVMUtil.current_print_depth -= 1
def make_high_level_object(obj: gdb.Value, rtt_name: str) -> gdb.Value:
try:
trace(f'try makeHighLevelObject for {rtt_name}')
hl_rep_class = SVMUtil.hlreps[rtt_name]
return hl_rep_class(obj)
except Exception as ex:
trace(f'<makeHighLevelObject> exception: {ex}')
return SVMPPClass(obj)
class SVMPrintParam(gdb.Parameter):
"""Use this command to enable/disable SVM pretty printing."""
set_doc = "Enable/Disable SVM pretty printer."
show_doc = "Show if SVM pretty printer are enabled/disabled."
def __init__(self, initial: bool = True):
super().__init__('svm-print', gdb.COMMAND_DATA, gdb.PARAM_BOOLEAN)
self.value = initial # default enabled
def get_set_string(self):
return SVMUtil.execout(f"{'enable' if self.value else 'disable'} pretty-printer .* {SVMUtil.pretty_printer_name}")
class SVMPrintStringLimit(gdb.Parameter):
"""Use this command to limit the number of characters in a string shown during pretty printing.
Does only limit java strings. To limit c strings use 'set print characters'."""
set_doc = "Set character limit for java strings."
show_doc = "Show character limit for java strings."
def __init__(self, initial: int = 200):
super().__init__('svm-print-string-limit', gdb.COMMAND_DATA, gdb.PARAM_ZUINTEGER_UNLIMITED)
self.value = initial
class SVMPrintElementLimit(gdb.Parameter):
"""Use this command to limit the number of elements in an array/collection shown during SVM pretty printing.
Does only limit java arrays and java some java collections. To limit other arrays use 'set print elements'.
However, 'print elements' also limits the amount of elements for java arrays and java collections.
If GDBs element limit is below the SVM element limit, printing will be limited by gdb."""
set_doc = "Set element limit for arrays and collections."
show_doc = "Show element limit for array and collections."
def __init__(self, initial: int = 10):
super().__init__('svm-print-element-limit', gdb.COMMAND_DATA, gdb.PARAM_ZUINTEGER_UNLIMITED)
self.value = initial
def get_set_string(self):
gdb_limit = gdb.parameter("print elements")
if gdb_limit >= 0 and (self.value > gdb_limit or self.value == -1):
return f"""The number of elements printed will be limited by GDBs 'print elements' which is {gdb_limit}.
To increase this limit use 'set print elements <limit>'"""
else:
return ""
class SVMPrintFieldLimit(gdb.Parameter):
"""Use this command to limit the number of fields in a java object shown during SVM pretty printing.
Does only limit java objects. To limit other objects use 'set print elements'.
However, 'print elements' also limits the amount of fields for java objects.
If GDBs element limit is below the field limit, field printing will be limited by gdb."""
set_doc = "Set field limit for objects."
show_doc = "Show field limit for objects."
def __init__(self, initial: int = 50):
super().__init__('svm-print-field-limit', gdb.COMMAND_DATA, gdb.PARAM_ZUINTEGER_UNLIMITED)
self.value = initial
def get_set_string(self):
gdb_limit = gdb.parameter("print elements")
if gdb_limit >= 0 and (self.value > gdb_limit or self.value == -1):
return f"""The number of fields printed will be limited by GDBs 'print elements' which is {gdb_limit}.
To increase this limit use 'set print elements <limit>'"""
else:
return ""
class SVMPrintDepthLimit(gdb.Parameter):
"""Use this command to limit the depth at which objects are printed by the SVM pretty printer.
Does only affect objects that are handled by the SVM pretty printer, similar to selfref checks.
However, 'print max-depth' also limits the svm print depth.
If GDBs max-depth limit is below the svm depth limit, printing will be limited by gdb."""
set_doc = "Set depth limit for svm objects."
show_doc = "Show depth limit for svm objects."
def __init__(self, initial: int = 1):
super().__init__('svm-print-depth-limit', gdb.COMMAND_DATA, gdb.PARAM_ZUINTEGER_UNLIMITED)
self.value = initial
def get_set_string(self):
gdb_limit = gdb.parameter("print max-depth")
if gdb_limit >= 0 and (self.value > gdb_limit or self.value == -1):
return f"""The print depth will be limited by GDBs 'print max-depth' which is {gdb_limit}.
To increase this limit use 'set print max-depth <limit>'"""
else:
return ""
class SVMUseHLRepParam(gdb.Parameter):
"""Use this command to enable/disable SVM high level representations.
Supported high level representations: ArrayList, HashMap"""
set_doc = "Enable/Disable pretty printing of high level representations."
show_doc = "Show if SVM pretty printer are enabled/disabled."
def __init__(self, initial: bool = True):
super().__init__('svm-use-hlrep', gdb.COMMAND_DATA, gdb.PARAM_BOOLEAN)
self.value = initial
class SVMInferGenericsParam(gdb.Parameter):
"""Use this command to set the limit of elements used to infer types for collections with generic type parameters.
(0 for no Inference, -1 for Inference over all elements)"""
set_doc = "Set limit of elements used for inferring generic type parameters."
show_doc = "Show limit of elements used for inferring generic type parameters."
def __init__(self, initial: int = 10):
super().__init__('svm-infer-generics', gdb.COMMAND_DATA, gdb.PARAM_ZUINTEGER_UNLIMITED)
self.value = initial
class SVMPrintAddressParam(gdb.Parameter):
"""Use this command to enable/disable additionally printing the addresses."""
set_doc = "Set additional printing of addresses."
show_doc = "Show additional printing of addresses."
def __init__(self, initial: str = 'disable'):
super().__init__('svm-print-address', gdb.COMMAND_DATA, gdb.PARAM_ENUM, ['on', 'enable', 'absolute', 'disable', 'off'])
self.with_adr = False
self.absolute_adr = False
self.value = initial
self.set_flags()
def set_flags(self):
if self.value == 'disable' or self.value == 'off':
self.with_adr = False
elif self.value == 'absolute':
self.absolute_adr = True
self.with_adr = True
else:
self.absolute_adr = False
self.with_adr = True
def get_set_string(self):
self.set_flags()
return ""
class SVMCheckSelfrefParam(gdb.Parameter):
"""Use this command to enable/disable cycle detection for pretty printing."""
set_doc = "Set selfref check."
show_doc = "Show selfref check."
def __init__(self, initial: bool = True):
super().__init__('svm-selfref-check', gdb.COMMAND_DATA, gdb.PARAM_BOOLEAN)
self.value = initial
def get_set_string(self):
# make sure selfrefs are cleared after changing this setting (to avoid unexpected behavior)
SVMUtil.selfref_cycles.clear()
return ""
class SVMPrintStaticFieldsParam(gdb.Parameter):
"""Use this command to enable/disable printing of static field members."""
set_doc = "Set print static fields."
show_doc = "Show print static fields."
def __init__(self, initial: bool = False):
super().__init__('svm-print-static-fields', gdb.COMMAND_DATA, gdb.PARAM_BOOLEAN)
self.value = initial
class SVMCompleteStaticVariablesParam(gdb.Parameter):
"""Use this command to enable/disable printing of static field members."""
set_doc = "Set complete static variables."
show_doc = "Show complete static variables."
def __init__(self, initial: bool = False):
super().__init__('svm-complete-static-variables', gdb.COMMAND_DATA, gdb.PARAM_BOOLEAN)
self.value = initial
class SVMDebugTraceParam(gdb.Parameter):
"""Use this command to enable/disable debug tracing for gdb-debughelpers.py.
Appends debug logs to the file 'gdb-debughelpers.trace.out' in the current working directory
(creates the file if it does not exist)."""
set_doc = "Set debug tracing."
show_doc = "Show debug tracing."
def __init__(self, initial: bool = False):
super().__init__('svm-debug-tracing', gdb.COMMAND_SUPPORT, gdb.PARAM_BOOLEAN)
self.value = initial
self.tracefile = open('gdb-debughelpers.trace.out', 'ab', 0) if initial else None
def get_set_string(self):
if self.value and self.tracefile is None:
self.tracefile = open('gdb-debughelpers.trace.out', 'ab', 0)
elif not self.value and self.tracefile is not None:
self.tracefile.close()
self.tracefile = None
return ""
def load_param(name: str, param_class):
try:
return param_class(globals()[name].value)
except (KeyError, AttributeError):
return param_class()
svm_print = load_param('svm_print', SVMPrintParam)
svm_print_string_limit = load_param('svm_print_string_limit', SVMPrintStringLimit)
svm_print_element_limit = load_param('svm_print_element_limit', SVMPrintElementLimit)
svm_print_field_limit = load_param('svm_print_field_limit', SVMPrintFieldLimit)
svm_print_depth_limit = load_param('svm_print_depth_limit', SVMPrintDepthLimit)
svm_use_hlrep = load_param('svm_use_hlrep', SVMUseHLRepParam)
svm_infer_generics = load_param('svm_infer_generics', SVMInferGenericsParam)
svm_print_address = load_param('svm_print_address', SVMPrintAddressParam)
svm_check_selfref = load_param('svm_check_selfref', SVMCheckSelfrefParam)
svm_print_static_fields = load_param('svm_print_static_fields', SVMPrintStaticFieldsParam)
svm_complete_static_variables = load_param('svm_complete_static_variables', SVMCompleteStaticVariablesParam)
svm_debug_tracing = load_param('svm_debug_tracing', SVMDebugTraceParam)
class SVMCommandDebugPrettyPrinting(gdb.Command):
"""Use this command to start debugging pretty printing."""
def __init__(self):
super().__init__('pdb', gdb.COMMAND_DATA)
def complete(self, text: str, word: str) -> int: # list[str] | int:
return gdb.COMPLETE_EXPRESSION
def invoke(self, arg: str, from_tty: bool) -> None:
trace(f'<SVMCommandDebugPrettyPrinting> - invoke({arg})')
command = "gdb.execute('print {}')".format(arg.replace("'", "\\'"))
import pdb
pdb.run(command)
SVMCommandDebugPrettyPrinting()
class SVMCommandPrint(gdb.Command):
"""Use this command for printing with awareness for java values.
This command shadows the alias 'p' for GDBs built-in print command if SVM pretty printing is enabled.
If the expression contains a java value, it is evaluated as such, otherwise GDBs default print command is used"""
class Token:
def __init__(self, kind: str = "", val: str = "", start: int = 0, end: int = 0):
self.kind = kind
self.val = val
self.start = start
self.end = end
class AutoComplete(RuntimeError):
def __init__(self, complete): # complete: list[str] | int
self.complete = complete
cache = dict()
scanner = None
expr = ""
t: Token = Token()
la: Token = Token()
sym: str = ""
def __init__(self):
super().__init__('p', gdb.COMMAND_DATA)
@staticmethod
def cast_to_rtt(obj: gdb.Value, obj_str: str) -> tuple: # tuple[gdb.Value, str]:
static_type = SVMUtil.get_basic_type(obj.type)
rtt = SVMUtil.get_rtt(obj)
obj = SVMUtil.cast_to(obj, rtt)
if static_type.name == rtt.name:
return obj, obj_str
else:
obj_oop = SVMUtil.get_compressed_oop(obj) if SVMUtil.is_compressed(rtt) else adr(obj)
return obj, f"(('{rtt.name}' *)({obj_oop}))"
# Define the token specifications
token_specification = [
('IDENT', r'\$?[a-zA-Z_][a-zA-Z0-9_]*'), # identifier (convenience variables may contain $)
('QIDENT', r"'\$?[a-zA-Z_][a-zA-Z0-9_.:]*'"), # quoted identifier
('FA', r'\.'), # field access
('LBRACK', r'\['), # opening bracket
('RBRACK', r'\]'), # closing bracket
('LPAREN', r'\('), # opening parentheses
('RPAREN', r'\)'), # closing parentheses
('COMMA', r','), # comma
('SKIP', r'\s+'), # skip over whitespaces
('OTHER', r'.'), # any other character, will be handled by gdb
]
token_regex = '|'.join(f'(?P<{kind}>{regex})' for (kind, regex) in token_specification)
def tokenize(self, expr: str) -> Iterable[Token]:
for match in re.finditer(self.token_regex, expr):
kind = match.lastgroup
val = match.group()
if kind == 'SKIP':
# skip whitespaces
continue
yield self.Token(kind, val, match.start(), match.end())
def setup_scanner(self, expr: str) -> None:
self.scanner = iter(self.tokenize(expr))
self.t = self.Token()
self.la = self.Token()
self.sym = ""
self.expr = expr
def scan(self):
self.t = self.la
self.la = next(self.scanner, self.Token())
self.sym = self.la.kind
def check(self, expected: str):
if self.sym == expected:
self.scan()
else:
raise RuntimeError(f"{expected} expected after {self.expr[:self.t.end]} but got {self.sym}")
def parse(self, completion: bool = False) -> str:
self.scan()
if self.sym == "" and completion:
raise self.AutoComplete(gdb.COMPLETE_EXPRESSION)
expr = self.expression(completion)
self.check("")
return expr
def expression(self, completion: bool = False) -> str:
expr = ""
while self.sym != "":
if self.sym == "IDENT":
expr += self.object(completion)
else:
# ignore everything that does not start with an identifier
self.scan()
expr += self.t.val
return expr
def object(self, completion: bool = False) -> str:
self.scan()
if self.sym == "" and completion:
raise self.AutoComplete(gdb.COMPLETE_EXPRESSION)
obj_str = self.t.val
if obj_str in self.cache:
obj, obj_str = self.cache[obj_str]
else:
try:
obj = gdb.parse_and_eval(obj_str)
except gdb.error:
# could not parse obj_str as obj -> let gdb deal with it later
return self.t.val
base_obj_str = obj_str
if not SVMUtil.is_primitive(obj.type) and SVMUtil.is_java_type(obj.type):
obj, obj_str = SVMCommandPrint.cast_to_rtt(obj, obj_str)
self.cache[base_obj_str] = (obj, obj_str)
while self.sym == "FA" or self.sym == "LPAREN" or self.sym == "LBRACK":
if self.sym == "FA":
self.scan()
if not completion:
self.check("IDENT")
else:
# handle auto-completion after field access
fields = SVMUtil.get_all_fields(obj.type, svm_complete_static_variables.value)
funcs = SVMUtil.get_all_member_functions(obj.type, svm_complete_static_variables.value, False)
field_names = set(f.name for f in fields)
func_names = set(f.name for f in funcs)
complete_set = field_names.union(func_names)
if self.sym == "":
raise self.AutoComplete(list(complete_set))
self.check("IDENT")
if self.sym == "":
raise self.AutoComplete([c for c in complete_set if c.startswith(self.t.val)])
obj = obj[self.t.val]
obj_str += "." + self.t.val
base_obj_str = obj_str
if not SVMUtil.is_primitive(obj.type) and SVMUtil.is_java_type(obj.type):
obj, obj_str = SVMCommandPrint.cast_to_rtt(obj, obj_str)
self.cache[base_obj_str] = (obj, obj_str)
elif self.sym == "LPAREN":
if obj.type.code != gdb.TYPE_CODE_METHOD:
raise RuntimeError(f"Method object expected at: {self.expr[:self.t.end]}")
self.scan()
param_str = self.params(completion)
self.check("RPAREN")
this, _, func_name = obj_str.rpartition('.')
if this in self.cache:
this_obj, this = self.cache[this]
else:
this_obj = gdb.parse_and_eval(this)
if this_obj.type.code == gdb.TYPE_CODE_PTR:
obj_str = f"{this}->{func_name}"
obj_str += f"({param_str})"
obj = gdb.parse_and_eval(obj_str)
base_obj_str = obj_str
if not SVMUtil.is_primitive(obj.type) and SVMUtil.is_java_type(obj.type):
obj, obj_str = SVMCommandPrint.cast_to_rtt(obj, obj_str)
self.cache[base_obj_str] = (obj, obj_str)
elif self.sym == "LBRACK":
is_array = obj.type.is_array_like or isinstance(gdb.default_visualizer(obj), SVMPPArray)
# gdb.types.get_basic_type strips typedefs and other type modifiers
is_pointer = gdb.types.get_basic_type(obj.type).code == gdb.TYPE_CODE_PTR
if not (is_array or is_pointer):
raise RuntimeError(f"Array object or pointer expected at: {self.expr[:self.t.end]}")
self.scan()
i_obj_str = self.array_index(completion)
if self.sym == "" and completion:
# handle autocompletion for array index
if SVMUtil.is_java_type(obj.type) and (i_obj_str == '' or i_obj_str.isnumeric()):
index = 0 if i_obj_str == '' else int(i_obj_str)
length = SVMUtil.get_int_field(obj, 'len')
complete = []
if index < length:
complete.append(f'{index}]')
if index + 1 < length:
complete.append(f'{index + 1}]')
if index + 2 < length:
complete.append(f'{length - 1}]')
raise self.AutoComplete(complete)
else:
raise self.AutoComplete(gdb.COMPLETE_EXPRESSION)
if i_obj_str in self.cache:
i_obj, i_obj_str = self.cache[i_obj_str]
else:
i_obj = gdb.parse_and_eval(i_obj_str)
self.check('RBRACK')
if is_array and SVMUtil.is_java_type(obj.type):
obj_str += ".data"
obj = SVMUtil.get_obj_field(obj, 'data', obj)
if isinstance(gdb.default_visualizer(i_obj), SVMPPBoxedPrimitive) or SVMUtil.is_primitive(i_obj.type):
if isinstance(gdb.default_visualizer(i_obj), SVMPPBoxedPrimitive):
index = SVMUtil.get_int_field(i_obj, 'value')
else:
index = int(i_obj)
obj_str += f"[{index}]"
obj = obj[index]
base_obj_str = obj_str
if not SVMUtil.is_primitive(obj.type) and SVMUtil.is_java_type(obj.type):
obj, obj_str = SVMCommandPrint.cast_to_rtt(obj, obj_str)
self.cache[base_obj_str] = (obj, obj_str)
else:
# let gdb figure out what to do
obj_str += f"[{i_obj_str}]"
if obj_str in self.cache:
obj, obj_str = self.cache[obj_str]
else:
obj = gdb.parse_and_eval(obj_str)
base_obj_str = obj_str
if not SVMUtil.is_primitive(obj.type) and SVMUtil.is_java_type(obj.type):
obj, obj_str = SVMCommandPrint.cast_to_rtt(obj, obj_str)
self.cache[base_obj_str] = (obj, obj_str)
if isinstance(gdb.default_visualizer(obj), SVMPPBoxedPrimitive):
obj_str += ".value"
return obj_str
def params(self, completion: bool = False) -> str:
param_str = ""
while self.sym != "RPAREN" and self.sym != "":
obj_str = ""
while self.sym != "RPAREN" and self.sym != "COMMA" and self.sym != "":
if self.sym == "IDENT":
obj_str += self.object(completion)
else:
self.scan()
obj_str += self.t.val
obj = gdb.parse_and_eval(obj_str) # check if gdb can handle the current param
if SVMUtil.is_java_type(obj.type) and SVMUtil.is_compressed(obj.type):
# uncompress compressed java params
obj_str = f"(('{SVMUtil.get_uncompressed_type(SVMUtil.get_basic_type(obj.type)).name}' *)({adr(obj)}))"
param_str += obj_str
if self.sym == "COMMA":
self.scan()
param_str += self.t.val
if self.sym == "" and completion:
# handle autocompletion for params
if self.t.kind == "LPAREN" or self.t.kind == "COMMA": # no open object access
raise self.AutoComplete(gdb.COMPLETE_EXPRESSION)
else:
raise self.AutoComplete(gdb.COMPLETE_NONE)
return param_str
def array_index(self, completion: bool = False) -> str:
i_obj_str = ""
while self.sym != "RBRACK" and self.sym != "":
if self.sym == "IDENT":
i_obj_str += self.object(completion)
else:
self.scan()
i_obj_str += self.t.val
return i_obj_str
def complete(self, text: str, word: str): # -> list[str] | int:
if not svm_print.value:
return gdb.COMPLETE_EXPRESSION
self.setup_scanner(text)
try:
self.parse(completion=True)
except self.AutoComplete as ac:
trace(f"<SVMCommandPrint> - complete({text}, {word}) -- autocomplete result: {ac.complete}")
return ac.complete
trace(f"<SVMCommandPrint> - complete({text}, {word}) -- no completion possible")
return gdb.COMPLETE_NONE
def invoke(self, arg: str, from_tty: bool) -> None:
if not svm_print.value:
gdb.execute(f"print {arg}")
return
output_format = ""
if arg.startswith('/'):
output_format, _, arg = arg.partition(' ')
self.setup_scanner(arg)
expr = self.parse()
trace(f"<SVMCommandPrint> - invoke({arg}) -- parsed arg: {expr}")
# handle print call as if it was a new prompt
SVMUtil.prompt_hook()
# let gdb evaluate the modified expression
gdb.execute(f"print{output_format} {expr}", False, False)
SVMCommandPrint()
class SVMCommandBreak(gdb.Command):
def __init__(self):
super().__init__('b', gdb.COMMAND_BREAKPOINTS, gdb.COMPLETE_LOCATION)
def invoke(self, arg: str, from_tty: bool) -> None:
args = gdb.string_to_argv(arg) + [''] # add an empty arg to avoid IndexError if arg is empty
# first argument can either be line number, symbol name, empty, or if condition
# # -> :: to make breakpoints work with IntelliJ function name notation
args[0] = args[0].replace('#', '::')
# let gdb execute the full break command with the updated symbol name
gdb.execute(f"break {''.join(args)}", False, False)
trace(f"<SVMCommandBreak> - invoke({arg}) -- invoked: break {''.join(args)}")
SVMCommandBreak()
class SVMFrameUnwinder(gdb.unwinder.Unwinder):
AMD64_RBP = 6
AMD64_RSP = 7
AMD64_RIP = 16
def __init__(self):
super().__init__('SubstrateVM FrameUnwinder')
self.stack_type = gdb.lookup_type('long')
self.deopt_frame_type = gdb.lookup_type('com.oracle.svm.core.deopt.DeoptimizedFrame')
def __call__(self, pending_frame):
if SVMUtil.deopt_stub_adr == 0:
# find deopt stub after its properly loaded
SVMUtil.deopt_stub_adr = gdb.lookup_global_symbol('com.oracle.svm.core.deopt.Deoptimizer::deoptStub',
gdb.SYMBOL_VAR_DOMAIN).value().address
try:
rsp = pending_frame.read_register('sp')
rip = pending_frame.read_register('pc')
if int(rip) == SVMUtil.deopt_stub_adr:
deopt_frame_stack_slot = rsp.cast(self.stack_type.pointer()).dereference()
deopt_frame = deopt_frame_stack_slot.cast(self.deopt_frame_type.pointer())
source_frame_size = deopt_frame['sourceTotalFrameSize']
# Now find the register-values for the caller frame
unwind_info = pending_frame.create_unwind_info(gdb.unwinder.FrameId(rsp, rip))
caller_rsp = rsp + int(source_frame_size)
unwind_info.add_saved_register(self.AMD64_RSP, gdb.Value(caller_rsp))
caller_rip = gdb.Value(caller_rsp - 8).cast(self.stack_type.pointer()).dereference()
unwind_info.add_saved_register(self.AMD64_RIP, gdb.Value(caller_rip))
return unwind_info
except Exception as e:
print(e)
# Fallback to default frame unwinding via debug_frame (dwarf)
return None
class SVMFrameFilter():
def __init__(self):
self.name = "SubstrateVM FrameFilter"
self.priority = 100
self.enabled = True
def filter(self, frame_iter):
for frame in frame_iter:
frame = frame.inferior_frame()
if SVMUtil.deopt_stub_adr and frame.pc() == SVMUtil.deopt_stub_adr:
yield SVMFrameDeopt(frame)
else:
yield SVMFrame(frame)
class SVMFrame(FrameDecorator):
def function(self):
frame = self.inferior_frame()
if not frame.name():
return 'Unknown Frame at ' + hex(int(frame.read_register('sp')))
func_name = str(frame.name().split('(')[0])
if frame.type() == gdb.INLINE_FRAME:
func_name = '<-- ' + func_name
filename = self.filename()
if filename:
line = self.line()
if line is None:
line = 0
eclipse_filename = '(' + os.path.basename(filename) + ':' + str(line) + ')'
else:
eclipse_filename = ''
return func_name + eclipse_filename
class SVMFrameDeopt(SVMFrame):
def function(self):
return '[DEOPT FRAMES ...]'
def frame_args(self):
return None
def frame_locals(self):
return None
try:
svminitfile = os.path.expandvars('${SVMGDBINITFILE}')
exec(open(svminitfile).read())
trace(f'successfully processed svminitfile: {svminitfile}')
except Exception as e:
trace(f'<exception in svminitfile execution: {e}>')
try:
gdb.prompt_hook = SVMUtil.prompt_hook
svm_objfile = gdb.current_objfile()
# Only if we have an objfile and an SVM specific symbol we consider this an SVM objfile
if svm_objfile and svm_objfile.lookup_global_symbol("com.oracle.svm.core.Isolates"):
gdb.printing.register_pretty_printer(svm_objfile, SVMPrettyPrinter(), True)
# deopt stub points to the wrong address at first -> set dummy value to fill later (0 from SVMUtil)
deopt_stub_available = gdb.lookup_global_symbol('com.oracle.svm.core.deopt.Deoptimizer::deoptStub',
gdb.SYMBOL_VAR_DOMAIN)
if deopt_stub_available:
SVMUtil.frame_unwinder = SVMFrameUnwinder()
gdb.unwinder.register_unwinder(svm_objfile, SVMUtil.frame_unwinder)
SVMUtil.frame_filter = SVMFrameFilter()
svm_objfile.frame_filters[SVMUtil.frame_filter.name] = SVMUtil.frame_filter
else:
print(f'Warning: Load {os.path.basename(__file__)} only in the context of an SVM objfile')
# fallback (e.g. if loaded manually -> look through all objfiles and attach pretty printer)
for of in gdb.objfiles():
if of.lookup_global_symbol("com.oracle.svm.core.Isolates"):
gdb.printing.register_pretty_printer(of, SVMPrettyPrinter(), True)
# save and restore SVM pretty printer for reloaded objfiles (e.g. shared libraries)
def new_objectfile(new_objfile_event):
objfile = new_objfile_event.new_objfile
if objfile.filename in SVMUtil.pretty_print_objfiles:
gdb.printing.register_pretty_printer(objfile, SVMPrettyPrinter(), True)
def free_objectfile(free_objfile_event):
objfile = free_objfile_event.objfile
if any(pp.name == SVMUtil.pretty_printer_name for pp in objfile.pretty_printers):
SVMUtil.pretty_print_objfiles.add(objfile.filename)
gdb.events.new_objfile.connect(new_objectfile)
gdb.events.free_objfile.connect(free_objectfile)
except Exception as e:
print(f'<exception in gdb-debughelpers initialization: {e}>')