diff --git a/docs/reference-manual/native-image/guides/debug-native-executables-with-gdb.md b/docs/reference-manual/native-image/guides/debug-native-executables-with-gdb.md index 01f1a94280dd..485fcbf8cadd 100644 --- a/docs/reference-manual/native-image/guides/debug-native-executables-with-gdb.md +++ b/docs/reference-manual/native-image/guides/debug-native-executables-with-gdb.md @@ -564,4 +564,5 @@ void svm_dbg_print_locationInfo(graal_isolatethread_t* thread, size_t mem); ### Related Documentation -* [Debug Info Feature](../DebugInfo.md) \ No newline at end of file +* [Debug Info Feature](../DebugInfo.md) +* [Debug Native Executables with a Python Helper Script](debug-native-executables-with-python-helper.md) \ No newline at end of file diff --git a/docs/reference-manual/native-image/guides/debug-native-executables-with-python-helper.md b/docs/reference-manual/native-image/guides/debug-native-executables-with-python-helper.md new file mode 100644 index 000000000000..2ea9b685f679 --- /dev/null +++ b/docs/reference-manual/native-image/guides/debug-native-executables-with-python-helper.md @@ -0,0 +1,166 @@ +--- +layout: ni-docs +toc_group: how-to-guides +link_title: Debug Native Executables with a Python Helper Script +permalink: /reference-manual/native-image/guides/debug-native-image-process-with-python-helper-script/ +--- +# Debug Native Executables with a Python Helper Script + +Additionally to the [GDB debugging](debug-native-executables-with-gdb.md), you can debug a `native-image` process using a Python helper script, _gdb-debughelpers.py_. +The [GDB Python API](https://sourceware.org/gdb/current/onlinedocs/gdb/Python.html) is used to provide a reasonably good experience for debugging native executables or shared libraries. +It requires GDB with Python support. +The debugging extension is tested against GDB 13.2 and supports the new debuginfo generation introduced in GraalVM for JDK 17 and later. + +> Note: The _gdb-debughelpers.py_ file does not work with versions older than version 13.2 of `gdb` or versions older than GraalVM for JDK 17. + +The Python script _gdb-debughelpers.py_ can be found in the _<GRAALVM\_HOME>/lib/svm/debug_ directory. +If debuginfo generation is enabled (see [Build a Native Executable with Debug Information](debug-native-executables-with-gdb.md#build-a-native-executable-with-debug-information)), the script is copied to the build directory. +The `native-image` tool adds the debugging section `.debug_gdb_scripts` to the debug info file, which causes GDB to automatically load _gdb-debughelpers.py_ from the current working directory. + +For [security reasons](https://sourceware.org/gdb/current/onlinedocs/gdb/Auto_002dloading-safe-path.html) +the first time GDB encounters a native executable or shared library that requests a specific Python file to be loaded it will print a warning: + +> warning: File "/gdb-debughelpers.py" auto-loading has been declined by your `auto-load safe-path' set to "$debugdir:$datadir/auto-load". +> +> To enable execution of this file add +>         add-auto-load-safe-path /gdb-debughelpers.py +> line to your configuration file "/.gdbinit". +> To completely disable this security protection add +>         add-auto-load-safe-path / +> line to your configuration file "/.gdbinit". +> For more information about this security protection see the +> "Auto-loading safe path" section in the GDB manual. E.g., run from the shell: +>         info "(gdb)Auto-loading safe path" + +To solve this, either add the current working directory to _~/.gdbinit_ as follows: + + echo "add-auto-load-safe-path /gdb-debughelpers.py" >> ~/.gdbinit + +or pass the path as a command line argument to `gdb`: + + gdb -iex "set auto-load safe-path /gdb-debughelpers.py" + +Both enable GDB to auto-load _gdb-debughelpers.py_ from the current working directory. + +Auto-loading is the recommended way to provide the script to GDB. +However, it is possible to manually load the script from GDB explicitly with: + + (gdb) source gdb-debughelpers.py + +## Pretty Printing Support + +Loading _gdb-debughelpers.py_ registers a new pretty printer to GDB, which adds an extra level of convenience for debugging native executables or shared libraries. +This pretty printer handles the printing of Java Objects, Arrays, Strings, and Enums for debugging native executables or shared libraries. +If the Java application uses `@CStruct` and `@CPointer` annotations to access C data structures, the pretty printer will also try to print them as if they were Java data structures. +If the C data structures cannot be printed by the pretty printer, printing is performed by GDB. + +The pretty printer also prints of the primitive value of a boxed primitive (instead of a Java Object). + +Whenever printing is done via the `p` alias of the `print` command the pretty printer intercepts that call to perform type casts to the respective runtime types of Java Objects. +This also applies for auto-completion when using the `p` alias. +This means that if the static type is different to the runtime type, the `print` command uses the static type, which leaves the user to discover the runtime type and typecast it. +Additionally, the `p` alias understands Java field and array access and function calls for Java Objects. + +#### Limitations + +The `print` command still uses its default implementation, as there is no way to overwrite it, while still keeping the capability of the default `print` command. +Overriding would cause printing non-Java Objects to not work properly. +Therefore, only the `p` alias for the `print` command is overwritten by the pretty printer, such that the user can still make use of the default GDB `print` command. + +### Options to Control the Pretty Printer Behavior + +In addition to the enhanced `p` alias, _gdb-debughelpers.py_ introduces some GDB parameters to customize the behavior of the pretty printer. +Parameters in GDB can be controlled with `set ` and `show ` commands, and thus integrate with GDB's customization options. + +* #### svm-print on/off + +Use this command to enable/disable the pretty printer. +This also resets the `print` command alias `p` to its default behavior. +Alternatively pretty printing can be suppressed with the +[`raw` printing option of GDB's `print` command](https://sourceware.org/gdb/current/onlinedocs/gdb/Output-Formats.html): + + (gdb) show svm-print + The current value of 'svm-print' is "on". + + (gdb) print str + $1 = "string" + + (gdb) print/r str + $2 = (java.lang.String *) 0x7ffff689d2d0 + + (gdb) set svm-print off + 1 printer disabled + 1 of 2 printers enabled + + (gdb) print str + $3 = (java.lang.String *) 0x7ffff689d2d0 + +* #### svm-print-string-limit <int> + +Customizes the maximum length for pretty printing a Java String. +The default value is `200`. +Set to `-1` or `unlimited` for unlimited printing of a Java String. +This does not change the limit for a C String, which can be controlled with GDB's `set print characters` command. + +* #### svm-print-element-limit <int> + +Customizes the maximum number of elements for pretty printing a Java Array, ArrayList, and HashMap. +The default value is `10`. +Set to `-1` or `unlimited` to print an unlimited number of elements. +This does not change the limit for a C array, which can be controlled with GDB's `set print elements` command. +However, GDB's parameter `print elements` is the upper bound for `svm-print-element-limit`. + +* #### svm-print-field-limit <int> + +Customizes the maximum number of elements for pretty printing fields of a Java Object. +The default value is `50`. +Set to `-1` or `unlimited` to print an unlimited number of fields. +GDB's parameter `print elements` is the upper bound for `svm-print-field-limit`. + +* #### svm-print-depth-limit <int> + +Customizes the maximum depth of recursive pretty printing. +The default value is `1`. +The children of direct children are printed (a sane default to make contents of boxed values visible). +Set to `-1` or `unlimited` to print unlimited depth. +GDB's parameter `print max-depth` is the upper bound for `svm-print-depth-limit`. + +* #### svm-use-hlrep on/off + +Enables/disables pretty printing for higher level representations. +It provides a more data-oriented view on some Java data structures with a known internal structure such as Lists or Maps. +Currently supports ArrayList and HashMap. + +* #### svm-infer-generics <int> + +Customizes the number of elements taken into account to infer the generic type of higher level representations. +The default value is `10`. +Set to `0` to not infer generic types and `-1` or `unlimited` to infer the generic type of all elements. + +* #### svm-print-address absolute/on/off + +Enables/disables printing of addresses in addition to regular pretty printing. +When `absolute` mode is used even compressed references are shown as absolute addresses. +Printing addresses is disabled by default. + +* #### svm-print-static-fields on/off + +Enables/disables printing of static fields for a Java Object. +Printing static fields is disabled by default. + +* #### svm-complete-static-variables on/off + +Enables/disables auto-completion of static field members for the enhanced `p` alias. +Auto-completion of static fields is enabled by default. + +* #### svm-selfref-check on/off + +Enables/disables self-reference checks for data structures. +The pretty printer detects a self-referential data structure and prevents further expansion to avoid endless recursion. +Self-reference checks are enabled by default. +For testing, this feature can be temporary disabled (usually you wouldn't want to do this). + +### Related Documentation + +* [Debug Info Feature](../DebugInfo.md) +* [Debug Native Executables with GDB](debug-native-executables-with-gdb.md) \ No newline at end of file diff --git a/docs/reference-manual/native-image/guides/guides.md b/docs/reference-manual/native-image/guides/guides.md index 6d16c9df9ae7..a899239fec00 100644 --- a/docs/reference-manual/native-image/guides/guides.md +++ b/docs/reference-manual/native-image/guides/guides.md @@ -25,6 +25,7 @@ Here you will learn how to: - [Containerize a Native Executable and Run in a Docker Container](containerise-native-executable-with-docker.md) - [Create a Heap Dump from a Native Executable](create-heap-dump-from-native-executable.md) - [Debug Native Executables with GDB](debug-native-executables-with-gdb.md) +- [Debug Native Executables with a Python Helper Script](debug-native-executables-with-python-helper.md) - [Include Reachability Metadata Using the Native Image Gradle Plugin](include-reachability-metadata-gradle.md) - [Include Reachability Metadata Using the Native Image Maven Plugin](include-reachability-metadata-maven.md) - [Include Resources in a Native Executable](include-resources.md) diff --git a/substratevm/CHANGELOG.md b/substratevm/CHANGELOG.md index 82ede13ea056..40a09a975202 100644 --- a/substratevm/CHANGELOG.md +++ b/substratevm/CHANGELOG.md @@ -2,6 +2,9 @@ This changelog summarizes major changes to GraalVM Native Image. +## GraalVM for JDK 24 (Internal Version 24.2.0) +* (GR-48384) Added a GDB Python script (`gdb-debughelpers.py`) to improve the Native Image debugging experience. + ## GraalVM for JDK 23 (Internal Version 24.1.0) * (GR-51520) The old class initialization strategy, which was deprecated in GraalVM for JDK 22, is removed. The option `StrictImageHeap` no longer has any effect. * (GR-51106) Fields that are accessed via a `VarHandle` or `MethodHandle` are no longer marked as "unsafe accessed" when the `VarHandle`/`MethodHandle` can be fully intrinsified. diff --git a/substratevm/mx.substratevm/mx_substratevm.py b/substratevm/mx.substratevm/mx_substratevm.py index 234393cd2d06..d811ed620079 100644 --- a/substratevm/mx.substratevm/mx_substratevm.py +++ b/substratevm/mx.substratevm/mx_substratevm.py @@ -986,6 +986,125 @@ def build_debug_test(variant_name, image_name, extra_args): os.environ.update({'debuginfotest_isolates' : 'yes'}) mx.run([os.environ.get('GDB_BIN', 'gdb'), '-ex', 'python "ISOLATES=True"', '-x', gdb_utils_py, '-x', testhello_py, hello_binary]) + +def _gdbdebughelperstest(native_image, path, with_isolates_only, build_only, test_only, args): + test_proj = mx.dependency('com.oracle.svm.test') + test_source_path = test_proj.source_dirs()[0] + tutorial_proj = mx.dependency('com.oracle.svm.tutorial') + tutorial_c_source_dir = join(tutorial_proj.dir, 'native') + tutorial_source_path = tutorial_proj.source_dirs()[0] + + gdbdebughelpers_py = join(mx.dependency('com.oracle.svm.hosted.image.debug').output_dir(), 'gdb-debughelpers.py') + + test_python_source_dir = join(test_source_path, 'com', 'oracle', 'svm', 'test', 'debug', 'helper') + test_pretty_printer_py = join(test_python_source_dir, 'test_pretty_printer.py') + test_cinterface_py = join(test_python_source_dir, 'test_cinterface.py') + test_class_loader_py = join(test_python_source_dir, 'test_class_loader.py') + test_settings_py = join(test_python_source_dir, 'test_settings.py') + test_svm_util_py = join(test_python_source_dir, 'test_svm_util.py') + + test_pretty_printer_args = [ + '-cp', classpath('com.oracle.svm.test'), + # We do not want to step into class initializer, so initialize everything at build time. + '--initialize-at-build-time=com.oracle.svm.test.debug.helper', + 'com.oracle.svm.test.debug.helper.PrettyPrinterTest' + ] + test_cinterface_args = [ + '--shared', + '-Dcom.oracle.svm.tutorial.headerfile=' + join(tutorial_c_source_dir, 'mydata.h'), + '-cp', tutorial_proj.output_dir() + ] + test_class_loader_args = [ + '-cp', classpath('com.oracle.svm.test'), + '-Dsvm.test.missing.classes=' + classpath('com.oracle.svm.test.missing.classes'), + '--initialize-at-build-time=com.oracle.svm.test.debug.helper', + # We need the static initializer of the ClassLoaderTest to run at image build time + '--initialize-at-build-time=com.oracle.svm.test.missing.classes', + 'com.oracle.svm.test.debug.helper.ClassLoaderTest' + ] + + gdb_args = [ + os.environ.get('GDB_BIN', 'gdb'), + '--nx', + '-q', # do not print the introductory and copyright messages + '-iex', 'set logging overwrite on', + '-iex', 'set logging redirect on', + '-iex', 'set logging enabled on', + ] + + def run_debug_test(image_name: str, testfile: str, source_path: str, with_isolates: bool = True, + build_cinterfacetutorial: bool = False, extra_args: list[str] = None, skip_build: bool = False): + extra_args = [] if extra_args is None else extra_args + build_dir = join(path, image_name + ("" if with_isolates else "_no_isolates")) + + if not test_only and not skip_build: + # clean / create output directory + if exists(build_dir): + mx.rmtree(build_dir) + mx.ensure_dir_exists(build_dir) + + build_args = args + [ + '-H:CLibraryPath=' + source_path, + '--native-image-info', + '-Djdk.graal.LogFile=graal.log', + '-g', '-O0', + ] + svm_experimental_options([ + '-H:+VerifyNamingConventions', + '-H:+SourceLevelDebug', + '-H:+IncludeDebugHelperMethods', + '-H:DebugInfoSourceSearchPath=' + source_path, + ]) + extra_args + + if not with_isolates: + build_args += svm_experimental_options(['-H:-SpawnIsolates']) + + if build_cinterfacetutorial: + build_args += ['-o', join(build_dir, 'lib' + image_name)] + else: + build_args += ['-o', join(build_dir, image_name)] + + mx.log(f"native_image {' '.join(build_args)}") + native_image(build_args) + + if build_cinterfacetutorial: + if mx.get_os() != 'windows': + c_command = ['cc', '-g', join(tutorial_c_source_dir, 'cinterfacetutorial.c'), + '-I.', '-L.', '-lcinterfacetutorial', + '-ldl', '-Wl,-rpath,' + build_dir, + '-o', 'cinterfacetutorial'] + + else: + c_command = ['cl', '-MD', join(tutorial_c_source_dir, 'cinterfacetutorial.c'), '-I.', + 'libcinterfacetutorial.lib'] + mx.log(' '.join(c_command)) + mx.run(c_command, cwd=build_dir) + if not build_only and mx.get_os() == 'linux': + # copying the most recent version of gdb-debughelpers.py (even if the native image was not built) + mx.log(f"Copying {gdbdebughelpers_py} to {build_dir}") + mx.copyfile(gdbdebughelpers_py, join(build_dir, 'gdb-debughelpers.py')) + + gdb_command = gdb_args + [ + '-iex', f"set auto-load safe-path {join(build_dir, 'gdb-debughelpers.py')}", + '-x', testfile, join(build_dir, image_name) + ] + mx.log(' '.join(gdb_command)) + # unittest may result in different exit code, nonZeroIsFatal ensures that we can go on with other test + mx.run(gdb_command, cwd=build_dir, nonZeroIsFatal=False) + + if not with_isolates_only: + run_debug_test('prettyPrinterTest', test_pretty_printer_py, test_source_path, False, + extra_args=test_pretty_printer_args) + run_debug_test('prettyPrinterTest', test_pretty_printer_py, test_source_path, extra_args=test_pretty_printer_args) + run_debug_test('prettyPrinterTest', test_settings_py, test_source_path, extra_args=test_pretty_printer_args, skip_build=True) + run_debug_test('prettyPrinterTest', test_svm_util_py, test_source_path, extra_args=test_pretty_printer_args, skip_build=True) + + run_debug_test('cinterfacetutorial', test_cinterface_py, tutorial_source_path, build_cinterfacetutorial=True, + extra_args=test_cinterface_args) + + run_debug_test('classLoaderTest', test_class_loader_py, test_source_path, extra_args=test_class_loader_args) + + + def _javac_image(native_image, path, args=None): args = [] if args is None else args mx_util.ensure_dir_exists(path) @@ -1520,6 +1639,30 @@ def debuginfotestshared(args, config=None): # ideally we ought to script a gdb run native_image_context_run(_cinterfacetutorial, all_args) +@mx.command(suite_name=suite.name, command_name='gdbdebughelperstest', usage_msg='[options]') +def gdbdebughelperstest(args, config=None): + """ + builds and tests gdb-debughelpers.py with multiple native images with debuginfo + """ + parser = ArgumentParser(prog='mx gdbdebughelperstest') + all_args = ['--output-path', '--with-isolates-only', '--build-only', '--test-only'] + masked_args = [_mask(arg, all_args) for arg in args] + parser.add_argument(all_args[0], metavar='', nargs=1, help='Path of the generated image', default=[join(svmbuild_dir(), "gdbdebughelperstest")]) + parser.add_argument(all_args[1], action='store_true', help='Only build and test the native image with isolates') + parser.add_argument(all_args[2], action='store_true', help='Only build the native image') + parser.add_argument(all_args[3], action='store_true', help='Only run the tests') + parser.add_argument('image_args', nargs='*', default=[]) + parsed = parser.parse_args(masked_args) + output_path = unmask(parsed.output_path)[0] + with_isolates_only = parsed.with_isolates_only + build_only = parsed.build_only + test_only = parsed.test_only + native_image_context_run( + lambda native_image, a: + _gdbdebughelperstest(native_image, output_path, with_isolates_only, build_only, test_only, a), unmask(parsed.image_args), + config=config + ) + @mx.command(suite_name=suite.name, command_name='helloworld', usage_msg='[options]') def helloworld(args, config=None): """ @@ -1878,6 +2021,17 @@ def isJDKDependent(self): return True +class GDBDebugHelpers(mx.ArchivableProject): + def output_dir(self): + return os.path.join(self.dir, 'src', self.name, 'gdbpy') + + def archive_prefix(self): + return '' + + def getResults(self): + return [os.path.join(self.output_dir(), 'gdb-debughelpers.py')] + + class SubstrateCompilerFlagsBuilder(mx.ArchivableProject): flags_build_dependencies = [ diff --git a/substratevm/mx.substratevm/suite.py b/substratevm/mx.substratevm/suite.py index f1fd53da29c3..f48805a982f0 100644 --- a/substratevm/mx.substratevm/suite.py +++ b/substratevm/mx.substratevm/suite.py @@ -735,6 +735,10 @@ "spotbugs" : "false", }, + "com.oracle.svm.hosted.image.debug": { + "class": "GDBDebugHelpers", + }, + # Native libraries below explicitly set _FORTIFY_SOURCE to 0. This constant controls how glibc handles some # functions that can cause a stack overflow like snprintf. If set to 1 or 2, it causes glibc to use internal # functions with extra checking that are not available in all libc implementations. Different distros use @@ -953,6 +957,21 @@ "jacoco" : "exclude", }, + "com.oracle.svm.test.missing.classes": { + "subDir": "src", + "sourceDirs": ["src"], + "dependencies": [ + ], + "annotationProcessors": [ + ], + "checkstyle": "com.oracle.svm.test", + "javaCompliance" : "21+", + "workingSets": "SVM", + "spotbugs": "false", + "testProject": True, + "jacoco" : "exclude", + }, + "com.oracle.svm.with.space.test": { "subDir": "src", "sourceDirs": ["src"], @@ -2086,10 +2105,20 @@ "sdk:NATIVEIMAGE", "SVM", "SVM_CONFIGURE", + "SVM_TEST_MISSING_CLASSES", ], "testDistribution" : True, }, + "SVM_TEST_MISSING_CLASSES" : { + "subDir": "src", + "relpath" : True, + "dependencies": [ + "com.oracle.svm.test.missing.classes" + ], + "testDistribution": True, + }, + # Special test distribution used for testing inclusion of resources from jar files with a space in their name. # The space in the distribution name is intentional. "SVM_TESTS WITH SPACE" : { @@ -2138,11 +2167,17 @@ }, }, + "SVM_DEBUG_HELPER": { + "dependencies": ["com.oracle.svm.hosted.image.debug"], + "javaCompliance" : "21+", + }, + "SVM_GRAALVM_SUPPORT" : { "native" : True, "platformDependent" : True, "description" : "SubstrateVM support distribution for the GraalVM", "layout" : { + "debug/": ["extracted-dependency:substratevm:SVM_DEBUG_HELPER"], "clibraries/" : ["extracted-dependency:substratevm:SVM_HOSTED_NATIVE"], "builder/clibraries/" : ["extracted-dependency:substratevm:SVM_HOSTED_NATIVE"], "builder/lib/" : ["dependency:com.oracle.svm.native.reporterchelper"], diff --git a/substratevm/src/com.oracle.svm.hosted.image.debug/gdbpy/gdb-debughelpers.py b/substratevm/src/com.oracle.svm.hosted.image.debug/gdbpy/gdb-debughelpers.py new file mode 100644 index 000000000000..3e7470464963 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted.image.debug/gdbpy/gdb-debughelpers.py @@ -0,0 +1,1638 @@ +# +# 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 pdb + +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]) < 13: + message = ( + 'Cannot load SubstrateVM debugging assistance for GDB from ' + os.path.basename(__file__) + + ': it requires at least GDB 13.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", gdb.SYMBOL_TYPES_DOMAIN) 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('(bool)__svm_use_heap_base')), True, gdb.error) + compressed_shift = try_or_else(lambda: int(gdb.parse_and_eval('(int)__svm_compressed_shift')), 0, gdb.error) + oop_tags_mask = try_or_else(lambda: int(gdb.parse_and_eval('(int)__svm_oop_tags_mask')), 0, gdb.error) + object_alignment = try_or_else(lambda: int(gdb.parse_and_eval('(int)__svm_object_alignment')), 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") + hub_type = gdb.lookup_type("java.lang.Class") + 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}', gdb.SYMBOL_TYPES_DOMAIN) 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_architecture(cls) -> str: + try: + arch_name = gdb.selected_frame().architecture().name() + except gdb.error: + # no frame available + arch_name = "" + + if "x86-64" in arch_name: + return "amd64" + elif "aarch64" in arch_name: + return "arm64" + else: + return arch_name + + @classmethod + def get_isolate_thread(cls) -> gdb.Value: + arch = cls.get_architecture() + if arch == "amd64": + return gdb.selected_frame().read_register('r15') + elif arch == "arm64": + return gdb.selected_frame().read_register('r28') + else: + return cls.null + + @classmethod + def get_heap_base(cls) -> gdb.Value: + arch = cls.get_architecture() + if arch == "amd64": + return gdb.selected_frame().read_register('r14') + elif arch == "arm64": + return gdb.selected_frame().read_register('r29') + 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) else t + trace(f' - 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' - get_compressed_type({t}) = {type_name}') + return gdb.lookup_type(type_name) + + @classmethod + def get_compressed_adr(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) + + absolute_adr = adr(obj) + if absolute_adr == 0: + return absolute_adr + + # recreate correct address for compressed oops + # For an explanation of the conversion rules see com.oracle.svm.core.heap.ReferenceAccess + is_hub = cls.get_rtt(obj) == cls.hub_type + oop_compressed_shift = cls.compressed_shift + oop_tag_shift = int.bit_count(cls.oop_tags_mask) + oop_align_shift = int.bit_count(cls.object_alignment - 1) + compressed_adr = absolute_adr + if cls.use_heap_base: + compressed_adr -= int(SVMUtil.get_heap_base()) + if is_hub: + if oop_compressed_shift == 0: + oop_compressed_shift = oop_align_shift + compressed_adr = compressed_adr << oop_tag_shift + compressed_adr = compressed_adr >> oop_compressed_shift + + return compressed_adr + + @classmethod + def get_unqualified_type_name(cls, qualified_type_name: str) -> str: + result = qualified_type_name.split('.')[-1] + result = result.split('$')[-1] + trace(f' - get_unqualified_type_name({qualified_type_name}) = {result}') + return result + + @classmethod + def is_compressed(cls, t: gdb.Type) -> bool: + 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 ::_z_. + result = type_name.startswith(cls.compressed_ref_prefix) or ('::' + cls.compressed_ref_prefix) in type_name + trace(f' - 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_adr(obj))})' + else: + result = f' @({hex(adr(obj))})' + trace(f' - 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' - 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' - 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' ') + cls.selfref_cycles.add(child_adr) + else: + trace(f' {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' - 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' - 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' - 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' - get_java_string({hex(adr(obj))}) = {result}') + return result + + @classmethod + def get_obj_field(cls, obj: gdb.Value, field_name: str, default: gdb.Value | None = null) -> gdb.Value | None: + try: + return obj[field_name] + except gdb.error: + return default + + @classmethod + def get_int_field(cls, obj: gdb.Value, field_name: str, default: int | None = 0) -> int | None: + 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' - 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) + + # 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' - 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_adr = cls.get_compressed_adr(obj) + else: + obj_adr = adr(obj) + + trace(f' - cast_to({hex(adr(obj))}, {t})') + if t.code != gdb.TYPE_CODE_PTR: + t = t.pointer() + + trace(f' - 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_adr).cast(t) + + @classmethod + def get_symbol_adr(cls, symbol: str) -> int: + trace(f' - get_symbol_adr({symbol})') + return gdb.parse_and_eval(symbol).address + + @classmethod + def execout(cls, cmd: str) -> str: + trace(f' - execout({cmd})') + return gdb.execute(cmd, False, True) + + @classmethod + def get_basic_type(cls, t: gdb.Type) -> gdb.Type: + trace(f' - 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' - is_primitive({t}) = {result}') + return result + + @classmethod + def is_primitive_wrapper(cls, t: gdb.Type) -> bool: + result = t in cls.wrapper_types + trace(f' - 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[gdb.Type], t: gdb.Type) -> 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[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[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' - 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 common 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 would not be enough + result = ((t.code == gdb.TYPE_CODE_STRUCT or t.code == gdb.TYPE_CODE_UNION) and + gdb.lookup_global_symbol(t.name + '.class', gdb.SYMBOL_VAR_DOMAIN) is not None) + + trace(f' - is_java_obj({t}) = {result}') + return result + + +class SVMPPString: + def __init__(self, obj: gdb.Value, java: bool = True): + trace(f' - __init__({hex(adr(obj))})') + self.__obj = obj + self.__java = java + + def to_string(self) -> str | gdb.Value: + trace(' - 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' - to_string = {value}') + return value + + +class SVMPPArray: + def __init__(self, obj: gdb.Value, java_array: bool = True): + trace(f' - __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(' - display_hint = array') + return 'array' + + def to_string(self) -> str | gdb.Value: + trace(' - 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' - to_string = {value}') + return value + + def __iter__(self): + trace(' - __iter__') + if self.__array is not None: + for i in range(self.__length): + yield self.__array[i] + + def children(self) -> Iterable[object]: + trace(' - 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' - 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' - __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' - __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 | gdb.Value: + trace(' - 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' - to_string = {result}') + return result + except gdb.error as ex: + trace(f" - to_string error - SVMPPClass: {ex}") + return 'object' + + def children(self) -> Iterable[object]: + trace(' - children (class field iterator)') + if self.__skip_children: + return + fields = [str(f.name) for f in SVMUtil.get_all_fields(self.__obj.type, svm_print_static_fields.value) if f.name != SVMUtil.hub_field_name] + for index, f in enumerate(fields): + trace(f' - 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' - __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 | gdb.Value: + result = SVMUtil.get_java_string(self.__name) + f"({self.__ordinal})" + if svm_print_address.with_adr: + result += SVMUtil.adr_str(self.__obj) + trace(f' - to_string = {result}') + return result + + +class SVMPPBoxedPrimitive: + def __init__(self, obj: gdb.Value): + trace(f' - __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 | gdb.Value: + result = str(self.__value) + if svm_print_address.with_adr: + result += SVMUtil.adr_str(self.__obj) + trace(f' - to_string = {result}') + return result + + +class SVMPPConst: + def __init__(self, val: str | None): + trace(' - __init__') + self.__val = val + + def to_string(self) -> str | gdb.Value: + result = "null" if self.__val is None else self.__val + trace(f' - 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' - __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' - __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 | gdb.Value: + trace(' - 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' - to_string = {res}') + return res + + def infer_generic_types(self) -> str: + elem_type: 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(' - display_hint = array') + return 'array' + + def __iter__(self) -> gdb.Value: + trace(' - __iter__') + if self.__data is not None: + for i in range(self.__size): + yield self.__data[i] + + def children(self) -> Iterable[object]: + trace(f' - 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' - 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' - __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 | gdb.Value: + trace(' - 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' - to_string = {res}') + return res + + def infer_generic_types(self) -> (str, str): + key_type: list[gdb.Type] = [] + value_type: 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(' - display_hint = map') + return "map" + + def __iter__(self) -> (gdb.Value, gdb.Value): + trace(' - __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' - 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' - 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' 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 '""" + 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 '""" + 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 '""" + 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) -> list[str] | int: + return gdb.COMPLETE_EXPRESSION + + def invoke(self, arg: str, from_tty: bool) -> None: + trace(f' - invoke({arg})') + command = "gdb.execute('print {}')".format(arg.replace("'", "\\'")) + 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: 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[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_adr = SVMUtil.get_compressed_adr(obj) if SVMUtil.is_compressed(rtt) else adr(obj) + return obj, f"(('{rtt.name}' *)({obj_adr}))" + + # 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": + if (not obj.type.is_array_like) and not isinstance(gdb.default_visualizer(obj), SVMPPArray): + raise RuntimeError(f"Array object 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 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]: + 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" - complete({text}, {word}) -- autocomplete result: {ac.complete}") + return ac.complete + trace(f" - 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" - 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" - 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'') + +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.SYMBOL_TYPES_DOMAIN): + 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.SYMBOL_TYPES_DOMAIN): + 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'') diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/image/NativeImageDebugInfoFeature.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/image/NativeImageDebugInfoFeature.java index 1d71e5cf98f8..10c48a7f62ce 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/image/NativeImageDebugInfoFeature.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/image/NativeImageDebugInfoFeature.java @@ -165,7 +165,7 @@ public boolean isLoadable() { var content = AssemblyBuffer.createOutputAssembler(objectFile.getByteOrder()); // 1 -> python file content.writeByte((byte) 1); - content.writeString("./svmhelpers.py"); + content.writeString("./gdb-debughelpers.py"); return new BasicProgbitsSectionImpl(content.getBlob()) { @Override public boolean isLoadable() { @@ -180,7 +180,7 @@ public boolean isLoadable() { objectFile.newUserDefinedSection(".debug.svm.imagebuild.arguments", makeSectionImpl.apply(DiagnosticUtils.getBuilderArguments(imageClassLoader))); objectFile.newUserDefinedSection(".debug.svm.imagebuild.java.properties", makeSectionImpl.apply(DiagnosticUtils.getBuilderProperties())); - Path svmDebugHelper = Path.of(System.getProperty("java.home"), "lib/svm/debug/svmhelpers.py"); + Path svmDebugHelper = Path.of(System.getProperty("java.home"), "lib/svm/debug/gdb-debughelpers.py"); if (Files.exists(svmDebugHelper)) { objectFile.newUserDefinedSection(".debug_gdb_scripts", makeGDBSectionImpl.get()); } diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/image/NativeImageViaCC.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/image/NativeImageViaCC.java index c3f9f2a8e16a..53e4149885fd 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/image/NativeImageViaCC.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/image/NativeImageViaCC.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 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 @@ -167,6 +167,12 @@ private void runLinkerCommand(String imageName, LinkerInvocation inv, List testClass; + public static Class testClass2; + public static Object testObj; + public static Object testObj2; + public static Method instanceMethod; + public static Method instanceMethod2; + + static { + try { + Path path = Paths.get(System.getProperty("svm.test.missing.classes")); + URL[] urls = new URL[]{path.toUri().toURL()}; + try (URLClassLoader classLoader = new URLClassLoader("testClassLoader", urls, ClassLoaderTest.class.getClassLoader())) { + testClass = classLoader.loadClass("com.oracle.svm.test.missing.classes.TestClass"); + testObj = testClass.getConstructor().newInstance(); + instanceMethod = testClass.getDeclaredMethod("instanceMethod"); + } catch (IOException | ClassNotFoundException | NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + // load the same class from two different classLoaders + try (URLClassLoader classLoader = new URLClassLoader(urls, ClassLoaderTest.class.getClassLoader())) { + testClass2 = classLoader.loadClass("com.oracle.svm.test.missing.classes.TestClass"); + testObj2 = testClass2.getConstructor().newInstance(); + instanceMethod2 = testClass2.getDeclaredMethod("instanceMethod"); + } catch (IOException | ClassNotFoundException | NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + } + + public static void main(String[] args) { + try { + System.out.println(instanceMethod.invoke(testObj)); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + try { + System.out.println(instanceMethod2.invoke(testObj2)); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } +} diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/debug/helper/PrettyPrinterTest.java b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/debug/helper/PrettyPrinterTest.java new file mode 100644 index 000000000000..12d77d16acc5 --- /dev/null +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/debug/helper/PrettyPrinterTest.java @@ -0,0 +1,167 @@ +/* + * 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. + */ +package com.oracle.svm.test.debug.helper; + +import com.oracle.svm.core.NeverInline; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class PrettyPrinterTest { + + enum Day { + Monday, + Tuesday, + Wednesday, + Thursday, + Friday, + Saturday, + Sunday + } + + static class ExampleClass { + public static String s1 = ident("test"); + + public long f1; + public int f2; + public short f3; + public char f4; + public byte f5; + public boolean f6; + public String f7; + public Day f8; + public Object f9; + public ExampleClass f10; + + ExampleClass(long f1, int f2, short f3, char f4, byte f5, boolean f6, String f7, Day f8, Object f9, ExampleClass f10) { + super(); + this.f1 = f1; + this.f2 = f2; + this.f3 = f3; + this.f4 = f4; + this.f5 = f5; + this.f6 = f6; + this.f7 = f7; + this.f8 = f8; + this.f9 = f9; + this.f10 = f10; + } + + ExampleClass() { + this(0, 1, (short) 2, '3', (byte) 4, false, "test string", Day.Monday, new Object(), null); + } + + @Override + public String toString() { + return "ExampleClass{" + + "f1=" + f1 + + ", f2=" + f2 + + ", f3=" + f3 + + ", f4=" + f4 + + ", f5=" + f5 + + ", f6=" + f6 + + ", f7='" + f7 + '\'' + + ", f8=" + f8 + + ", f9=" + f9 + + ", f10=" + f10 + + ", s1=" + s1 + + '}'; + } + + @NeverInline("For testing purposes") + private static String ident(String s) { + return s; + } + } + + @SuppressWarnings("unused") + @NeverInline("For testing purposes") + static void testPrimitive(byte b, Byte bObj, short s, Short sObj, char c, Character cObj, int i, Integer iObj, long l, Long lObj, + float f, Float fObj, double d, Double dObj, boolean x, Boolean xObj) { + System.out.print(""); + } + + @SuppressWarnings("unused") + @NeverInline("For testing purposes") + static void testString(String nullStr, String emptyStr, String str, String uStr1, String uStr2, String uStr3, String uStr4, String uStr5, String str0) { + System.out.print(""); + } + + @SuppressWarnings("unused") + @NeverInline("For testing purposes") + static void testArray(int[] ia, Object[] oa, String[] sa) { + System.out.print(""); + } + + @SuppressWarnings("unused") + @NeverInline("For testing purposes") + static void testObject(ExampleClass object, ExampleClass recObject) { + System.out.print(""); + } + + @SuppressWarnings("unused") + @NeverInline("For testing purposes") + static void testArrayList(ArrayList strList, List mixedList, ArrayList nullList) { + System.out.print(""); + } + + @SuppressWarnings("unused") + @NeverInline("For testing purposes") + static void testHashMap(HashMap strMap, Map mixedMap) { + System.out.print(""); + } + + static ExampleClass setupExampleObject(boolean recursive) { + ExampleClass example = new ExampleClass(); + example.f10 = new ExampleClass(10, 20, (short) 30, '\40', (byte) 50, true, "60", Day.Sunday, new Object(), null); + example.f9 = new ArrayList<>(List.of(example.f10, new ExampleClass())); + if (recursive) { + example.f10.f10 = example; // indirect recursion + example.f10 = example; // direct recursion + } + return example; + } + + public static void main(String[] args) { + testPrimitive((byte) 1, (byte) 1, (short) 2, (short) 2, '3', '3', 4, 4, 5L, 5L, + 6.125F, 6.125F, 7.25, 7.25, true, true); + // Checkstyle: stop + testString(null, "", "string", "Привет Java", "Բարեւ Java", "你好的 Java", "こんにちは Java", "𝄞и𝄞и𝄞и𝄞и𝄞", "first " + '\0' + "second"); + // Checkstyle: resume + testArray(new int[]{0, 1, 2, 3}, new Object[]{0, "random", new Object(), new ArrayList()}, new String[]{"this", "is", "a", "string", "array"}); + testObject(setupExampleObject(false), setupExampleObject(true)); + + ArrayList nullList = new ArrayList<>(); + nullList.add(null); + nullList.add(null); + nullList.add(null); + + testArrayList(new ArrayList<>(List.of("this", "is", "a", "string", "list")), new ArrayList<>(List.of(1, 2L, "string")), nullList); + testHashMap(new HashMap<>(Map.of("this", "one", "is", "two", "a", "three", "string", "four", "list", "five")), + new HashMap<>(Map.of(1, new ExampleClass(), 2L, "string", (byte) 3, new ArrayList<>()))); + } +} diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/debug/helper/gdb_helper.py b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/debug/helper/gdb_helper.py new file mode 100644 index 000000000000..588da04703e1 --- /dev/null +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/debug/helper/gdb_helper.py @@ -0,0 +1,205 @@ +# +# 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. +# +# pylint: skip-file +from __future__ import absolute_import, division, print_function + +import logging +import sys +import re +import gdb + +logging.basicConfig(format='%(name)s %(levelname)s: %(message)s') +logger = logging.getLogger('[DebugTest]') + + +int_rexp = re.compile(r"\d+") +float_rexp = re.compile(r"\d+(.\d+)?") +char_rexp = re.compile(r"'(.|\\.+)'") +string_rexp = re.compile(r'(null|".*")') +boolean_rexp = re.compile(r"(true|false)") +array_rexp = re.compile(r'.+\[\d+]\s*=\s*{.*}') +args_rexp = re.compile(r'.*\(.*\)\s*\((?P.*)\)') +hex_rexp = re.compile(r"[\da-fA-F]+") + + +def gdb_execute(command: str) -> str: + gdb.flush() + logger.info(f'(gdb) {command}') + exec_string = gdb.execute(command, False, True) + try: + gdb.execute("py SVMUtil.prompt_hook()", False, False) # inject prompt hook for autonomous tests (will otherwise not be called) + except gdb.error: + logger.debug("Could not execute prompt hook (SVMUtil was not yet loaded)") # SVMUtil not yet loaded + gdb.flush() + return exec_string + + +def clear_pretty_printers() -> None: + logger.info('Clearing pretty printers from objfiles') + for objfile in gdb.objfiles(): + objfile.pretty_printers = [] + try: + gdb_execute('py SVMUtil.pretty_print_objfiles.clear()') # SVMUtil is not visible here - let gdb handle this + except gdb.error: + logger.debug("SVMUtil was not yet loaded") # SVMUtil not yet loaded + + +def gdb_reload_executable() -> None: + logger.info('Reloading main executable') + filename = gdb.objfiles()[0].filename + gdb_execute('file') # remove executable to clear symbols + gdb_execute(f'file {filename}') + + +def gdb_output(var: str, output_format: str = None) -> str: + logger.info(f'Print variable {var}') + return gdb_execute('output{} {}'.format("" if output_format is None else "/" + output_format, var)) + + +def gdb_print(var: str, output_format: str = None) -> str: + logger.info(f'Print variable {var}') + return gdb_execute('print{} {}'.format("" if output_format is None else "/" + output_format, var)) + + +def gdb_advanced_print(var: str, output_format: str = None) -> str: + logger.info(f'Print variable {var}') + return gdb_execute('p{} {}'.format("" if output_format is None else "/" + output_format, var)) + + +def gdb_set_breakpoint(location: str) -> None: + logger.info(f"Setting breakpoint at: {location}") + gdb_execute(f"break {location}") + + +def gdb_set_param(name: str, value: str) -> None: + logger.info(f"Setting parameter '{name}' to '{value}'") + gdb.set_parameter(name, value) + + +def gdb_get_param(name: str) -> str: + logger.info(f"Fetching parameter '{name}'") + return gdb.parameter(name) + + +def gdb_delete_breakpoints() -> None: + logger.info("Deleting all breakpoints") + gdb_execute("delete breakpoints") + + +def gdb_run() -> None: + logger.info('Run current program') + gdb_execute('run') + + +def gdb_start() -> None: + logger.info('start current program') + gdb_execute('start') + + +def gdb_kill() -> None: + logger.info('Kill current program') + try: + gdb_execute('kill') + except gdb.error: + pass # no running program + + +def gdb_continue() -> None: + logger.info('Continue current program') + gdb_execute('continue') + + +def gdb_step() -> None: + logger.info('Sourceline STEP') + gdb_execute('step') + + +def gdb_next() -> None: + logger.info('Sourceline NEXT') + gdb_execute('next') + + +def gdb_step_i() -> None: + logger.info('Machine instruction STEP') + gdb_execute('stepi') + + +def gdb_next_i() -> None: + logger.info('Machine instruction NEXT') + gdb_execute('nexti') + + +def gdb_finish() -> None: + logger.info('Function FINISH') + gdb_execute('finish') + + +def set_up_test() -> None: + logger.info('Set up gdb') + # gdb setup + gdb.set_parameter('print array', 'off') # enforce compact format + gdb.set_parameter('print pretty', 'off') # enforce compact format + gdb.set_parameter('confirm', 'off') # ensure we can exit easily + gdb.set_parameter('height', 'unlimited') # sane console output + gdb.set_parameter('width', 'unlimited') + gdb.set_parameter('pagination', 'off') + gdb.set_parameter('overload-resolution', 'off') + + +def set_up_gdb_debughelpers() -> None: + logger.info('Set up gdb-debughelpers') + # gdb-debughelpers setup + gdb.set_parameter('svm-print', 'on') + gdb.set_parameter('svm-print-string-limit', '200') + gdb.set_parameter('svm-print-element-limit', '10') + gdb.set_parameter('svm-print-field-limit', '50') + gdb.set_parameter('svm-print-depth-limit', '1') + gdb.set_parameter('svm-use-hlrep', 'on') + gdb.set_parameter('svm-infer-generics', '10') + gdb.set_parameter('svm-print-address', 'off') + gdb.set_parameter('svm-selfref-check', 'on') + gdb.set_parameter('svm-print-static-fields', 'off') + gdb.set_parameter('svm-complete-static-variables', 'off') + + # set gdb limits for gdb-debughelpers + gdb.set_parameter('print max-depth', '10') + gdb.set_parameter('print elements', '100') + gdb.set_parameter('print characters', '200') + + +def tear_down_test() -> None: + logger.info('Tear down') + sys.exit(0) + + +def dump_debug_context() -> None: + logger.info('Dump Debugger Context Begin') + gdb_execute('print $pc') + gdb_execute('info scope *$pc') + gdb_execute('info args') + gdb_execute('info locals') + gdb_execute('list') + gdb_execute('disassemble') + logger.info('Dump Debugger Context End') diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/debug/helper/test_cinterface.py b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/debug/helper/test_cinterface.py new file mode 100644 index 000000000000..0d304dabb4a8 --- /dev/null +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/debug/helper/test_cinterface.py @@ -0,0 +1,116 @@ +# +# Copyright (c) 2024, 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. +# + +import os +import sys +import unittest + +# add test directory to path to allow import of gdb_helper.py +sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)))) + +from gdb_helper import * + + +class TestLoadPrettyPrinter(unittest.TestCase): + @classmethod + def setUp(cls): + set_up_test() + clear_pretty_printers() + gdb_reload_executable() + + @classmethod + def tearDown(cls): + gdb_delete_breakpoints() + gdb_kill() + + def test_auto_load(self): + gdb_start() + exec_string = gdb_execute('info pretty-printer') + self.assertIn("SubstrateVM", exec_string, 'pretty-printer was not loaded') + # assume that there are no other pretty-printers were attached to an objfile + self.assertIn("objfile", exec_string, 'pretty-printer was not attached to an objfile') + + def test_manual_load(self): + backup_auto_load_param = gdb_get_param("auto-load python-scripts") + gdb_set_param("auto-load python-scripts", "off") + gdb_start() + try: + exec_string = gdb_execute('info pretty-printer') + self.assertNotIn("objfile", exec_string, "No objfile pretty printer should be loaded yet") + gdb_execute('source gdb-debughelpers.py') + exec_string = gdb_execute('info pretty-printer') + self.assertIn("objfile", exec_string) # check if any objfile has a pretty-printer + self.assertIn("SubstrateVM", exec_string) + finally: + # make sure auto-loading is re-enabled for other tests + gdb_set_param("auto-load python-scripts", backup_auto_load_param) + + def test_manual_load_without_executable(self): + self.assertIn('AssertionError', gdb_execute("source gdb-debughelpers.py")) + + def test_auto_reload(self): + gdb_start() + gdb_start() # all loaded shared libraries get freed (their pretty printers are removed) and newly attached + exec_string = gdb_execute('info pretty-printer') + self.assertIn("SubstrateVM", exec_string, 'pretty-printer was not loaded') + self.assertIn("objfile", exec_string, 'pretty-printer was not attached to an objfile') + + +class TestCInterface(unittest.TestCase): + @classmethod + def setUp(cls): + cls.maxDiff = None + set_up_test() + gdb_start() + set_up_gdb_debughelpers() + + @classmethod + def tearDown(cls): + gdb_delete_breakpoints() + gdb_kill() + + def test_print_from_c(self): + gdb_set_breakpoint("com.oracle.svm.tutorial.CInterfaceTutorial::releaseData") + gdb_continue() + gdb_set_breakpoint("org.graalvm.nativeimage.ObjectHandles::getGlobal") + gdb_continue() + gdb_finish() + gdb_next() + self.assertTrue(gdb_output('javaObject').startswith('"Hello World at')) + + def test_print_from_java_shared_libray(self): + gdb_set_breakpoint("com.oracle.svm.tutorial.CInterfaceTutorial::dump") + gdb_continue() + exec_string = gdb_output("data") + self.assertTrue(exec_string.startswith('my_data = {'), f"GDB output: '{exec_string}'") + self.assertIn('f_primitive = 42', exec_string) + self.assertIn('f_array = int32_t [4] = {...}', exec_string) + self.assertRegex(exec_string, f'f_cstr = 0x{hex_rexp.pattern} "Hello World"') + self.assertRegex(exec_string, f'f_java_object_handle = 0x{hex_rexp.pattern}') + self.assertRegex(exec_string, f'f_print_function = 0x{hex_rexp.pattern} ') + + +# redirect unittest output to terminal +unittest.main(testRunner=unittest.TextTestRunner(stream=sys.__stdout__)) diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/debug/helper/test_class_loader.py b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/debug/helper/test_class_loader.py new file mode 100644 index 000000000000..e2fc5db77e19 --- /dev/null +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/debug/helper/test_class_loader.py @@ -0,0 +1,89 @@ +# +# Copyright (c) 2024, 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. +# + +import os +import sys +import unittest + +# add test directory to path to allow import of gdb_helper.py +sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)))) + +from gdb_helper import * + + +class TestClassLoader(unittest.TestCase): + + def setUp(self): + set_up_test() + gdb_start() + set_up_gdb_debughelpers() + + def tearDown(self): + gdb_delete_breakpoints() + gdb_kill() + + def test_instanceMethod_named_classloader(self): + gdb_set_breakpoint("com.oracle.svm.test.missing.classes.TestClass::instanceMethod") + gdb_continue() # named classloader is called first in test code + self.assertRegex(gdb_output("this"), rf'testClassLoader_{hex_rexp.pattern}::com\.oracle\.svm\.test\.missing\.classes\.TestClass = {{instanceField = null}}') + gdb_output("$other=(('java.lang.Object' *)this)") + self.assertIn("null", gdb_advanced_print("$other.instanceField")) # force a typecast + + def test_instanceMethod_unnamed_classloader(self): + gdb_set_breakpoint("com.oracle.svm.test.missing.classes.TestClass::instanceMethod") + gdb_continue() # skip named classloader + gdb_continue() # unnamed classloader is called second in test code + self.assertRegex(gdb_output("this"), rf'URLClassLoader_{hex_rexp.pattern}::com\.oracle\.svm\.test\.missing\.classes\.TestClass = {{instanceField = null}}') + gdb_output("$other=(('java.lang.Object' *)this)") + self.assertIn("null", gdb_advanced_print("$other.instanceField")) # force a typecast + + +class TestClassloaderObjUtils(unittest.TestCase): + + compressed_type = gdb.lookup_type('_z_.java.lang.Object') + uncompressed_type = gdb.lookup_type('java.lang.Object') + + def setUp(self): + self.maxDiff = None + set_up_test() + set_up_gdb_debughelpers() + + def tearDown(self): + gdb_delete_breakpoints() + gdb_kill() + + def test_get_classloader_namespace(self): + gdb_set_breakpoint("com.oracle.svm.test.missing.classes.TestClass::instanceMethod") + gdb_run() + clazz = gdb.parse_and_eval("'java.lang.Object.class'") # type = java.lang.Class -> no classloader name + this = gdb.parse_and_eval('this') + field = gdb.parse_and_eval('this.instanceField') + self.assertEqual(SVMUtil.get_classloader_namespace(clazz), "") + self.assertRegex(SVMUtil.get_classloader_namespace(this), f'testClassLoader_{hex_rexp.pattern}') + self.assertEqual(SVMUtil.get_classloader_namespace(field), "") # field is null + + +# redirect unittest output to terminal +unittest.main(testRunner=unittest.TextTestRunner(stream=sys.__stdout__)) diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/debug/helper/test_pretty_printer.py b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/debug/helper/test_pretty_printer.py new file mode 100644 index 000000000000..df454f74c7aa --- /dev/null +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/debug/helper/test_pretty_printer.py @@ -0,0 +1,382 @@ +# +# 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. +# + +import os +import sys +import unittest + +# add test directory to path to allow import of gdb_helper.py +sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)))) + +from gdb_helper import * + + +class TestPrintPrimitives(unittest.TestCase): + @classmethod + def setUpClass(cls): + set_up_test() + set_up_gdb_debughelpers() + gdb_set_breakpoint("com.oracle.svm.test.debug.helper.PrettyPrinterTest::testPrimitive") + gdb_run() + + @classmethod + def tearDownClass(cls): + gdb_delete_breakpoints() + gdb_kill() + + def test_byte(self): + exec_string = gdb_output("b") + self.assertRegex(exec_string, int_rexp, f"'{exec_string}' is not a byte") + self.assertEqual(int(exec_string), 1) + + def test_boxed_byte(self): + exec_string = gdb_output("bObj") + self.assertRegex(exec_string, int_rexp, f"'{exec_string}' is not a byte") + self.assertEqual(int(exec_string), 1) + + def test_short(self): + exec_string = gdb_output("s") + self.assertRegex(exec_string, int_rexp, f"'{exec_string}' is not a short") + self.assertEqual(int(exec_string), 2) + + def test_boxed_short(self): + exec_string = gdb_output("sObj") + self.assertRegex(exec_string, int_rexp, f"'{exec_string}' is not a short") + self.assertEqual(int(exec_string), 2) + + def test_char(self): + exec_string = gdb_output("c") + self.assertRegex(exec_string, char_rexp, f"'{exec_string}' is not a char") + self.assertEqual(exec_string, "'3'") + + def test_boxed_char(self): + exec_string = gdb_output("cObj") + self.assertRegex(exec_string, char_rexp, f"'{exec_string}' is not a char") + self.assertEqual(exec_string, "'3'") + + def test_int(self): + exec_string = gdb_output("i") + self.assertRegex(exec_string, int_rexp, f"'{exec_string}' is not a int") + self.assertEqual(int(exec_string), 4) + + def test_boxed_int(self): + exec_string = gdb_output("iObj") + self.assertRegex(exec_string, int_rexp, f"'{exec_string}' is not a int") + self.assertEqual(int(exec_string), 4) + + def test_long(self): + exec_string = gdb_output("l") + self.assertRegex(exec_string, int_rexp, f"'{exec_string}' is not a long") + self.assertEqual(int(exec_string), 5) + + def test_boxed_long(self): + exec_string = gdb_output("lObj") + self.assertRegex(exec_string, int_rexp, f"'{exec_string}' is not a long") + self.assertEqual(int(exec_string), 5) + + def test_float(self): + exec_string = gdb_output("f") + self.assertRegex(exec_string, float_rexp, f"'{exec_string}' is not a float") + self.assertEqual(float(exec_string), 6.125) + + def test_boxed_float(self): + exec_string = gdb_output("fObj") + self.assertRegex(exec_string, float_rexp, f"'{exec_string}' is not a float") + self.assertEqual(float(exec_string), 6.125) + + def test_double(self): + exec_string = gdb_output("d") + self.assertRegex(exec_string, float_rexp, f"'{exec_string}' is not a double") + self.assertEqual(float(exec_string), 7.25) + + def test_boxed_double(self): + exec_string = gdb_output("dObj") + self.assertRegex(exec_string, float_rexp, f"'{exec_string}' is not a double") + self.assertEqual(float(exec_string), 7.25) + + def test_boolean(self): + exec_string = gdb_output("x") + self.assertRegex(exec_string, boolean_rexp, f"'{exec_string}' is not a boolean") + self.assertEqual(bool(exec_string), True) + + def test_boxed_boolean(self): + exec_string = gdb_output("xObj") + self.assertRegex(exec_string, boolean_rexp, f"'{exec_string}' is not a boolean") + self.assertEqual(bool(exec_string), True) + + +class TestPrintStrings(unittest.TestCase): + @classmethod + def setUpClass(cls): + set_up_test() + set_up_gdb_debughelpers() + gdb_set_breakpoint("com.oracle.svm.test.debug.helper.PrettyPrinterTest::testString") + gdb_run() + + @classmethod + def tearDownClass(cls): + gdb_delete_breakpoints() + gdb_kill() + + def test_null(self): + exec_string = gdb_output("nullStr") + self.assertRegex(exec_string, string_rexp, f"'{exec_string}' is not a string") + self.assertEqual(exec_string, "null") + + def test_empty_string(self): + exec_string = gdb_output("emptyStr") + self.assertRegex(exec_string, string_rexp, f"'{exec_string}' is not a string") + self.assertEqual(exec_string, '""') + + def test_nonempty_string(self): + exec_string = gdb_output("str") + self.assertRegex(exec_string, string_rexp, f"'{exec_string}' is not a string") + self.assertEqual(exec_string, '"string"') + + def test_unicode_strings(self): + exec_string = gdb_output("uStr1") + self.assertRegex(exec_string, string_rexp, f"'{exec_string}' is not a string") + self.assertEqual(exec_string, '"Привет Java"') + exec_string = gdb_output("uStr2") + self.assertRegex(exec_string, string_rexp, f"'{exec_string}' is not a string") + self.assertEqual(exec_string, '"Բարեւ Java"') + exec_string = gdb_output("uStr3") + self.assertRegex(exec_string, string_rexp, f"'{exec_string}' is not a string") + self.assertEqual(exec_string, '"你好的 Java"') + exec_string = gdb_output("uStr4") + self.assertRegex(exec_string, string_rexp, f"'{exec_string}' is not a string") + self.assertEqual(exec_string, '"こんにちは Java"') + exec_string = gdb_output("uStr5") + self.assertRegex(exec_string, string_rexp, f"'{exec_string}' is not a string") + self.assertEqual(exec_string, '"𝄞и𝄞и𝄞и𝄞и𝄞"') + + def test_string_containing_0(self): + exec_string = gdb_output("str0") + self.assertRegex(exec_string, string_rexp, f"'{exec_string}' is not a string") + self.assertEqual(exec_string, r'"first \0second"') + + +class TestPrintArrays(unittest.TestCase): + @classmethod + def setUpClass(cls): + set_up_test() + set_up_gdb_debughelpers() + gdb_set_breakpoint("com.oracle.svm.test.debug.helper.PrettyPrinterTest::testArray") + gdb_run() + + @classmethod + def tearDownClass(cls): + gdb_delete_breakpoints() + gdb_kill() + + def test_primitive_array(self): + exec_string = gdb_output("ia") + self.assertRegex(exec_string, array_rexp, f"'{exec_string}' is not an array") + self.assertEqual(exec_string, "int [4] = {0, 1, 2, 3}") + + def test_string_array(self): + exec_string = gdb_output("sa") + self.assertRegex(exec_string, array_rexp, f"'{exec_string}' is not an array") + self.assertEqual(exec_string, 'java.lang.String[5] = {"this", "is", "a", "string", "array"}') + + def test_object_array(self): + exec_string = gdb_output("oa") + self.assertRegex(exec_string, array_rexp, f"'{exec_string}' is not an array") + self.assertEqual(exec_string, 'java.lang.Object[4] = {0, "random", java.lang.Object = {...}, ' + 'java.util.ArrayList(0) = {...}}') + + +class TestPrintFunctionHeader(unittest.TestCase): + @classmethod + def setUpClass(cls): + set_up_test() + set_up_gdb_debughelpers() + + @classmethod + def tearDown(cls): + gdb_delete_breakpoints() + gdb_kill() + + def test_primitive_arguments(self): + gdb_set_breakpoint("com.oracle.svm.test.debug.helper.PrettyPrinterTest::testPrimitive") + gdb_run() + exec_string = gdb_execute("frame") + self.assertRegex(exec_string, args_rexp, f"Could not find args in '{exec_string}'") + args = args_rexp.match(exec_string).group("args") + self.assertIn("b=1", args) + self.assertIn("bObj=1", args) + self.assertIn("s=2", args) + self.assertIn("sObj=2", args) + self.assertIn("c='3'", args) + self.assertIn("cObj='3'", args) + self.assertIn("i=4", args) + self.assertIn("iObj=4", args) + self.assertIn("l=5", args) + self.assertIn("lObj=5", args) + self.assertIn("f=6.125", args) + self.assertIn("fObj=6.125", args) + self.assertIn("d=7.25", args) + self.assertIn("dObj=7.25", args) + self.assertIn("x=true", args) + self.assertIn("xObj=true", args) + + def test_string_arguments(self): + gdb_set_breakpoint("com.oracle.svm.test.debug.helper.PrettyPrinterTest::testString") + gdb_run() + exec_string = gdb_execute("frame") + self.assertRegex(exec_string, args_rexp, f"Could not find args in '{exec_string}'") + args = args_rexp.match(exec_string).group("args") + self.assertIn(r'nullStr=null', args) + self.assertIn(r'emptyStr=""', args) + self.assertIn(r'str="string"', args) + self.assertIn(r'uStr1="Привет Java"', args) + self.assertIn(r'uStr2="Բարեւ Java"', args) + self.assertIn(r'uStr3="你好的 Java"', args) + self.assertIn(r'uStr4="こんにちは Java"', args) + self.assertIn(r'uStr5="𝄞и𝄞и𝄞и𝄞и𝄞"', args) + self.assertIn(r'str0="first \0second"', args) + + def test_array_arguments(self): + gdb_set_breakpoint("com.oracle.svm.test.debug.helper.PrettyPrinterTest::testArray") + gdb_run() + exec_string = gdb_execute("frame") + self.assertRegex(exec_string, args_rexp, f"Could not find args in '{exec_string}'") + args = args_rexp.match(exec_string).group("args") + self.assertIn('ia=int [4] = {...}', args) + self.assertIn('oa=java.lang.Object[4] = {...}', args) + self.assertIn('sa=java.lang.String[5] = {...}', args) + + +class TestPrintClassMembers(unittest.TestCase): + @classmethod + def setUpClass(cls): + set_up_test() + set_up_gdb_debughelpers() + gdb_set_breakpoint("com.oracle.svm.test.debug.helper.PrettyPrinterTest::testObject") + gdb_run() + + @classmethod + def tearDownClass(cls): + gdb_delete_breakpoints() + gdb_kill() + + def test_all_members(self): + self.maxDiff = None + gdb_set_param("svm-print-depth", "2") + exec_string = gdb_output("object") + self.assertTrue(exec_string.startswith("com.oracle.svm.test.debug.helper.PrettyPrinterTest$ExampleClass = {"), f"GDB output: '{exec_string}'") + self.assertIn('f1 = 0', exec_string) + self.assertIn('f2 = 1', exec_string) + self.assertIn('f3 = 2', exec_string) + self.assertIn("f4 = '3'", exec_string) + self.assertIn('f5 = 4', exec_string) + self.assertIn('f6 = false', exec_string) + self.assertIn('f7 = "test string"', exec_string) + self.assertIn('f8 = Monday(0)', exec_string) + self.assertIn('f9 = java.util.ArrayList(2) = {com.oracle.svm.test.debug.helper.PrettyPrinterTest$ExampleClass = {...}, com.oracle.svm.test.debug.helper.PrettyPrinterTest$ExampleClass = {...}}', exec_string) + self.assertIn('f10 = com.oracle.svm.test.debug.helper.PrettyPrinterTest$ExampleClass = {', exec_string) + self.assertIn('f1 = 10', exec_string) + self.assertIn('f2 = 20', exec_string) + self.assertIn('f3 = 30', exec_string) + self.assertIn("f4 = ' '", exec_string) + self.assertIn('f5 = 50', exec_string) + self.assertIn('f6 = true', exec_string) + self.assertIn('f7 = "60"', exec_string) + self.assertIn('f8 = Sunday(6)', exec_string) + self.assertIn('f9 = java.lang.Object', exec_string) + self.assertIn('f10 = null', exec_string) + + def test_all_members_recursive(self): + gdb_set_param("svm-print-depth", "2") + exec_string = gdb_output("recObject") + self.assertTrue(exec_string.startswith("com.oracle.svm.test.debug.helper.PrettyPrinterTest$ExampleClass = {"), f"GDB output: '{exec_string}'") + self.assertIn('f1 = 0', exec_string) + self.assertIn('f2 = 1', exec_string) + self.assertIn('f3 = 2', exec_string) + self.assertIn("f4 = '3'", exec_string) + self.assertIn('f5 = 4', exec_string) + self.assertIn('f6 = false', exec_string) + self.assertIn('f7 = "test string"', exec_string) + self.assertIn('f8 = Monday(0)', exec_string) + self.assertIn('f9 = java.util.ArrayList(2) = {com.oracle.svm.test.debug.helper.PrettyPrinterTest$ExampleClass = {...}, com.oracle.svm.test.debug.helper.PrettyPrinterTest$ExampleClass = {...}}', exec_string) + # {...} due to self-reference + self.assertIn('f10 = com.oracle.svm.test.debug.helper.PrettyPrinterTest$ExampleClass = {...}', exec_string) + + +class TestPrintCollections(unittest.TestCase): + @classmethod + def setUp(cls): + set_up_test() + set_up_gdb_debughelpers() + + @classmethod + def tearDown(cls): + gdb_delete_breakpoints() + gdb_kill() + + def test_string_array_list(self): + gdb_set_breakpoint("com.oracle.svm.test.debug.helper.PrettyPrinterTest::testArrayList") + gdb_run() + exec_string = gdb_output("strList") + self.assertEqual(exec_string, 'java.util.ArrayList(5) = {"this", "is", "a", "string", "list"}') + + def test_mixed_array_list(self): + gdb_set_breakpoint("com.oracle.svm.test.debug.helper.PrettyPrinterTest::testArrayList") + gdb_run() + exec_string = gdb_output("mixedList") + self.assertEqual(exec_string, 'java.util.ArrayList(3) = {1, 2, "string"}') + + def test_null_array_list(self): + gdb_set_breakpoint("com.oracle.svm.test.debug.helper.PrettyPrinterTest::testArrayList") + gdb_run() + exec_string = gdb_output("nullList") + self.assertEqual(exec_string, 'java.util.ArrayList(3) = {null, null, null}') + + def test_string_string_hash_map(self): + gdb_set_breakpoint("com.oracle.svm.test.debug.helper.PrettyPrinterTest::testHashMap") + gdb_run() + exec_string = gdb_output("strMap") + # there is no guarantee that the hashes are always the same -> just check if entries are contained in the output + self.assertTrue(exec_string.startswith('java.util.HashMap(5) = {'), f"GDB output: '{exec_string}'") + self.assertIn('["this"] = "one"', exec_string) + self.assertIn('["is"] = "two"', exec_string) + self.assertIn('["a"] = "three"', exec_string) + self.assertIn('["string"] = "four"', exec_string) + self.assertIn('["list"] = "five"', exec_string) + self.assertTrue(exec_string.endswith('}')) + + def test_mixed_hash_map(self): + gdb_set_breakpoint("com.oracle.svm.test.debug.helper.PrettyPrinterTest::testHashMap") + gdb_run() + exec_string = gdb_output("mixedMap") + self.assertTrue(exec_string.startswith('java.util.HashMap(3) = {'), f"GDB output: '{exec_string}'") + self.assertIn('[1] = com.oracle.svm.test.debug.helper.PrettyPrinterTest$ExampleClass = {...}', exec_string) + self.assertIn('[2] = "string"', exec_string) + self.assertIn('[3] = java.util.ArrayList(0) = {...}', exec_string) + self.assertTrue(exec_string.endswith('}')) + + +# redirect unittest output to terminal +unittest.main(testRunner=unittest.TextTestRunner(stream=sys.__stdout__)) diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/debug/helper/test_settings.py b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/debug/helper/test_settings.py new file mode 100644 index 000000000000..377699ccfb63 --- /dev/null +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/debug/helper/test_settings.py @@ -0,0 +1,281 @@ +# +# Copyright (c) 2024, 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. +# + +import os +import sys +import unittest +import gdb + +# add test directory to path to allow import of gdb_helper.py +sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)))) + +from gdb_helper import * + + +class TestCustomBreakCommand(unittest.TestCase): + def setUp(self): + self.maxDiff = None + set_up_test() + set_up_gdb_debughelpers() + + def tearDown(self): + gdb_delete_breakpoints() + gdb_kill() + + def test_replace_hash(self): + svm_command_break = SVMCommandBreak() + svm_command_break.invoke("com.oracle.svm.test.debug.helper.PrettyPrinterTest::testObject", False) + self.assertGreater(len(gdb.breakpoints()), 0) + self.assertEqual(gdb.breakpoints()[-1].locations[0].function, "_ZN50com.oracle.svm.test.debug.helper.PrettyPrinterTest10testObjectEJvP63com.oracle.svm.test.debug.helper.PrettyPrinterTest$ExampleClassS1_") + + +class TestCustomPrintCommand(unittest.TestCase): + + svm_command_print = SVMCommandPrint() + + def setUp(self): + self.maxDiff = None + set_up_test() + set_up_gdb_debughelpers() + + def tearDown(self): + gdb_delete_breakpoints() + gdb_kill() + + def test_print(self): + gdb_set_breakpoint("com.oracle.svm.test.debug.helper.PrettyPrinterTest::testObject") + gdb_run() + self.assertEqual(gdb_advanced_print("object").split('=', 2)[-1], gdb_print("object").split('=', 2)[-1]) + self.assertEqual(gdb_advanced_print("object", 'r').split('=', 2)[-1], gdb_print("object", 'r').split('=', 2)[-1]) + + def test_print_array(self): + gdb_set_breakpoint("com.oracle.svm.test.debug.helper.PrettyPrinterTest::testArray") + gdb_run() + + # check if expansion works and does not interfere with manual expansion + self.assertEqual(gdb_advanced_print("oa[1]").split('=', 2)[-1], gdb_print("oa.data[1]").split('=', 2)[-1]) + self.assertEqual(gdb_advanced_print("oa[1]").split('=', 2)[-1], gdb_advanced_print("oa.data[1]").split('=', 2)[-1]) + + # oa is of type Object[], thus the elements static type is Object, 'p' applies the type cast to the rtt + self.assertNotEqual(gdb_advanced_print("oa[1]", 'r').split('=', 2)[-1], gdb_print("oa.data[1]", 'r').split('=', 2)[-1]) + self.assertIn("(_z_.java.lang.String *)", gdb_advanced_print("oa[1]", 'r')) + + # check a more complex expansion + self.assertEqual(gdb_advanced_print("oa[1].value[1]").split('=', 2)[-1], gdb_print("(('_z_.java.lang.String' *)oa.data[1]).value.data[1]").split('=', 2)[-1]) + + # check a nested expansion + self.assertEqual(gdb_advanced_print("oa[ia[ia[oa[0]+1]]]").split('=', 2)[-1], gdb_print("oa.data[1]").split('=', 2)[-1]) + + def test_print_function_call(self): + gdb_set_breakpoint("com.oracle.svm.test.debug.helper.PrettyPrinterTest::testArray") + gdb_run() + self.assertEqual(gdb_advanced_print("oa[1].charAt(0)").split('=', 2)[-1], gdb_print("(('java.lang.String')(*oa.data[1])).charAt(0)").split('=', 2)[-1]) + self.assertEqual(gdb_advanced_print("oa[1].endsWith(oa[1])").split('=', 2)[-1], gdb_print("(('java.lang.String')(*oa.data[1])).endsWith(&(('java.lang.String')(*oa.data[1])))").split('=', 2)[-1]) + self.assertIn('true', gdb_advanced_print("oa[1].endsWith(oa[1])")) + + def test_print_non_java(self): + gdb_start() + self.assertEqual(gdb_advanced_print('"test"').split('=', 2)[-1], gdb_print('"test"').split('=', 2)[-1]) # char* + self.assertEqual(gdb_advanced_print('{1,2,3,4}').split('=', 2)[-1], gdb_print('{1,2,3,4}').split('=', 2)[-1]) # int[4] + + def test_print_formatted(self): + gdb_set_breakpoint("com.oracle.svm.test.debug.helper.PrettyPrinterTest::testArray") + gdb_run() + self.assertNotEqual(gdb_advanced_print("oa[1]", 'r').split('=', 2)[-1], gdb_advanced_print("oa[1]").split('=', 2)[-1]) + self.assertIn("(_z_.java.lang.String *)", gdb_advanced_print("oa[1]", 'r')) + self.assertNotIn("(_z_.java.lang.String *)", gdb_advanced_print("oa[1]")) + + def test_auto_complete_empty(self): + gdb_start() + self.assertEqual(self.svm_command_print.complete("", ""), gdb.COMPLETE_EXPRESSION) + + def test_auto_complete_array(self): + gdb_set_breakpoint("com.oracle.svm.test.debug.helper.PrettyPrinterTest::testArray") + gdb_run() + self.assertEqual(self.svm_command_print.complete("ia[", ""), ["0]", "1]", "3]"]) + self.assertEqual(self.svm_command_print.complete("ia[a", ""), gdb.COMPLETE_EXPRESSION) + + def test_auto_complete_function_param(self): + gdb_set_breakpoint("com.oracle.svm.test.debug.helper.PrettyPrinterTest::testArray") + gdb_run() + self.assertEqual(self.svm_command_print.complete("sa[0].charAt(", ""), gdb.COMPLETE_EXPRESSION) + + def test_auto_complete_field_access(self): + gdb_set_breakpoint("com.oracle.svm.test.debug.helper.PrettyPrinterTest::testArray") + gdb_run() + result = self.svm_command_print.complete("oa[3].add", "") + self.assertIn('add', result) + self.assertIn('addAll', result) + self.assertIn('addFirst', result) + self.assertIn('addLast', result) + + def test_auto_complete_invalid(self): + gdb_set_breakpoint("com.oracle.svm.test.debug.helper.PrettyPrinterTest::testArray") + gdb_run() + self.assertEqual(self.svm_command_print.complete("oa[3].addNone", ""), []) + + def test_auto_complete_None(self): + gdb_set_breakpoint("com.oracle.svm.test.debug.helper.PrettyPrinterTest::testArray") + gdb_run() + self.assertEqual(self.svm_command_print.complete("oa[3]", ""), gdb.COMPLETE_NONE) + + +class TestParameters(unittest.TestCase): + @classmethod + def setUp(cls): + cls.maxDiff = None + set_up_test() + set_up_gdb_debughelpers() + + @classmethod + def tearDown(cls): + gdb_delete_breakpoints() + gdb_kill() + + def test_svm_print(self): + gdb_set_breakpoint("com.oracle.svm.test.debug.helper.PrettyPrinterTest::testObject") + gdb_run() + gdb_set_param('svm-print', 'off') + self.assertEqual(gdb_output("object"), gdb_output("object", "r")) + self.assertRaises(gdb.error, lambda: gdb_advanced_print("object.toString()", 'r')) # check if 'p' command is skipped + gdb_set_param('svm-print', 'on') + self.assertNotEqual(gdb_output("object"), gdb_output("object", "r")) + self.assertIn("(java.lang.String *)", gdb_advanced_print("object.toString()", 'r')) + + def test_svm_print_string_limit(self): + gdb_set_breakpoint("com.oracle.svm.test.debug.helper.PrettyPrinterTest::testString") + gdb_run() + gdb_set_param('svm-print-string-limit', '2') + self.assertEqual(gdb_output("str"), '"st..."') + gdb_set_param('svm-print-string-limit', 'unlimited') + self.assertEqual(gdb_output("str"), '"string"') + + def test_svm_print_element_limit(self): + gdb_set_breakpoint("com.oracle.svm.test.debug.helper.PrettyPrinterTest::testArray") + gdb_run() + gdb_set_param('svm-print-element-limit', '2') + self.assertEqual(gdb_output("ia"), "int [4] = {0, 1, ...}") + gdb_set_param('svm-print-element-limit', 'unlimited') + self.assertEqual(gdb_output("ia"), "int [4] = {0, 1, 2, 3}") + + def test_svm_print_field_limit(self): + gdb_set_breakpoint("com.oracle.svm.test.debug.helper.PrettyPrinterTest::testObject") + gdb_run() + gdb_set_param('svm-print-field-limit', '1') + self.assertIn('f7 = "test string"', gdb_output('object')) + self.assertIn('f8 = ...', gdb_output('object')) + gdb_set_param('svm-print-field-limit', 'unlimited') + self.assertIn('f7 = "test string"', gdb_output('object')) + self.assertIn('f8 = Monday(0)', gdb_output('object')) + + def test_svm_print_depth_limit(self): + gdb_set_breakpoint("com.oracle.svm.test.debug.helper.PrettyPrinterTest::testObject") + gdb_run() + gdb_set_param('svm-print-depth-limit', '0') + self.assertEqual(gdb_output('object'), "com.oracle.svm.test.debug.helper.PrettyPrinterTest$ExampleClass = {...}") + gdb_set_param('svm-print-depth-limit', '2') + self.assertIn("f9 = java.util.ArrayList(2) = {com.oracle.svm.test.debug.helper.PrettyPrinterTest$ExampleClass = {...}, com.oracle.svm.test.debug.helper.PrettyPrinterTest$ExampleClass = {...}}", gdb_output('object')) + + def test_svm_use_hlrep(self): + gdb_set_breakpoint("com.oracle.svm.test.debug.helper.PrettyPrinterTest::testArrayList") + gdb_run() + gdb_set_param('svm-use-hlrep', 'off') + exec_string = gdb_output("strList") + self.assertTrue(exec_string.startswith('java.util.ArrayList = {')) + self.assertIn('modCount = 0', exec_string) + self.assertIn('size = 5', exec_string) + self.assertIn('elementData = java.lang.Object[5] = {...}', exec_string) + gdb_set_param('svm-use-hlrep', 'on') + self.assertEqual(gdb_output("strList"), 'java.util.ArrayList(5) = {"this", "is", "a", "string", "list"}') + + def test_svm_infer_generics(self): + gdb_set_breakpoint("com.oracle.svm.test.debug.helper.PrettyPrinterTest::testArrayList") + gdb_run() + gdb_set_param('svm-infer-generics', '0') + self.assertEqual(gdb_output("mixedList"), 'java.util.ArrayList(3) = {1, 2, "string"}') + gdb_set_param('svm-infer-generics', '1') + self.assertEqual(gdb_output("mixedList"), 'java.util.ArrayList(3) = {1, 2, "string"}') + gdb_set_param('svm-infer-generics', '2') + self.assertEqual(gdb_output("mixedList"), 'java.util.ArrayList(3) = {1, 2, "string"}') + gdb_set_param('svm-infer-generics', 'unlimited') + self.assertEqual(gdb_output("mixedList"), 'java.util.ArrayList(3) = {1, 2, "string"}') + + def test_svm_print_address(self): + def assert_adr_regex(adr_regex: str = ''): + exec_string = gdb_output("object") + self.assertRegex(exec_string, rf'^com\.oracle\.svm\.test\.debug\.helper\.PrettyPrinterTest\$ExampleClass{adr_regex} = {{') + self.assertRegex(exec_string, rf'f7 = "test string"{adr_regex}') + self.assertRegex(exec_string, rf'f8 = Monday\(0\){adr_regex}') + self.assertRegex(exec_string, rf'f9 = java\.util\.ArrayList\(2\) = {{\.\.\.}}{adr_regex}') + self.assertRegex(exec_string, rf'f10 = com\.oracle\.svm\.test\.debug\.helper\.PrettyPrinterTest\$ExampleClass = {{\.\.\.}}{adr_regex}') + self.assertRegex(exec_string, r'f1 = 0[,}]') + self.assertRegex(exec_string, r'f2 = 1[,}]') + self.assertRegex(exec_string, r'f3 = 2[,}]') + self.assertRegex(exec_string, r"f4 = '3'[,}]") + self.assertRegex(exec_string, r'f5 = 4[,}]') + self.assertRegex(exec_string, r'f6 = false[,}]') + + gdb_set_breakpoint("com.oracle.svm.test.debug.helper.PrettyPrinterTest::testObject") + gdb_run() + gdb_set_param('svm-print-address', 'enable') + assert_adr_regex(f' @z?\\(0x{hex_rexp.pattern}\\)') + gdb_set_param('svm-print-address', 'absolute') + assert_adr_regex(f' @\\(0x{hex_rexp.pattern}\\)') + gdb_set_param('svm-print-address', 'disable') + assert_adr_regex() + + def test_svm_selfref_check(self): + gdb_set_breakpoint("com.oracle.svm.test.debug.helper.PrettyPrinterTest::testObject") + gdb_run() + gdb_set_param('svm-selfref-check', 'off') + gdb_set_param('svm-print-depth-limit', '4') # increase print depth to test for selfref check + # printing is only restricted by depth -> more characters are printed + exec_string1 = gdb_output("recObject") + gdb_set_param('svm-selfref-check', 'on') + exec_string2 = gdb_output("recObject") + self.assertGreater(len(exec_string1), len(exec_string2)) + + def test_svm_print_static_fields(self): + gdb_set_breakpoint("com.oracle.svm.test.debug.helper.PrettyPrinterTest::testArrayList") + gdb_run() + gdb_set_param('svm-print-static-fields', 'on') + gdb_set_param('svm-use-hlrep', 'off') + self.assertIn('DEFAULT_CAPACITY', gdb_output("strList")) + gdb_set_param('svm-print-static-fields', 'off') + self.assertNotIn('DEFAULT_CAPACITY', gdb_output("strList")) + + def test_svm_complete_static_variables(self): + svm_command_print = SVMCommandPrint() + gdb_set_breakpoint("com.oracle.svm.test.debug.helper.PrettyPrinterTest::testArrayList") + gdb_run() + gdb_set_param('svm-complete-static-variables', 'on') + gdb_set_param('svm-use-hlrep', 'off') + self.assertIn('DEFAULT_CAPACITY', svm_command_print.complete("strList.", "")) + gdb_set_param('svm-complete-static-variables', 'off') + self.assertNotIn('DEFAULT_CAPACITY', svm_command_print.complete("strList.", "")) + + +# redirect unittest output to terminal +unittest.main(testRunner=unittest.TextTestRunner(stream=sys.__stdout__)) diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/debug/helper/test_svm_util.py b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/debug/helper/test_svm_util.py new file mode 100644 index 000000000000..332bc88dd364 --- /dev/null +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/debug/helper/test_svm_util.py @@ -0,0 +1,164 @@ +# +# Copyright (c) 2024, 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. +# + +import os +import sys +import unittest +import gdb + +# add test directory to path to allow import of gdb_helper.py +sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)))) + +from gdb_helper import * + + +class TestTypeUtils(unittest.TestCase): + + compressed_type = gdb.lookup_type('_z_.java.lang.Object') + uncompressed_type = gdb.lookup_type('java.lang.Object') + + def setUp(self): + self.maxDiff = None + set_up_test() + set_up_gdb_debughelpers() + + def tearDown(self): + gdb_delete_breakpoints() + gdb_kill() + + def test_compressed_check(self): + gdb_start() + self.assertTrue(SVMUtil.is_compressed(self.compressed_type)) + self.assertFalse(SVMUtil.is_compressed(self.uncompressed_type)) + + def test_uncompress(self): + gdb_start() + self.assertEqual(SVMUtil.get_uncompressed_type(self.compressed_type), self.uncompressed_type) + + def test_uncompress_uncompressed_type(self): + gdb_start() + self.assertEqual(SVMUtil.get_uncompressed_type(self.uncompressed_type), self.uncompressed_type) + + def test_compress(self): + gdb_start() + self.assertEqual(SVMUtil.get_compressed_type(self.uncompressed_type), self.compressed_type) + + def test_compress_compressed_type(self): + gdb_start() + self.assertEqual(SVMUtil.get_compressed_type(self.compressed_type), self.compressed_type) + + def test_get_unqualified_type_name(self): + self.assertEqual(SVMUtil.get_unqualified_type_name("classloader_name::package.name.Parent$Inner"), "Inner") + + +class TestObjUtils(unittest.TestCase): + + compressed_type = gdb.lookup_type('_z_.java.lang.Object') + uncompressed_type = gdb.lookup_type('java.lang.Object') + + def setUp(self): + self.maxDiff = None + set_up_test() + set_up_gdb_debughelpers() + gdb_start() + + def tearDown(self): + gdb_delete_breakpoints() + gdb_kill() + + def test_compressed_hub_adr(self): + gdb_set_breakpoint("com.oracle.svm.test.debug.helper.PrettyPrinterTest::testArrayList") + gdb_continue() + # get a compressed hub + z_hub = gdb.parse_and_eval('strList.hub') + self.assertNotEqual(int(z_hub), 0) + self.assertTrue(SVMUtil.is_compressed(z_hub.type)) + # get the uncompressed value for the hub + hub = z_hub.dereference() + hub = hub.cast(SVMUtil.get_uncompressed_type(hub.type)) + self.assertFalse(SVMUtil.is_compressed(hub.type)) + self.assertEqual(SVMUtil.get_compressed_adr(hub), int(z_hub)) + + hub_str = str(hub) + SVMUtil.prompt_hook() + z_hub_str = str(z_hub) + SVMUtil.prompt_hook() + self.assertEqual(hub_str, z_hub_str) + + def test_compressed_adr(self): + gdb_set_breakpoint("com.oracle.svm.test.debug.helper.PrettyPrinterTest::testArrayList") + gdb_continue() + # get a compressed object + z_name = gdb.parse_and_eval('strList.hub.name') + self.assertNotEqual(int(z_name), 0) + self.assertTrue(SVMUtil.is_compressed(z_name.type)) + # get the uncompressed value for the hub name + name = z_name.dereference() + name = name.cast(SVMUtil.get_uncompressed_type(name.type)) + self.assertFalse(SVMUtil.is_compressed(name.type)) + self.assertEqual(SVMUtil.get_compressed_adr(name), int(z_name)) + + name_str = str(name) + SVMUtil.prompt_hook() + z_name_str = str(z_name) + SVMUtil.prompt_hook() + self.assertEqual(name_str, z_name_str) + + def test_adr_str(self): + null = gdb.Value(0) + val = gdb.Value(int(0xCAFE)) + self.assertEqual(SVMUtil.adr_str(null.cast(self.compressed_type.pointer())), ' @z(0x0)') + self.assertEqual(SVMUtil.adr_str(val.cast(self.compressed_type.pointer())), ' @z(0xcafe)') + self.assertEqual(SVMUtil.adr_str(null.cast(self.uncompressed_type.pointer())), ' @(0x0)') + self.assertEqual(SVMUtil.adr_str(val.cast(self.uncompressed_type.pointer())), ' @(0xcafe)') + + def test_java_string(self): + gdb_set_breakpoint("com.oracle.svm.test.debug.helper.PrettyPrinterTest::testString") + gdb_continue() + string = gdb.parse_and_eval("str") + null = gdb.Value(0).cast(string.type) + self.assertEqual(SVMUtil.get_java_string(string), 'string') + self.assertEqual(SVMUtil.get_java_string(null), "") + + def test_java_string_as_gdb_output_string(self): + gdb_set_breakpoint("com.oracle.svm.test.debug.helper.PrettyPrinterTest::testString") + gdb_continue() + string = gdb.parse_and_eval("str") + gdb_set_param("svm-print-string-limit", "2") + self.assertEqual(SVMUtil.get_java_string(string, True), 'st...') + gdb_set_param("svm-print-string-limit", "unlimited") + self.assertEqual(SVMUtil.get_java_string(string, True), 'string') + + def test_get_rtt(self): + gdb_set_breakpoint("com.oracle.svm.test.debug.helper.PrettyPrinterTest::testArrayList") + gdb_continue() + mixed_list = gdb.parse_and_eval("mixedList") # static type is List + str_list = gdb.parse_and_eval("strList") # static type is ArrayList + self.assertEqual(SVMUtil.get_rtt(str_list), SVMUtil.get_rtt(mixed_list)) # both are of rtt ArrayList + self.assertEqual(SVMUtil.get_rtt(str_list['elementData']['data'][0]), gdb.lookup_type('_z_.java.lang.String')) + + +# redirect unittest output to terminal +unittest.main(testRunner=unittest.TextTestRunner(stream=sys.__stdout__))