diff options
Diffstat (limited to 'js/src/gdb')
65 files changed, 5185 insertions, 0 deletions
diff --git a/js/src/gdb/README b/js/src/gdb/README new file mode 100644 index 0000000000..9c98e192b5 --- /dev/null +++ b/js/src/gdb/README @@ -0,0 +1,198 @@ +This directory holds Python code to support debugging SpiderMonkey with +GDB. It includes pretty-printers for common SpiderMonkey types like JS::Value, +jsid, and JSObject, and makes GDB "see through" the SpiderMonkey rooting +types like js::Rooted and JS::Handle. For example: + + (gdb) frame + #0 js::baseops::SetPropertyHelper (cx=0xbf3460, + obj=(JSObject * const) 0x7ffff150b060 [object global] delegate, + receiver=(JSObject * const) 0x7ffff150b060 [object global] delegate, + id=$jsid("x"), defineHow=4, vp=$JS::Int32Value(1), strict=0) + at /home/jimb/moz/archer/js/src/jsobj.cpp:4495 + 4495 MOZ_ASSERT((defineHow & ~(DNP_CACHE_RESULT | DNP_UNQUALIFIED)) == 0); + (gdb) + +Things to note here: + +- obj, a JS::HandleObject, prints as: + obj=(JSObject * const) 0x7ffff150b060 [object global] delegate, + This immediately shows the handle's referent, along with a JavaScript-like summary + of the object. + +- id, a JS::HandleId, prints as: + id=$jsid("x"), + We show the handle's referent, and print the identifier as a string. + +- vp, a JS::MutableHandleValue, prints as: + vp=$JS::Int32Value(1) + We show the handle's referent, using the JS::Value's tag to print it noting + its particular internal type and value. + +You can still see the raw form of a value with 'print/r': + + (gdb) p/r obj + $1 = {<js::HandleBase<JSObject*>> = {<No data fields>}, ptr = 0x7fffffffca60} + (gdb) + +You can also use GDB's 'disable pretty-printer' command to turn off +individual pretty-printers; try 'info pretty-printer' first. + +GDB should pick these extensions up automatically when you debug the shell or +the browser, by auto-loading the 'js-gdb.py' file that the build system +installs alongside the 'js' executable (or 'libxul.so-gdb.py' for the browser). +You may need to add a command like the following to your '$HOME/.gdbinit' file: + + # Tell GDB to trust auto-load files found under ~/moz. + add-auto-load-safe-path ~/moz + +If you do need this, GDB will tell you. + +In general, pretty-printers for pointer types include a summary of the +pointer's referent: + + (gdb) b math_atan2 + Breakpoint 1 at 0x542e0a: file /home/jimb/moz/archer/js/src/jsmath.cpp, line 214. + (gdb) run + js> Math.atan2('Spleen', 42) + Breakpoint 1, math_atan2 (cx=0xbf3440, argc=2, vp=0x7ffff172f0a0) + (gdb) print vp[0] + $1 = $JS::Value((JSObject *) 0x7ffff151c0c0 [object Function "atan2"]) + (gdb) print vp[1] + $2 = $JS::Value((JSObject *) 0x7ffff150d0a0 [object Math]) + (gdb) print vp[2] + $3 = $JS::Value("Spleen") + (gdb) print vp[3] + $4 = $JS::Int32Value(42) + (gdb) + +We used to also have pretty-printers for the actual contents of a JSString +struct, that knew which union branches were live and which were dead. These were +more fragile than the summary pretty-printers, and harder to test, so I've +removed them until we can see how to do better. + +There are unit tests; see 'Running the unit tests', below. + +I'd love for others to pitch in. GDB's Python API is documented in the GDB +manual. + +I've recently rewritten the printers. The new code is simpler, and more +robust; unit tests are easier to write; and the new test harness can run +the tests in parallel. If a printer you'd contributed to in the past was +dropped in the process, I apologize; I felt we should have good test +coverage for any printer landed in-tree. You may also be interested in +'Personal pretty-printers', below. + +Directory layout +---------------- + +- js/src/gdb/mozilla: The actual SpiderMonkey support code. GDB auto-loads this + when you debug an executable or shared library that contains SpiderMonkey. +- js/src/gdb/tests: Unit tests for the above. + - Each '.py' file is a unit test, to be run by js/src/gdb/run-tests.py. + - Each '.cpp' file contains C++ code fragments for some unit test to use. +- js/src/gdb/lib-for-tests: Python modules used by the unit tests. + +In js/src/gdb: + +- run-tests.py: test harness for GDB SpiderMonkey support unit tests. See + 'Running the unit tests', below. +- taskpool.py, progressbar.py: Python modules used by run-tests.py. +- gdb-tests.cpp, gdb-tests.h: Driver program for C++ code fragments. +- gdb-tests-gdb.py.in: Template for GDB autoload file for gdb-tests. + +Personal pretty-printers +------------------------ + +If you'd like to write your own pretty-printers, you can put them in a +module named 'my_mozilla_printers' in a directory somewhere on your Python +module search path. Our autoload code tries to import 'my_mozilla_printers' +after importing our other SpiderMonkey support modules. For example: + + $ echo $PYTHONPATH + /home/jimb/python + $ cat ~/python/my_mozilla_printers.py + import gdb + from mozilla.prettyprinters import ptr_pretty_printer + + # Simple char16_t * printer. Doesn't show address; chases null pointers. + @ptr_pretty_printer('char16_t') + class char16Ptr(object): + def __init__(self, value, cache): self.value = value + def display_hint(self): return 'string' + def to_string(self): + c = u'' + for i in range(50): + if self.value[i] == 0: break + c += unichr(self.value[i]) + return c + $ + ... + (gdb) whatis sample + type = char16_t [4] + (gdb) print &sample[0] + $1 = "Hi!" + +Running the unit tests +---------------------- + +These extensions have unit tests, invoked as follows: + +$ python run-tests.py [OPTIONS] OBJDIR [TESTS...] + +where OBJDIR is a directory containing a standalone SpiderMonkey build; TESTS +are names of selected tests to run (if omitted, we run them all); and OPTIONS +are drawn from the list below. + +--gdb=EXECUTABLE + Instead of running whatever 'gdb' we find in our search path, use + EXECUTABLE to run the tests. + +--srcdir=SRCDIR + Find the sources corresponding to OBJDIR/dist/bin/libmozjs.so in SRCDIR. + Without this option, we use the parent of the directory containing + 'run-tests.py'. Note that SRCDIR must be a complete SpiderMonkey source + directory, as our tests #include internal SpiderMonkey header files (to + test pretty-printers for internal types, like parse nodes.) + +--testdir=TESTDIR + Search for Python scripts and any accompanying C++ source code in + TESTDIR. If omitted, we use the 'tests' directory in the directory + containing 'run-tests.py'. + +--builddir=BUILDDIR + Build the C++ executable that GDB debugs to run the tests in BUILDDIR. + If omitted, create a 'gdb-tests' subdirectory of OBJDIR/js/src. + +(It is safe to use relative paths for OBJDIR, SRCDIR, and so on. They are +always interpreted relative to the directory that was current when +run-tests.py was started.) + +For example, since I build in a subdirectory 'obj~' of the 'js/src' +directory, I use this command from 'js/src' to run the pretty-printer unit +tests: + + $ python gdb/run-tests.py obj~ + +Writing new unit tests +---------------------- + +Each unit test consists of a Python script, possibly with some accompanying +C++ code. Running tests works like this: + +- The run-tests.py script calls 'make' in 'BUILDDIR/gdb' to build + 'gdb-tests'. + +- Then, for each '.py' test script in js/src/gdb/tests, the harness starts + GDB on the 'gdb-tests' executable, and then has GDB run + js/src/gdb/lib-for-tests/prologue.py, passing it the test script's path as + its first command-line argument. + +Thanks To: +---------- + +- David Anderson +- Steve Fink +- Chris Leary +- Josh Matthews +- Jason Orendorff +- Andrew Sutherland diff --git a/js/src/gdb/TODO b/js/src/gdb/TODO new file mode 100644 index 0000000000..07186aa108 --- /dev/null +++ b/js/src/gdb/TODO @@ -0,0 +1,22 @@ +* Ideas: +- char16_t * +- js::Shape, js::Baseshape +- printers for structures with horrible unions (JSString, JSParseNode) +- bring back parse_node.py +- New 'js show' command for showing full trees, property lists, hash table + contents, and so on --- JSParseNode * should not show the whole tree. + Possibly clean up some "pointer-only" stuff in parse_node.py. + - 'js show <defn>' lists a JSDefinition's uses + - 'js show <parsenode>' shows entire tree + - 'js show <scope>' lists all properties (parents) + - 'js tree <scope>' shows property tree +- avoid dead union branches in js::Shape; print attrs nicely +- Print JSScope with identifier. +- Print JSAtomSets, and thus PN_NAMESET. +- JSParseNode PN_NAMESET +- 'JSClass *' pretty-printer + + +Local variables: +mode: org +End: diff --git a/js/src/gdb/gdb-tests-gdb.py.in b/js/src/gdb/gdb-tests-gdb.py.in new file mode 100644 index 0000000000..c6e4009816 --- /dev/null +++ b/js/src/gdb/gdb-tests-gdb.py.in @@ -0,0 +1,11 @@ +"""GDB Python customization auto-loader for GDB test executable""" +#filter substitution + +import os.path +sys.path[0:0] = [os.path.join('@topsrcdir@', 'gdb')] + +import mozilla.autoload +mozilla.autoload.register(gdb.current_objfile()) + +import mozilla.asmjs +mozilla.asmjs.install() diff --git a/js/src/gdb/gdb-tests.cpp b/js/src/gdb/gdb-tests.cpp new file mode 100644 index 0000000000..6000c90f79 --- /dev/null +++ b/js/src/gdb/gdb-tests.cpp @@ -0,0 +1,95 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "gdb-tests.h" +#include "jsapi.h" +#include "jsfriendapi.h" +#include "js/Context.h" +#include "js/GlobalObject.h" +#include "js/Initialization.h" +#include "js/RealmOptions.h" +#include "js/Warnings.h" // JS::SetWarningReporter + +using namespace JS; + +/* The class of the global object. */ +static const JSClass global_class = {"global", JSCLASS_GLOBAL_FLAGS, + &DefaultGlobalClassOps}; + +static volatile int dontOptimizeMeAway = 0; + +void usePointer(const void* ptr) { dontOptimizeMeAway = 1; } + +template <typename T> +static inline T* checkPtr(T* ptr) { + if (!ptr) { + abort(); + } + return ptr; +} + +static void checkBool(bool success) { + if (!success) { + abort(); + } +} + +/* The warning reporter callback. */ +void reportWarning(JSContext* cx, JSErrorReport* report) { + fprintf(stderr, "%s:%u: %s\n", + report->filename ? report->filename : "<no filename>", + (unsigned int)report->lineno, report->message().c_str()); +} + +// prologue.py sets a breakpoint on this function; test functions can call it +// to easily return control to GDB where desired. +void breakpoint() { + // If we leave this function empty, the linker will unify it with other + // empty functions throughout SpiderMonkey. If we then set a GDB + // breakpoint on it, that breakpoint will hit at all sorts of random + // times. So make it perform a distinctive side effect. + fprintf(stderr, "Called " __FILE__ ":breakpoint\n"); +} + +GDBFragment* GDBFragment::allFragments = nullptr; + +int main(int argc, const char** argv) { + if (!JS_Init()) return 1; + JSContext* cx = checkPtr(JS_NewContext(1024 * 1024)); + + JS_SetGCParameter(cx, JSGC_MAX_BYTES, 0xffffffff); + + checkBool(JS::InitSelfHostedCode(cx)); + JS::SetWarningReporter(cx, reportWarning); + + /* Create the global object. */ + JS::RealmOptions options; + RootedObject global( + cx, checkPtr(JS_NewGlobalObject(cx, &global_class, nullptr, + JS::FireOnNewGlobalHook, options))); + JSAutoRealm ar(cx, global); + + argv++; + while (*argv) { + const char* name = *argv++; + GDBFragment* fragment; + for (fragment = GDBFragment::allFragments; fragment; + fragment = fragment->next) { + if (strcmp(fragment->name(), name) == 0) { + fragment->run(cx, argv); + break; + } + } + if (!fragment) { + fprintf(stderr, "Unrecognized fragment name: %s\n", name); + exit(1); + } + } + + return 0; +} diff --git a/js/src/gdb/gdb-tests.h b/js/src/gdb/gdb-tests.h new file mode 100644 index 0000000000..ecaa3d7969 --- /dev/null +++ b/js/src/gdb/gdb-tests.h @@ -0,0 +1,87 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=8 sts=2 et sw=2 tw=80: + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef gdb_gdb_tests_h +#define gdb_gdb_tests_h + +// Support for C++ fragments to be used by Python unit tests for SpiderMonkey's +// GDB support. +// +// That is: +// - js/src/gdb/mozilla holds the actual GDB SpiderMonkey support code. +// - Each '.py' file in js/src/gdb/tests is a unit test for the above. +// - Each '.cpp' file in js/src/gdb/tests is C++ code for one of the unit tests +// to run. +// +// (So the .cpp files are two steps removed from being anything one would +// actually run.) + +#include "NamespaceImports.h" + +#include "js/GCAnnotations.h" + +void breakpoint(); + +extern void usePointer(const void* ptr); + +template <typename T> +void use(const T& thing) { + usePointer(&thing); +} + +struct GDBFragment { + GDBFragment() { + next = allFragments; + allFragments = this; + } + + // The name of this fragment. gdb-tests.cpp runs the fragments whose names + // are passed to it on the command line. + virtual const char* name() = 0; + + // Run the fragment code. |argv| is a reference to the pointer into the + // command-line argument vector, referring to the argument immediately + // following this fragment's name. The fragment can consume arguments and + // advance argv if it wishes. + virtual void run(JSContext* cx, const char**& argv) = 0; + + // We declare one instance of this type for each fragment to run. The + // constructor adds each instance to a linked list, of which this is + // the head. + static GDBFragment* allFragments; + + // The link in the list of all instances. + GDBFragment* next; +}; + +// Macro for declaring a C++ fragment for some Python unit test to call. Usage: +// +// FRAGMENT(<category>, <name>) { <body of fragment function> } +// +// where <category> and <name> are identifiers. The gdb-tests executable +// takes a series of fragment names as command-line arguments and runs them in +// turn; each fragment is named <category>.<name> on the command line. +// +// The body runs in a scope where 'cx' is a usable JSContext*. + +#define FRAGMENT(category, subname) \ + class FRAGMENT_CLASS_NAME(category, subname) : public GDBFragment { \ + void run(JSContext* cx, const char**& argv) override; \ + const char* name() override { \ + return FRAGMENT_STRING_NAME(category, subname); \ + } \ + static FRAGMENT_CLASS_NAME(category, subname) singleton; \ + }; \ + FRAGMENT_CLASS_NAME(category, subname) \ + FRAGMENT_CLASS_NAME(category, subname)::singleton; \ + void FRAGMENT_CLASS_NAME(category, subname)::run(JSContext* cx, \ + const char**& argv) + +#define FRAGMENT_STRING_NAME(category, subname) (#category "." #subname) +#define FRAGMENT_CLASS_NAME(category, subname) Fragment_##category##_##subname + +#endif /* gdb_gdb_tests_h */ diff --git a/js/src/gdb/lib-for-tests/catcher.py b/js/src/gdb/lib-for-tests/catcher.py new file mode 100644 index 0000000000..8ef3529255 --- /dev/null +++ b/js/src/gdb/lib-for-tests/catcher.py @@ -0,0 +1,35 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. + +# Apparently, there's simply no way to ask GDB to exit with a non-zero +# status when the script run with the --eval-command option fails. Thus, if +# we have --eval-command run prologue.py directly, syntax errors there will +# lead GDB to exit with no indication anything went wrong. +# +# To avert that, we use this very small launcher script to run prologue.py +# and catch errors. +# +# Remember, errors in this file will cause spurious passes, so keep this as +# simple as possible! +# flake8: noqa: F821 + +import os +import sys +import traceback + + +def execfile(filename, globs, locs): + with open(filename) as f: + code = compile(f.read(), filename, "exec") + exec(code, globs, locs) + + +try: + # testlibdir is set on the GDB command line, via: + # --eval-command python testlibdir=... + execfile(os.path.join(testlibdir, "prologue.py"), globals(), locals()) +except Exception as err: + sys.stderr.write("Error running GDB prologue:\n") + traceback.print_exc() + sys.exit(1) diff --git a/js/src/gdb/lib-for-tests/prologue.py b/js/src/gdb/lib-for-tests/prologue.py new file mode 100644 index 0000000000..e94ce480d6 --- /dev/null +++ b/js/src/gdb/lib-for-tests/prologue.py @@ -0,0 +1,129 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. + +# flake8: noqa: F821 + +import re +import sys +import traceback + +import gdb + +# testlibdir is set on the GDB command line, via --eval-command python testlibdir=... +sys.path[0:0] = [testlibdir] + +active_fragment = None + +# Run the C++ fragment named |fragment|, stopping on entry to |function| +# ('breakpoint', by default) and then select the calling frame. + + +def run_fragment(fragment, function="gdb-tests.cpp:breakpoint"): + # Arrange to stop at a reasonable place in the test program. + bp = gdb.Breakpoint(function) + try: + gdb.execute("run %s" % (fragment,)) + # Check that we did indeed stop by hitting the breakpoint we set. + assert bp.hit_count == 1 + finally: + bp.delete() + gdb.execute("frame 1") + + global active_fragment + active_fragment = fragment + + +# Assert that |actual| is equal to |expected|; if not, complain in a helpful way. + + +def assert_eq(actual, expected): + if actual != expected: + raise AssertionError( + """Unexpected result: +expected: %r +actual: %r""" + % (expected, actual) + ) + + +# Assert that |expected| regex matches |actual| result; if not, complain in a helpful way. + + +def assert_match(actual, expected): + if re.match(expected, actual, re.MULTILINE) is None: + raise AssertionError( + """Unexpected result: +expected pattern: %r +actual: %r""" + % (expected, actual) + ) + + +# Assert that |value|'s pretty-printed form is |form|. If |value| is a +# string, then evaluate it with gdb.parse_and_eval to produce a value. + + +def assert_pretty(value, form): + if isinstance(value, str): + value = gdb.parse_and_eval(value) + assert_eq(str(value), form) + + +# Assert that |value|'s pretty-printed form match the pattern |pattern|. If +# |value| is a string, then evaluate it with gdb.parse_and_eval to produce a +# value. + + +def assert_regexp_pretty(value, form): + if isinstance(value, str): + value = gdb.parse_and_eval(value) + assert_match(str(value), form) + + +# Check that the list of registered pretty-printers includes one named +# |printer|, with a subprinter named |subprinter|. + + +def assert_subprinter_registered(printer, subprinter): + # Match a line containing |printer| followed by a colon, and then a + # series of more-indented lines containing |subprinter|. + + names = {"printer": re.escape(printer), "subprinter": re.escape(subprinter)} + pat = r"^( +)%(printer)s *\n(\1 +.*\n)*\1 +%(subprinter)s *\n" % names + output = gdb.execute("info pretty-printer", to_string=True) + if not re.search(pat, output, re.MULTILINE): + raise AssertionError( + "assert_subprinter_registered failed to find pretty-printer:\n" + " %s:%s\n" + "'info pretty-printer' says:\n" + "%s" % (printer, subprinter, output) + ) + + +# Request full stack traces for Python errors. +gdb.execute("set python print-stack full") + +# Tell GDB not to ask the user about the things we tell it to do. +gdb.execute("set confirm off", False) + +# Some print settings that make testing easier. +gdb.execute("set print static-members off") +gdb.execute("set print address off") +gdb.execute("set print pretty off") +gdb.execute("set width 0") + +try: + # testscript is set on the GDB command line, via: + # --eval-command python testscript=... + execfile(testscript, globals(), locals()) +except AssertionError as err: + header = "\nAssertion traceback" + if active_fragment: + header += " for " + active_fragment + sys.stderr.write(header + ":\n") + (t, v, tb) = sys.exc_info() + traceback.print_tb(tb) + sys.stderr.write("\nTest assertion failed:\n") + sys.stderr.write(str(err)) + sys.exit(1) diff --git a/js/src/gdb/moz.build b/js/src/gdb/moz.build new file mode 100644 index 0000000000..550dc93bec --- /dev/null +++ b/js/src/gdb/moz.build @@ -0,0 +1,66 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +GeckoProgram("gdb-tests", linkage=None) + +include("../js-cxxflags.mozbuild") +include("../js-standalone.mozbuild") + +SOURCES += [ + # This file must not be unified with any of the test files, or the use() + # and breakpoint() functions might get optimized out. + "gdb-tests.cpp" +] + +UNIFIED_SOURCES += [ + "tests/enum-printers.cpp", + "tests/test-asmjs.cpp", + "tests/test-ExecutableAllocator.cpp", + "tests/test-GCCellPtr.cpp", + "tests/test-Interpreter.cpp", + "tests/test-jsbytecode.cpp", + "tests/test-jsid.cpp", + "tests/test-JSObject.cpp", + "tests/test-jsop.cpp", + "tests/test-JSString.cpp", + "tests/test-JSSymbol.cpp", + "tests/test-jsval.cpp", + "tests/test-Root.cpp", + "tests/test-unwind.cpp", + "tests/typedef-printers.cpp", +] + +SOURCES += ["tests/test-prettyprinters.cpp"] + +if CONFIG["CC_TYPE"] != "clang-cl": + # Test expects to see pre-typedef names of base classes, but the compiler will + # normally omit those from the debuginfo. The current clang-cl does not support + # this option. + SOURCES["tests/test-prettyprinters.cpp"].flags += [ + "-fno-eliminate-unused-debug-types", + ] + +if CONFIG["CC_TYPE"] == "clang": + # Clang may complain that -fno-eliminate-unused-debug-types is unused when used + # in conjunction with other flags such as -gline-tables-only. + SOURCES["tests/test-prettyprinters.cpp"].flags += [ + "-Qunused-arguments", + ] + +DEFINES["EXPORT_JS_API"] = True + +LOCAL_INCLUDES += [ + "!..", + "..", +] + +USE_LIBS += [ + "static:js", +] + +DEFINES["topsrcdir"] = "%s/js/src" % TOPSRCDIR +FINAL_TARGET_PP_FILES += ["gdb-tests-gdb.py.in"] +OBJDIR_FILES.js.src.gdb += ["!/dist/bin/gdb-tests-gdb.py"] diff --git a/js/src/gdb/mozilla/CellHeader.py b/js/src/gdb/mozilla/CellHeader.py new file mode 100644 index 0000000000..cb667b3ad0 --- /dev/null +++ b/js/src/gdb/mozilla/CellHeader.py @@ -0,0 +1,25 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. + +import gdb + + +def get_header_ptr(value, ptr_t): + # Return the pointer stored in Cell::header_ for subclasses of + # TenuredCellWithNonGCPointer and CellWithTenuredGCPointer. + return value["header_"]["value_"].cast(ptr_t) + + +def get_header_length_and_flags(value, cache): + # Return the length and flags values for subclasses of + # CellWithLengthAndFlags. + flags = value["header_"]["value_"].cast(cache.uintptr_t) + try: + length = value["length_"] + except gdb.error: + # If we couldn't fetch the length directly, it must be stored + # within `flags`. + length = flags >> 32 + flags = flags % 2 ** 32 + return length, flags diff --git a/js/src/gdb/mozilla/ExecutableAllocator.py b/js/src/gdb/mozilla/ExecutableAllocator.py new file mode 100644 index 0000000000..5187e2d1f1 --- /dev/null +++ b/js/src/gdb/mozilla/ExecutableAllocator.py @@ -0,0 +1,97 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. + +""" +All jitted code is allocated via the ExecutableAllocator class. Make GDB aware +of them, such that we can query for pages which are containing code which are +allocated by the Jits. +""" + +import gdb + +import mozilla.prettyprinters +from mozilla.prettyprinters import pretty_printer, ptr_pretty_printer + +# Forget any printers from previous loads of this module. +mozilla.prettyprinters.clear_module_printers(__name__) + + +class jsjitExecutableAllocatorCache(object): + """Cache information about the ExecutableAllocator type for this objfile.""" + + def __init__(self): + self.d = None + + def __getattr__(self, name): + if self.d is None: + self.initialize() + return self.d[name] + + def initialize(self): + self.d = {} + self.d["ExecutableAllocator"] = gdb.lookup_type("js::jit::ExecutableAllocator") + self.d["ExecutablePool"] = gdb.lookup_type("js::jit::ExecutablePool") + self.d["HashNumber"] = gdb.lookup_type("mozilla::HashNumber") + + +@pretty_printer("js::jit::ExecutableAllocator") +class jsjitExecutableAllocator(object): + def __init__(self, value, cache): + if not cache.mod_ExecutableAllocator: + cache.mod_ExecutableAllocator = jsjitExecutableAllocatorCache() + self.value = value + self.cache = cache.mod_ExecutableAllocator + + def to_string(self): + return "ExecutableAllocator([%s])" % ", ".join([str(x) for x in self]) + + def __iter__(self): + return self.PoolIterator(self) + + class PoolIterator(object): + def __init__(self, allocator): + self.allocator = allocator + self.entryType = allocator.cache.ExecutablePool.pointer() + self.hashNumType = allocator.cache.HashNumber + # Emulate the HashSet::Range + self.table = allocator.value["m_pools"]["mImpl"]["mTable"] + self.index = 0 + kHashNumberBits = 32 + hashShift = allocator.value["m_pools"]["mImpl"]["mHashShift"] + self.capacity = 1 << (kHashNumberBits - hashShift) + if self.table == 0: + self.capacity = 0 + # auto hashes = reinterpret_cast<HashNumber*>(mTable); + self.hashes = self.table.cast(self.hashNumType.pointer()) + # auto entries = reinterpret_cast<Entry*>(&hashes[capacity()]); + self.entries = (self.hashes + self.capacity).cast(self.entryType.pointer()) + + def __iter__(self): + return self + + def next(self): + return self.__next__() + + def __next__(self): + cur = self.index + if cur >= self.capacity: + raise StopIteration() + self.index = self.index + 1 + if self.hashes[cur] > 1: # table[i]->isLive() + return self.entries[cur] + return self.__next__() + + +@ptr_pretty_printer("js::jit::ExecutablePool") +class jsjitExecutablePool(mozilla.prettyprinters.Pointer): + def __init__(self, value, cache): + if not cache.mod_ExecutableAllocator: + cache.mod_ExecutableAllocator = jsjitExecutableAllocatorCache() + self.value = value + self.cache = cache.mod_ExecutableAllocator + + def to_string(self): + pages = self.value["m_allocation"]["pages"] + size = self.value["m_allocation"]["size"] + return "ExecutablePool %08x-%08x" % (pages, pages + size) diff --git a/js/src/gdb/mozilla/GCCellPtr.py b/js/src/gdb/mozilla/GCCellPtr.py new file mode 100644 index 0000000000..e42ecfa893 --- /dev/null +++ b/js/src/gdb/mozilla/GCCellPtr.py @@ -0,0 +1,125 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. + +# Pretty-printers for GCCellPtr values. + +import gdb + +import mozilla.prettyprinters +from mozilla.prettyprinters import pretty_printer + +# Forget any printers from previous loads of this module. +mozilla.prettyprinters.clear_module_printers(__name__) + +# Cache information about the types for this objfile. + + +class GCCellPtrTypeCache(object): + def __init__(self, cache): + self.TraceKind_t = gdb.lookup_type("JS::TraceKind") + self.AllocKind_t = gdb.lookup_type("js::gc::AllocKind") + self.Arena_t = gdb.lookup_type("js::gc::Arena") + self.Cell_t = gdb.lookup_type("js::gc::Cell") + self.TenuredCell_t = gdb.lookup_type("js::gc::TenuredCell") + + trace_kinds = gdb.types.make_enum_dict(self.TraceKind_t) + alloc_kinds = gdb.types.make_enum_dict(self.AllocKind_t) + + def trace_kind(k): + return trace_kinds["JS::TraceKind::" + k] + + def alloc_kind(k): + return alloc_kinds["js::gc::AllocKind::" + k] + + # Build a mapping from TraceKind enum values to the types they denote. + trace_map = { + # Inline types. + "Object": "JSObject", + "BigInt": "JS::BigInt", + "String": "JSString", + "Symbol": "JS::Symbol", + "Shape": "js::Shape", + "BaseShape": "js::BaseShape", + "Null": "std::nullptr_t", + # Out-of-line types. + "JitCode": "js::jit::JitCode", + "Script": "js::BaseScript", + "Scope": "js::Scope", + "RegExpShared": "js::RegExpShared", + "GetterSetter": "js::GetterSetter", + "PropMap": "js::PropMap", + } + + # Map from AllocKind to TraceKind for out-of-line types. + alloc_map = { + "JITCODE": "JitCode", + "SCRIPT": "Script", + "SCOPE": "Scope", + "REGEXP_SHARED": "RegExpShared", + "GETTER_SETTER": "GetterSetter", + "COMPACT_PROP_MAP": "PropMap", + "NORMAL_PROP_MAP": "PropMap", + "DICT_PROP_MAP": "PropMap", + } + + self.trace_kind_to_type = { + trace_kind(k): gdb.lookup_type(v) for k, v in trace_map.items() + } + self.alloc_kind_to_trace_kind = { + alloc_kind(k): trace_kind(v) for k, v in alloc_map.items() + } + + self.Null = trace_kind("Null") + self.tracekind_mask = gdb.parse_and_eval("JS::OutOfLineTraceKindMask") + self.arena_mask = gdb.parse_and_eval("js::gc::ArenaMask") + + +@pretty_printer("JS::GCCellPtr") +class GCCellPtr(object): + def __init__(self, value, cache): + self.value = value + if not cache.mod_GCCellPtr: + cache.mod_GCCellPtr = GCCellPtrTypeCache(cache) + self.cache = cache + + def to_string(self): + ptr = self.value["ptr"] + kind = ptr & self.cache.mod_GCCellPtr.tracekind_mask + if kind == self.cache.mod_GCCellPtr.Null: + return "JS::GCCellPtr(nullptr)" + if kind == self.cache.mod_GCCellPtr.tracekind_mask: + # Out-of-line trace kinds. + # + # Compute the underlying type for out-of-line kinds by + # reimplementing the GCCellPtr::outOfLineKind() method. + # + # The extra casts below are only present to make it easier to + # compare this code against the C++ implementation. + + # GCCellPtr::asCell() + cell_ptr = ptr & ~self.cache.mod_GCCellPtr.tracekind_mask + cell = cell_ptr.reinterpret_cast(self.cache.mod_GCCellPtr.Cell_t.pointer()) + + # Cell::asTenured() + tenured = cell.cast(self.cache.mod_GCCellPtr.TenuredCell_t.pointer()) + + # TenuredCell::arena() + addr = int(tenured) + arena_ptr = addr & ~self.cache.mod_GCCellPtr.arena_mask + arena = arena_ptr.reinterpret_cast( + self.cache.mod_GCCellPtr.Arena_t.pointer() + ) + + # Arena::getAllocKind() + alloc_kind = arena["allocKind"].cast(self.cache.mod_GCCellPtr.AllocKind_t) + alloc_idx = int( + alloc_kind.cast(self.cache.mod_GCCellPtr.AllocKind_t.target()) + ) + + # Map the AllocKind to a TraceKind. + kind = self.cache.mod_GCCellPtr.alloc_kind_to_trace_kind[alloc_idx] + type_name = self.cache.mod_GCCellPtr.trace_kind_to_type[int(kind)] + return "JS::GCCellPtr(({}*) {})".format( + type_name, ptr.cast(self.cache.void_ptr_t) + ) diff --git a/js/src/gdb/mozilla/Interpreter.py b/js/src/gdb/mozilla/Interpreter.py new file mode 100644 index 0000000000..cc7cd73726 --- /dev/null +++ b/js/src/gdb/mozilla/Interpreter.py @@ -0,0 +1,94 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. + +# Pretty-printers for InterpreterRegs. + +import gdb + +import mozilla.prettyprinters as prettyprinters + +prettyprinters.clear_module_printers(__name__) + +from mozilla.prettyprinters import pretty_printer + + +class InterpreterTypeCache(object): + # Cache information about the Interpreter types for this objfile. + def __init__(self): + self.tValue = gdb.lookup_type("JS::Value") + self.tJSOp = gdb.lookup_type("JSOp") + try: + self.tScriptFrameIterData = gdb.lookup_type("js::ScriptFrameIter::Data") + except gdb.error: + # Work around problem with gcc optimized debuginfo where it doesn't + # seem to be able to see that ScriptFrameIter inherits the + # FrameIter::Data type. + self.tScriptFrameIterData = gdb.lookup_type("js::FrameIter::Data") + self.tInterpreterFrame = gdb.lookup_type("js::InterpreterFrame") + self.tBaselineFrame = gdb.lookup_type("js::jit::BaselineFrame") + self.tRematerializedFrame = gdb.lookup_type("js::jit::RematerializedFrame") + self.tDebugFrame = gdb.lookup_type("js::wasm::DebugFrame") + + +@pretty_printer("js::InterpreterRegs") +class InterpreterRegs(object): + def __init__(self, value, cache): + self.value = value + self.cache = cache + if not cache.mod_Interpreter: + cache.mod_Interpreter = InterpreterTypeCache() + self.itc = cache.mod_Interpreter + + # There's basically no way to co-operate with 'set print pretty' (how would + # you get the current level of indentation?), so we don't even bother + # trying. No 'children', just 'to_string'. + def to_string(self): + fp_ = "fp_ = {}".format(self.value["fp_"]) + slots = (self.value["fp_"] + 1).cast(self.itc.tValue.pointer()) + sp = "sp = fp_.slots() + {}".format(self.value["sp"] - slots) + pc = "pc = {}".format(self.value["pc"]) + return "{{ {}, {}, {} }}".format(fp_, sp, pc) + + +@pretty_printer("js::AbstractFramePtr") +class AbstractFramePtr(object): + Tag_ScriptFrameIterData = 0x0 + Tag_InterpreterFrame = 0x1 + Tag_BaselineFrame = 0x2 + Tag_RematerializedFrame = 0x3 + Tag_WasmDebugFrame = 0x4 + TagMask = 0x7 + + def __init__(self, value, cache): + self.value = value + self.cache = cache + if not cache.mod_Interpreter: + cache.mod_Interpreter = InterpreterTypeCache() + self.itc = cache.mod_Interpreter + + def to_string(self): + ptr = self.value["ptr_"] + tag = ptr & AbstractFramePtr.TagMask + ptr = ptr & ~AbstractFramePtr.TagMask + if tag == AbstractFramePtr.Tag_ScriptFrameIterData: + label = "js::ScriptFrameIter::Data" + ptr = ptr.cast(self.itc.tScriptFrameIterData.pointer()) + if tag == AbstractFramePtr.Tag_InterpreterFrame: + label = "js::InterpreterFrame" + ptr = ptr.cast(self.itc.tInterpreterFrame.pointer()) + if tag == AbstractFramePtr.Tag_BaselineFrame: + label = "js::jit::BaselineFrame" + ptr = ptr.cast(self.itc.tBaselineFrame.pointer()) + if tag == AbstractFramePtr.Tag_RematerializedFrame: + label = "js::jit::RematerializedFrame" + ptr = ptr.cast(self.itc.tRematerializedFrame.pointer()) + if tag == AbstractFramePtr.Tag_WasmDebugFrame: + label = "js::wasm::DebugFrame" + ptr = ptr.cast(self.itc.tDebugFrame.pointer()) + return "AbstractFramePtr (({} *) {})".format(label, ptr) + + # Provide the ptr_ field as a child, so it prints after the pretty string + # provided above. + def children(self): + yield ("ptr_", self.value["ptr_"]) diff --git a/js/src/gdb/mozilla/IonGraph.py b/js/src/gdb/mozilla/IonGraph.py new file mode 100644 index 0000000000..639baf3ace --- /dev/null +++ b/js/src/gdb/mozilla/IonGraph.py @@ -0,0 +1,283 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. + +""" +Debugging JIT Compilations can be obscure without large context. This python +script provide commands to let GDB open an image viewer to display the graph of +any compilation, as they are executed within GDB. + +This python script should be sourced within GDB after loading the python scripts +provided with SpiderMonkey. +""" + +import os +import subprocess +import tempfile +import time + +import gdb + +import mozilla.prettyprinters +from mozilla.prettyprinters import pretty_printer + +# Forget any printers from previous loads of this module. +mozilla.prettyprinters.clear_module_printers(__name__) + +# Cache information about the JSString type for this objfile. + + +class jsvmPrinterCache(object): + def __init__(self): + self.d = None + + def __getattr__(self, name): + if self.d is None: + self.initialize() + return self.d[name] + + def initialize(self): + self.d = {} + self.d["char"] = gdb.lookup_type("char") + + +# Dummy class used to store the content of the type cache in the context of the +# iongraph command, which uses the jsvmLSprinter. + + +class ModuleCache(object): + def __init__(self): + self.mod_IonGraph = None + + +@pretty_printer("js::vm::LSprinter") +class jsvmLSprinter(object): + def __init__(self, value, cache): + self.value = value + if not cache.mod_IonGraph: + cache.mod_IonGraph = jsvmPrinterCache() + self.cache = cache.mod_IonGraph + + def to_string(self): + next = self.value["head_"] + tail = self.value["tail_"] + if next == 0: + return "" + res = "" + while next != tail: + chars = (next + 1).cast(self.cache.char.pointer()) + res = res + chars.string("ascii", "ignore", next["length"]) + next = next["next"] + length = next["length"] - self.value["unused_"] + chars = (next + 1).cast(self.cache.char.pointer()) + res = res + chars.string("ascii", "ignore", length) + return res + + +def search_in_path(bin): + paths = os.getenv("PATH", "") + if paths == "": + return "" + for d in paths.split(":"): + f = os.path.join(d, bin) + if os.access(f, os.X_OK): + return f + return "" + + +class IonGraphBinParameter(gdb.Parameter): + set_doc = "Set the path to iongraph binary, used by iongraph command." + show_doc = "Show the path to iongraph binary, used by iongraph command." + + def get_set_string(self): + return "Path to iongraph binary changed to: %s" % self.value + + def get_show_string(self, value): + return "Path to iongraph binary set to: %s" % value + + def __init__(self): + super(IonGraphBinParameter, self).__init__( + "iongraph-bin", gdb.COMMAND_SUPPORT, gdb.PARAM_FILENAME + ) + self.value = os.getenv("GDB_IONGRAPH", "") + if self.value == "": + self.value = search_in_path("iongraph") + + +class DotBinParameter(gdb.Parameter): + set_doc = "Set the path to dot binary, used by iongraph command." + show_doc = "Show the path to dot binary, used by iongraph command." + + def get_set_string(self): + return "Path to dot binary changed to: %s" % self.value + + def get_show_string(self, value): + return "Path to dot binary set to: %s" % value + + def __init__(self): + super(DotBinParameter, self).__init__( + "dot-bin", gdb.COMMAND_SUPPORT, gdb.PARAM_FILENAME + ) + self.value = os.getenv("GDB_DOT", "") + if self.value == "": + self.value = search_in_path("dot") + + +class PngViewerBinParameter(gdb.Parameter): + set_doc = "Set the path to a png viewer binary, used by iongraph command." + show_doc = "Show the path to a png viewer binary, used by iongraph command." + + def get_set_string(self): + return "Path to a png viewer binary changed to: %s" % self.value + + def get_show_string(self): + return "Path to a png viewer binary set to: %s" % self.value + + def __init__(self): + super(PngViewerBinParameter, self).__init__( + "pngviewer-bin", gdb.COMMAND_SUPPORT, gdb.PARAM_FILENAME + ) + self.value = os.getenv("GDB_PNGVIEWER", "") + if self.value == "": + self.value = search_in_path("xdg-open") + + +iongraph = IonGraphBinParameter() +dot = DotBinParameter() +pngviewer = PngViewerBinParameter() + + +class IonGraphCommand(gdb.Command): + """Command used to display the current state of the MIR graph in a png + viewer by providing an expression to access the MIRGenerator. + """ + + def __init__(self): + super(IonGraphCommand, self).__init__( + "iongraph", gdb.COMMAND_DATA, gdb.COMPLETE_EXPRESSION + ) + self.typeCache = ModuleCache() + + def invoke(self, mirGenExpr, from_tty): + """Call function from the graph spewer to populate the json printer with + the content generated by the jsonSpewer. Then we read the json content + from the jsonPrinter internal data, and gives that as input of iongraph + command.""" + + # From the MIRGenerator, find the graph spewer which contains both the + # jsonPrinter (containing the result of the output), and the jsonSpewer + # (continaining methods for spewing the graph). + mirGen = gdb.parse_and_eval(mirGenExpr) + jsonPrinter = mirGen["gs_"]["jsonPrinter_"] + jsonSpewer = mirGen["gs_"]["jsonSpewer_"] + graph = mirGen["graph_"] + + # These commands are doing side-effects which are saving the state of + # the compiled code on the LSprinter dedicated for logging. Fortunately, + # if you are using these gdb command, this probably means that other + # ways of getting this content failed you already, so making a mess in + # these logging strings should not cause much issues. + gdb.parse_and_eval( + "(*(%s*)(%s)).clear()" + % ( + jsonPrinter.type, + jsonPrinter.address, + ) + ) + gdb.parse_and_eval( + "(*(%s*)(%s)).beginFunction((JSScript*)0)" + % ( + jsonSpewer.type, + jsonSpewer.address, + ) + ) + gdb.parse_and_eval( + '(*(%s*)(%s)).beginPass("gdb")' + % ( + jsonSpewer.type, + jsonSpewer.address, + ) + ) + gdb.parse_and_eval( + "(*(%s*)(%s)).spewMIR((%s)%s)" + % ( + jsonSpewer.type, + jsonSpewer.address, + graph.type, + graph, + ) + ) + gdb.parse_and_eval( + "(*(%s*)(%s)).spewLIR((%s)%s)" + % ( + jsonSpewer.type, + jsonSpewer.address, + graph.type, + graph, + ) + ) + gdb.parse_and_eval( + "(*(%s*)(%s)).endPass()" + % ( + jsonSpewer.type, + jsonSpewer.address, + ) + ) + gdb.parse_and_eval( + "(*(%s*)(%s)).endFunction()" + % ( + jsonSpewer.type, + jsonSpewer.address, + ) + ) + + # Dump the content of the LSprinter containing the JSON view of the + # graph into a python string. + json = jsvmLSprinter(jsonPrinter, self.typeCache).to_string() + + # We are in the middle of the program execution and are messing up with + # the state of the logging data. As this might not be the first time we + # call beginFunction, we might have an extra comma at the beginning of + # the output, just strip it. + if json[0] == ",": + json = json[1:] + + # Usually this is added by the IonSpewer. + json = '{ "functions": [%s] }' % json + + # Display the content of the json with iongraph and other tools. + self.displayMIRGraph(json) + + def displayMIRGraph(self, jsonStr): + png = tempfile.NamedTemporaryFile() + + # start all processes in a shell-like equivalent of: + # iongraph < json | dot > tmp.png; xdg-open tmp.png + i = subprocess.Popen( + [iongraph.value, "--funcnum", "0", "--passnum", "0", "--out-mir", "-", "-"], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + ) + d = subprocess.Popen([dot.value, "-Tpng"], stdin=i.stdout, stdout=png) + + # Write the json file as the input of the iongraph command. + i.stdin.write(jsonStr.encode("utf8")) + i.stdin.close() + i.stdout.close() + + # Wait for iongraph and dot, such that the png file contains all the + # bits needed to by the png viewer. + i.wait() + d.communicate()[0] + + # Spawn & detach the png viewer, to which we give the name of the + # temporary file. Note, as we do not want to wait on the image viewer, + # there is a minor race between the removal of the temporary file, which + # would happen at the next garbage collection cycle, and the start of + # the png viewer. We could use a pipe, but unfortunately, this does not + # seems to be supported by xdg-open. + subprocess.Popen([pngviewer.value, png.name], stdin=None, stdout=None) + time.sleep(1) + + +iongraph_cmd = IonGraphCommand() diff --git a/js/src/gdb/mozilla/JSObject.py b/js/src/gdb/mozilla/JSObject.py new file mode 100644 index 0000000000..95accc93b9 --- /dev/null +++ b/js/src/gdb/mozilla/JSObject.py @@ -0,0 +1,119 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. + +# Pretty-printers for SpiderMonkey JSObjects. + +import re + +import gdb + +import mozilla.prettyprinters as prettyprinters +from mozilla.CellHeader import get_header_ptr +from mozilla.jsval import JSValue +from mozilla.prettyprinters import ptr_pretty_printer, ref_pretty_printer + +prettyprinters.clear_module_printers(__name__) + + +class JSObjectTypeCache(object): + def __init__(self): + object_flag = gdb.lookup_type("js::ObjectFlag") + self.objectflag_IsUsedAsPrototype = prettyprinters.enum_value( + object_flag, "js::ObjectFlag::IsUsedAsPrototype" + ) + self.value_ptr_t = gdb.lookup_type("JS::Value").pointer() + self.func_ptr_t = gdb.lookup_type("JSFunction").pointer() + self.class_NON_NATIVE = gdb.parse_and_eval("JSClass::NON_NATIVE") + self.BaseShape_ptr_t = gdb.lookup_type("js::BaseShape").pointer() + self.Shape_ptr_t = gdb.lookup_type("js::Shape").pointer() + self.JSClass_ptr_t = gdb.lookup_type("JSClass").pointer() + self.JSScript_ptr_t = gdb.lookup_type("JSScript").pointer() + self.JSFunction_AtomSlot = gdb.parse_and_eval("JSFunction::AtomSlot") + self.JSFunction_NativeJitInfoOrInterpretedScriptSlot = gdb.parse_and_eval( + "JSFunction::NativeJitInfoOrInterpretedScriptSlot" + ) + + +# There should be no need to register this for JSFunction as well, since we +# search for pretty-printers under the names of base classes, and +# JSFunction has JSObject as a base class. + + +gdb_string_regexp = re.compile(r'(?:0x[0-9a-z]+ )?(?:<.*> )?"(.*)"', re.I) + + +@ptr_pretty_printer("JSObject") +class JSObjectPtrOrRef(prettyprinters.Pointer): + def __init__(self, value, cache): + super(JSObjectPtrOrRef, self).__init__(value, cache) + if not cache.mod_JSObject: + cache.mod_JSObject = JSObjectTypeCache() + self.otc = cache.mod_JSObject + + def summary(self): + shape = get_header_ptr(self.value, self.otc.Shape_ptr_t) + baseshape = get_header_ptr(shape, self.otc.BaseShape_ptr_t) + classp = get_header_ptr(baseshape, self.otc.JSClass_ptr_t) + non_native = classp["flags"] & self.otc.class_NON_NATIVE + + # Use GDB to format the class name, but then strip off the address + # and the quotes. + class_name = str(classp["name"]) + m = gdb_string_regexp.match(class_name) + if m: + class_name = m.group(1) + + if non_native: + return "[object {}]".format(class_name) + else: + flags = shape["objectFlags_"]["flags_"] + used_as_prototype = bool(flags & self.otc.objectflag_IsUsedAsPrototype) + name = None + if class_name == "Function": + function = self.value + concrete_type = function.type.strip_typedefs() + if concrete_type.code == gdb.TYPE_CODE_REF: + function = function.address + name = get_function_name(function, self.cache) + return "[object {}{}]{}".format( + class_name, + " " + name if name else "", + " used_as_prototype" if used_as_prototype else "", + ) + + +def get_function_name(function, cache): + if not cache.mod_JSObject: + cache.mod_JSObject = JSObjectTypeCache() + otc = cache.mod_JSObject + + function = function.cast(otc.func_ptr_t) + fixed_slots = (function + 1).cast(otc.value_ptr_t) + atom_value = JSValue(fixed_slots[otc.JSFunction_AtomSlot], cache) + + if atom_value.is_undefined(): + return "<unnamed>" + + return str(atom_value.get_string()) + + +def get_function_script(function, cache): + if not cache.mod_JSObject: + cache.mod_JSObject = JSObjectTypeCache() + otc = cache.mod_JSObject + + function = function.cast(otc.func_ptr_t) + fixed_slots = (function + 1).cast(otc.value_ptr_t) + slot = otc.JSFunction_NativeJitInfoOrInterpretedScriptSlot + script_value = JSValue(fixed_slots[slot], cache) + + if script_value.is_undefined(): + return 0 + + return script_value.get_private() + + +@ref_pretty_printer("JSObject") +def JSObjectRef(value, cache): + return JSObjectPtrOrRef(value, cache) diff --git a/js/src/gdb/mozilla/JSString.py b/js/src/gdb/mozilla/JSString.py new file mode 100644 index 0000000000..3c758e0352 --- /dev/null +++ b/js/src/gdb/mozilla/JSString.py @@ -0,0 +1,105 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. + +# Pretty-printers for SpiderMonkey strings. + +import gdb + +import mozilla.prettyprinters +from mozilla.CellHeader import get_header_length_and_flags +from mozilla.prettyprinters import ptr_pretty_printer + +try: + chr(10000) # UPPER RIGHT PENCIL +except ValueError: # yuck, we are in Python 2.x, so chr() is 8-bit + chr = unichr # replace with teh unicodes + +# Forget any printers from previous loads of this module. +mozilla.prettyprinters.clear_module_printers(__name__) + + +class JSStringTypeCache(object): + # Cache information about the JSString type for this objfile. + def __init__(self, cache): + dummy = gdb.Value(0).cast(cache.JSString_ptr_t) + self.ATOM_BIT = dummy["ATOM_BIT"] + self.LINEAR_BIT = dummy["LINEAR_BIT"] + self.INLINE_CHARS_BIT = dummy["INLINE_CHARS_BIT"] + self.TYPE_FLAGS_MASK = dummy["TYPE_FLAGS_MASK"] + self.LATIN1_CHARS_BIT = dummy["LATIN1_CHARS_BIT"] + + +class Common(mozilla.prettyprinters.Pointer): + def __init__(self, value, cache): + super(Common, self).__init__(value, cache) + if not cache.mod_JSString: + cache.mod_JSString = JSStringTypeCache(cache) + self.stc = cache.mod_JSString + + +@ptr_pretty_printer("JSString") +class JSStringPtr(Common): + def display_hint(self): + return "string" + + def chars(self): + d = self.value["d"] + length, flags = get_header_length_and_flags(self.value, self.cache) + + corrupt = { + 0x2F2F2F2F: "JS_FRESH_NURSERY_PATTERN", + 0x2B2B2B2B: "JS_SWEPT_NURSERY_PATTERN", + 0xE5E5E5E5: "jemalloc freed memory", + }.get(flags & 0xFFFFFFFF) + if corrupt: + for ch in "<CORRUPT:%s>" % corrupt: + yield ch + return + is_rope = (flags & self.stc.LINEAR_BIT) == 0 + if is_rope: + for c in JSStringPtr(d["s"]["u2"]["left"], self.cache).chars(): + yield c + for c in JSStringPtr(d["s"]["u3"]["right"], self.cache).chars(): + yield c + else: + is_inline = (flags & self.stc.INLINE_CHARS_BIT) != 0 + is_latin1 = (flags & self.stc.LATIN1_CHARS_BIT) != 0 + if is_inline: + if is_latin1: + chars = d["inlineStorageLatin1"] + else: + chars = d["inlineStorageTwoByte"] + else: + if is_latin1: + chars = d["s"]["u2"]["nonInlineCharsLatin1"] + else: + chars = d["s"]["u2"]["nonInlineCharsTwoByte"] + for i in range(int(length)): + yield chars[i] + + def to_string(self, maxlen=200): + s = "" + invalid_chars_allowed = 2 + for c in self.chars(): + if len(s) >= maxlen: + s += "..." + break + + try: + # Convert from gdb.Value to string. + s += chr(c) + except ValueError: + if invalid_chars_allowed == 0: + s += "<TOO_MANY_INVALID_CHARS>" + break + else: + invalid_chars_allowed -= 1 + s += "\\x%04x" % (c & 0xFFFF) + return s + + +@ptr_pretty_printer("JSAtom") +class JSAtomPtr(Common): + def to_string(self): + return self.value.cast(self.cache.JSString_ptr_t) diff --git a/js/src/gdb/mozilla/JSSymbol.py b/js/src/gdb/mozilla/JSSymbol.py new file mode 100644 index 0000000000..db96898c91 --- /dev/null +++ b/js/src/gdb/mozilla/JSSymbol.py @@ -0,0 +1,40 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. + +# Pretty-printer for SpiderMonkey symbols. + +import mozilla.prettyprinters +from mozilla.CellHeader import get_header_ptr +from mozilla.prettyprinters import ptr_pretty_printer + +# Forget any printers from previous loads of this module. +mozilla.prettyprinters.clear_module_printers(__name__) + +# JS::SymbolCode enumerators +PrivateNameSymbol = 0xFFFFFFFD +InSymbolRegistry = 0xFFFFFFFE +UniqueSymbol = 0xFFFFFFFF + + +@ptr_pretty_printer("JS::Symbol") +class JSSymbolPtr(mozilla.prettyprinters.Pointer): + def __init__(self, value, cache): + super(JSSymbolPtr, self).__init__(value, cache) + self.value = value + + def to_string(self): + code = int(self.value["code_"]) & 0xFFFFFFFF + desc = str(get_header_ptr(self.value, self.cache.JSString_ptr_t)) + if code == InSymbolRegistry: + return "Symbol.for({})".format(desc) + elif code == UniqueSymbol: + return "Symbol({})".format(desc) + elif code == PrivateNameSymbol: + return "#{}".format(desc) + else: + # Well-known symbol. Strip off the quotes added by the JSString * + # pretty-printer. + assert desc[0] == '"' + assert desc[-1] == '"' + return desc[1:-1] diff --git a/js/src/gdb/mozilla/PropertyKey.py b/js/src/gdb/mozilla/PropertyKey.py new file mode 100644 index 0000000000..517976a6da --- /dev/null +++ b/js/src/gdb/mozilla/PropertyKey.py @@ -0,0 +1,62 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. + +# Pretty-printers for JSID values. + +import mozilla.prettyprinters +import mozilla.Root +from mozilla.prettyprinters import pretty_printer + +# Forget any printers from previous loads of this module. +mozilla.prettyprinters.clear_module_printers(__name__) + + +@pretty_printer("JS::PropertyKey") +class PropertyKey(object): + # Since people don't always build with macro debugging info, I can't + # think of any way to avoid copying these values here, short of using + # inferior calls for every operation (which, I hear, is broken from + # pretty-printers in some recent GDBs). + StringTypeTag = 0x0 + IntTagBit = 0x1 + VoidTypeTag = 0x2 + SymbolTypeTag = 0x4 + TypeMask = 0x7 + + def __init__(self, value, cache): + self.value = value + self.cache = cache + self.concrete_type = self.value.type.strip_typedefs() + + def to_string(self): + bits = self.value["asBits_"] + tag = bits & PropertyKey.TypeMask + if tag == PropertyKey.StringTypeTag: + body = bits.cast(self.cache.JSString_ptr_t) + elif tag & PropertyKey.IntTagBit: + body = bits >> 1 + elif tag == PropertyKey.VoidTypeTag: + return "JS::VoidPropertyKey" + elif tag == PropertyKey.SymbolTypeTag: + body = (bits & ~PropertyKey.TypeMask).cast(self.cache.JSSymbol_ptr_t) + else: + body = "<unrecognized>" + return "$jsid(%s)" % (body,) + + +@pretty_printer("JS::Rooted<long>") +def RootedPropertyKey(value, cache): + # Hard-code the referent type pretty-printer for PropertyKey roots and + # handles. See the comment for mozilla.Root.Common.__init__. + return mozilla.Root.Rooted(value, cache, PropertyKey) + + +@pretty_printer("JS::Handle<long>") +def HandlePropertyKey(value, cache): + return mozilla.Root.Handle(value, cache, PropertyKey) + + +@pretty_printer("JS::MutableHandle<long>") +def MutableHandlePropertyKey(value, cache): + return mozilla.Root.MutableHandle(value, cache, PropertyKey) diff --git a/js/src/gdb/mozilla/Root.py b/js/src/gdb/mozilla/Root.py new file mode 100644 index 0000000000..fa80f3fec6 --- /dev/null +++ b/js/src/gdb/mozilla/Root.py @@ -0,0 +1,104 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. + +# Pretty-printers and utilities for SpiderMonkey rooting templates: +# Rooted, Handle, MutableHandle, etc. + +import mozilla.prettyprinters +from mozilla.prettyprinters import template_pretty_printer + +# Forget any printers from previous loads of this module. +mozilla.prettyprinters.clear_module_printers(__name__) + + +class Common(object): + # Common base class for all the rooting template pretty-printers. All these + # templates have one member holding the referent (or a pointer to it), so + # there's not much to it. + + # The name of the template member holding the referent. + member = "ptr" + + # If True, this is a handle type, and should be dereferenced. If False, + # the template member holds the referent directly. + handle = False + + # If True, we should strip typedefs from our referent type. (Rooted<T> + # uses template magic that gives the referent a noisy type.) + strip_typedefs = False + + # Initialize a pretty-printer for |value|, using |cache|. + # + # If given, |content_printer| is a pretty-printer constructor to use for + # this handle/root/etc.'s referent. Usually, we can just omit this argument + # and let GDB choose a pretty-printer for the referent given its type, but + # when the referent is a typedef of an integral type (say, |jsid| in a + # non-|DEBUG| build), the GNU toolchain (at least) loses the typedef name, + # and all we know about the referent is its fundamental integer type --- + # |JS::Rooted<jsid>|, for example, appears in GDB as |JS::Rooted<long>| --- + # and we are left with no way to choose a meaningful pretty-printer based on + # the type of the referent alone. However, because we know that the only + # integer type for which |JS::Rooted| is likely to be instantiated is + # |jsid|, we *can* register a pretty-printer constructor for the full + # instantiation |JS::Rooted<long>|. That constructor creates a |JS::Rooted| + # pretty-printer, and explicitly specifies the constructor for the referent, + # using this initializer's |content_printer| argument. + def __init__(self, value, cache, content_printer=None): + self.value = value + self.cache = cache + self.content_printer = content_printer + + def to_string(self): + ptr = self.value[self.member] + if self.handle: + ptr = ptr.dereference() + if self.strip_typedefs: + ptr = ptr.cast(ptr.type.strip_typedefs()) + if self.content_printer: + return self.content_printer(ptr, self.cache).to_string() + else: + # As of 2012-11, GDB suppresses printing pointers in replacement + # values; see http://sourceware.org/ml/gdb/2012-11/msg00055.html + # That means that simply returning the 'ptr' member won't work. + # Instead, just invoke GDB's formatter ourselves. + return str(ptr) + + +@template_pretty_printer("JS::Rooted") +class Rooted(Common): + strip_typedefs = True + + +@template_pretty_printer("JS::Handle") +class Handle(Common): + handle = True + + +@template_pretty_printer("JS::MutableHandle") +class MutableHandle(Common): + handle = True + + +@template_pretty_printer("js::BarrieredBase") +class BarrieredBase(Common): + member = "value" + + +def deref(root): + # Return the referent of a HeapPtr, Rooted, or Handle. + tag = root.type.strip_typedefs().tag + if not tag: + raise TypeError( + "Can't dereference type with no structure tag: %s" % (root.type,) + ) + elif tag.startswith("js::HeapPtr<"): + return root["value"] + elif tag.startswith("JS::Rooted<"): + return root["ptr"] + elif tag.startswith("JS::Handle<"): + return root["ptr"] + elif tag.startswith("js::GCPtr<"): + return root["value"] + else: + raise NotImplementedError("Unrecognized tag: " + tag) diff --git a/js/src/gdb/mozilla/__init__.py b/js/src/gdb/mozilla/__init__.py new file mode 100644 index 0000000000..7084d5052e --- /dev/null +++ b/js/src/gdb/mozilla/__init__.py @@ -0,0 +1,5 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. + +# Yes, Python, this is a package. diff --git a/js/src/gdb/mozilla/asmjs.py b/js/src/gdb/mozilla/asmjs.py new file mode 100644 index 0000000000..85a19a5cfc --- /dev/null +++ b/js/src/gdb/mozilla/asmjs.py @@ -0,0 +1,51 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. + +""" +In asm code, out-of-bounds heap accesses cause segfaults, which the engine +handles internally. Make GDB ignore them. +""" + +import gdb + +SIGSEGV = 11 + +# A sigaction buffer for each inferior process. +sigaction_buffers = {} + + +def on_stop(event): + if isinstance(event, gdb.SignalEvent) and event.stop_signal == "SIGSEGV": + # Allocate memory for sigaction, once per js shell process. + process = gdb.selected_inferior() + buf = sigaction_buffers.get(process) + if buf is None: + buf = gdb.parse_and_eval( + "(struct sigaction *) malloc(sizeof(struct sigaction))" + ) + sigaction_buffers[process] = buf + + # See if WasmFaultHandler is installed as the SIGSEGV signal action. + sigaction_fn = gdb.parse_and_eval( + "(void(*)(int,void*,void*))__sigaction" + ).dereference() + sigaction_fn(SIGSEGV, 0, buf) + WasmTrapHandler = gdb.parse_and_eval("WasmTrapHandler") + if buf["__sigaction_handler"]["sa_handler"] == WasmTrapHandler: + # Advise the user that magic is happening. + print("js/src/gdb/mozilla/asmjs.py: Allowing WasmTrapHandler to run.") + + # If WasmTrapHandler doesn't handle this segfault, it will unhook + # itself and re-raise. + gdb.execute("continue") + + +def on_exited(event): + if event.inferior in sigaction_buffers: + del sigaction_buffers[event.inferior] + + +def install(): + gdb.events.stop.connect(on_stop) + gdb.events.exited.connect(on_exited) diff --git a/js/src/gdb/mozilla/autoload.py b/js/src/gdb/mozilla/autoload.py new file mode 100644 index 0000000000..bc62e04555 --- /dev/null +++ b/js/src/gdb/mozilla/autoload.py @@ -0,0 +1,41 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. + +# mozilla/autoload.py: Autoload SpiderMonkey pretty-printers. + +print("Loading JavaScript value pretty-printers; see js/src/gdb/README.") +print("If they cause trouble, type: disable pretty-printer .* SpiderMonkey") + +import gdb.printing + +import mozilla.ExecutableAllocator + +# Import the pretty-printer modules. As a side effect, loading these +# modules registers their printers with mozilla.prettyprinters. +import mozilla.GCCellPtr +import mozilla.Interpreter +import mozilla.IonGraph +import mozilla.JSObject +import mozilla.jsop +import mozilla.JSString +import mozilla.JSSymbol +import mozilla.jsval +import mozilla.prettyprinters +import mozilla.PropertyKey +import mozilla.Root +import mozilla.unwind + +# The user may have personal pretty-printers. Get those, too, if they exist. +try: + import my_mozilla_printers # NOQA: F401 +except ImportError: + pass + + +def register(objfile): + # Register our pretty-printers with |objfile|. + lookup = mozilla.prettyprinters.lookup_for_objfile(objfile) + if lookup: + gdb.printing.register_pretty_printer(objfile, lookup, replace=True) + mozilla.unwind.register_unwinder(objfile) diff --git a/js/src/gdb/mozilla/jitsrc.py b/js/src/gdb/mozilla/jitsrc.py new file mode 100644 index 0000000000..9a9ffff28b --- /dev/null +++ b/js/src/gdb/mozilla/jitsrc.py @@ -0,0 +1,161 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. + +# This is a gdb extension to automate the process of tracing backwards in rr +# from a jit instruction to the code that generated that instruction. +# +# Usage: +# (rr) x/i $pc +# => 0x240e954ac13a: pushq (%rbx) +# (rr) jitsrc 0x240e954ac13a + +import re + +import gdb + +# (base_name, hops, func_name, source_var, dest_var) tuples, such that : +# - `base_name`: a regex matching the name of the function that implements +# the actual write +# - `hops`: the number of stack frames between `base_name` and `func_name` +# - `func_name`: a regex matching the name of the function that calls memcpy +# - `source_var`: an expression that can be evaluated in the frame +# corresponding to `func_name` to get the source of the memcpy +# - `dest_var`: an expression that can be evaluated in the frame +# corresponding to `func_name` to get the destination of the memcpy +# +# If an invocation of `jitsrc` stops in the middle of a memcpy, the solution +# is normally to add a new pattern here. +patterns = [ + ( + "__memmove_(avx|evex)_unaligned_erms", + 1, + "js::jit::X86Encoding::BaseAssembler::executableCopy", + "src", + "dst", + ), + ( + "__memcpy_(avx|evex)_unaligned", + 1, + "js::jit::X86Encoding::BaseAssembler::executableCopy", + "src", + "dst", + ), + ( + "__memmove_(avx|evex)_unaligned_erms", + 1, + "arena_t::RallocSmallOrLarge", + "aPtr", + "ret", + ), + ("__memcpy_(avx|evex)_unaligned", 1, "arena_t::RallocSmallOrLarge", "aPtr", "ret"), + ( + "mozilla::detail::VectorImpl<.*>::new_<.*>", + 3, + "mozilla::Vector<.*>::convertToHeapStorage", + "beginNoCheck()", + "newBuf", + ), + ( + "__memmove_(avx|evex)_unaligned_erms", + 1, + "js::jit::AssemblerBufferWithConstantPools", + "&cur->instructions[0]", + "dest", + ), + ( + "__memcpy_sse2_unaligned", + 1, + "js::jit::AssemblerBufferWithConstantPools", + "&cur->instructions[0]", + "dest", + ), + ( + "__memcpy_sse2_unaligned", + 2, + "js::jit::AssemblerX86Shared::executableCopy", + "masm.m_formatter.m_buffer.m_buffer.mBegin", + "buffer", + ), + ("__memcpy_sse2_unaligned", 1, "arena_t::RallocSmallOrLarge", "aPtr", "ret"), + ("js::jit::X86Encoding::SetInt32", 0, "js::jit::X86Encoding::SetInt32", "0", "0"), + ( + "js::jit::X86Encoding::SetPointer", + 0, + "js::jit::X86Encoding::SetPointer", + "0", + "0", + ), + ( + "<unnamed>", + 1, + "js::jit::AssemblerBufferWithConstantPools<.*>::executableCopy", + "&cur->instructions[0]", + "dest", + ), + ("std::__copy_move", 4, "CopySpan", "source.data()", "target.data()"), + ( + "__memmove_(avx|evex)_unaligned_erms", + 1, + "mozilla::detail::EndianUtils::copyAndSwapTo<.*0,.*0", + "aSrc", + "(size_t) aDest", + ), +] + + +class JitSource(gdb.Command): + def __init__(self): + super(JitSource, self).__init__("jitsrc", gdb.COMMAND_RUNNING) + self.dont_repeat() + + def disable_breakpoints(self): + self.disabled_breakpoints = [b for b in gdb.breakpoints() if b.enabled] + for b in self.disabled_breakpoints: + b.enabled = False + + def enable_breakpoints(self): + for b in self.disabled_breakpoints: + b.enabled = True + + def search_stack(self, base_name, hops, name, src, dst, address): + current_frame_name = gdb.newest_frame().name() or "<unnamed>" + if not re.match(base_name, current_frame_name): + return None + f = gdb.newest_frame() + for _ in range(hops): + f = f.older() + if not re.match(name, f.name()): + return None + f.select() + src_val = gdb.parse_and_eval(src) + dst_val = gdb.parse_and_eval(dst) + return hex(src_val + int(address, 16) - dst_val) + + def next_address(self, old): + for pattern in patterns: + found = self.search_stack(*pattern, old) + if found: + return found + return None + + def runback(self, address): + b = gdb.Breakpoint( + "*" + address, type=gdb.BP_WATCHPOINT, wp_class=gdb.WP_WRITE, internal=True + ) + while b.hit_count == 0: + gdb.execute("rc", to_string=True) + b.delete() + + def invoke(self, arg, from_tty): + args = gdb.string_to_argv(arg) + address = args[0] + self.disable_breakpoints() + while address: + self.runback(address) + address = self.next_address(address) + self.enable_breakpoints() + + +# Register to the gdb runtime +JitSource() diff --git a/js/src/gdb/mozilla/jsop.py b/js/src/gdb/mozilla/jsop.py new file mode 100644 index 0000000000..635acb7528 --- /dev/null +++ b/js/src/gdb/mozilla/jsop.py @@ -0,0 +1,60 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. + +# Pretty-printers for JSOp and jsbytecode values. + +import gdb +import gdb.types + +import mozilla.prettyprinters +from mozilla.prettyprinters import pretty_printer, ptr_pretty_printer + +# Forget any printers from previous loads of this module. +mozilla.prettyprinters.clear_module_printers(__name__) + + +class JSOpTypeCache(object): + # Cache information about the JSOp type for this objfile. + def __init__(self, cache): + self.tJSOp = gdb.lookup_type("JSOp") + + @classmethod + def get_or_create(cls, cache): + if not cache.mod_JSOp: + cache.mod_JSOp = cls(cache) + return cache.mod_JSOp + + +@pretty_printer("JSOp") +class JSOp(object): + def __init__(self, value, cache): + self.value = value + self.cache = cache + self.jotc = JSOpTypeCache.get_or_create(cache) + + def to_string(self): + # JSOp's storage type is |uint8_t|, but gdb uses a signed value. + # Manually convert it to an unsigned value. + # + # https://sourceware.org/bugzilla/show_bug.cgi?id=25325 + idx = int(self.value.cast(self.jotc.tJSOp.target())) + assert 0 <= idx and idx <= 255 + fields = self.jotc.tJSOp.fields() + if idx < len(fields): + return fields[idx].name + return "(JSOp) {:d}".format(idx) + + +@ptr_pretty_printer("jsbytecode") +class JSBytecodePtr(mozilla.prettyprinters.Pointer): + def __init__(self, value, cache): + super(JSBytecodePtr, self).__init__(value, cache) + self.jotc = JSOpTypeCache.get_or_create(cache) + + def to_string(self): + try: + opcode = str(self.value.dereference().cast(self.jotc.tJSOp)) + except Exception: + opcode = "bad pc" + return "{} ({})".format(self.value.cast(self.cache.void_ptr_t), opcode) diff --git a/js/src/gdb/mozilla/jsval.py b/js/src/gdb/mozilla/jsval.py new file mode 100644 index 0000000000..d01a5ceacc --- /dev/null +++ b/js/src/gdb/mozilla/jsval.py @@ -0,0 +1,188 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. + +# Pretty-printers for SpiderMonkey's JS::Value. + +import struct + +import gdb +import gdb.types + +import mozilla.prettyprinters +from mozilla.prettyprinters import pretty_printer + +# Forget any printers from previous loads of this module. +mozilla.prettyprinters.clear_module_printers(__name__) + +# See Value.h [SMDOC] JS::Value Boxing Formats for details on the JS::Value boxing +# formats handled below. + + +class Box(object): + def __init__(self, asBits, jtc): + self.asBits = asBits + self.jtc = jtc + # Value::asBits is uint64_t, but somehow the sign bit can be botched + # here, even though Python integers are arbitrary precision. + if self.asBits < 0: + self.asBits = self.asBits + (1 << 64) + + # Return this value's type tag. + def tag(self): + raise NotImplementedError + + # Return this value as a 32-bit integer, double, or address. + def as_uint32(self): + raise NotImplementedError + + def as_double(self): + packed = struct.pack("q", self.asBits) + (unpacked,) = struct.unpack("d", packed) + return unpacked + + def as_address(self): + raise NotImplementedError + + +class Punbox(Box): + # Packed non-number boxing --- the format used on x86_64. It would be nice to + # simply call Value::toInt32, etc. here, but the debugger is likely to see many + # Values, and doing several inferior calls for each one seems like a bad idea. + + FULL_WIDTH = 64 + TAG_SHIFT = 47 + PAYLOAD_MASK = (1 << TAG_SHIFT) - 1 + TAG_MASK = (1 << (FULL_WIDTH - TAG_SHIFT)) - 1 + TAG_MAX_DOUBLE = 0x1FFF0 + TAG_TYPE_MASK = 0x0000F + + def tag(self): + tag = self.asBits >> Punbox.TAG_SHIFT + if tag <= Punbox.TAG_MAX_DOUBLE: + return self.jtc.DOUBLE + else: + return tag & Punbox.TAG_TYPE_MASK + + def as_uint32(self): + return int(self.asBits & ((1 << 32) - 1)) + + def as_address(self): + return gdb.Value(self.asBits & Punbox.PAYLOAD_MASK) + + +class Nunbox(Box): + TAG_SHIFT = 32 + TAG_CLEAR = 0xFFFF0000 + PAYLOAD_MASK = 0xFFFFFFFF + TAG_TYPE_MASK = 0x0000000F + + def tag(self): + tag = self.asBits >> Nunbox.TAG_SHIFT + if tag < Nunbox.TAG_CLEAR: + return self.jtc.DOUBLE + return tag & Nunbox.TAG_TYPE_MASK + + def as_uint32(self): + return int(self.asBits & Nunbox.PAYLOAD_MASK) + + def as_address(self): + return gdb.Value(self.asBits & Nunbox.PAYLOAD_MASK) + + +class JSValueTypeCache(object): + # Cache information about the Value type for this objfile. + + def __init__(self, cache): + # Capture the tag values. + d = gdb.types.make_enum_dict(gdb.lookup_type("JSValueType")) + + # The enum keys are prefixed when building with some compilers (clang at + # a minimum), so use a helper function to handle either key format. + def get(key): + val = d.get(key) + if val is not None: + return val + return d["JSValueType::" + key] + + self.DOUBLE = get("JSVAL_TYPE_DOUBLE") + self.INT32 = get("JSVAL_TYPE_INT32") + self.UNDEFINED = get("JSVAL_TYPE_UNDEFINED") + self.BOOLEAN = get("JSVAL_TYPE_BOOLEAN") + self.MAGIC = get("JSVAL_TYPE_MAGIC") + self.STRING = get("JSVAL_TYPE_STRING") + self.SYMBOL = get("JSVAL_TYPE_SYMBOL") + self.BIGINT = get("JSVAL_TYPE_BIGINT") + self.NULL = get("JSVAL_TYPE_NULL") + self.OBJECT = get("JSVAL_TYPE_OBJECT") + + # Let self.magic_names be an array whose i'th element is the name of + # the i'th magic value. + d = gdb.types.make_enum_dict(gdb.lookup_type("JSWhyMagic")) + self.magic_names = list(range(max(d.values()) + 1)) + for (k, v) in d.items(): + self.magic_names[v] = k + + # Choose an unboxing scheme for this architecture. + self.boxer = Punbox if cache.void_ptr_t.sizeof == 8 else Nunbox + + +@pretty_printer("JS::Value") +class JSValue(object): + def __init__(self, value, cache): + # Save the generic typecache, and create our own, if we haven't already. + self.cache = cache + if not cache.mod_JS_Value: + cache.mod_JS_Value = JSValueTypeCache(cache) + self.jtc = cache.mod_JS_Value + + self.value = value + self.box = self.jtc.boxer(value["asBits_"], self.jtc) + + def to_string(self): + tag = self.box.tag() + + if tag == self.jtc.UNDEFINED: + return "$JS::UndefinedValue()" + if tag == self.jtc.NULL: + return "$JS::NullValue()" + if tag == self.jtc.BOOLEAN: + return "$JS::BooleanValue(%s)" % str(self.box.as_uint32() != 0).lower() + if tag == self.jtc.MAGIC: + value = self.box.as_uint32() + if 0 <= value and value < len(self.jtc.magic_names): + return "$JS::MagicValue(%s)" % (self.jtc.magic_names[value],) + else: + return "$JS::MagicValue(%d)" % (value,) + + if tag == self.jtc.INT32: + value = self.box.as_uint32() + signbit = 1 << 31 + value = (value ^ signbit) - signbit + return "$JS::Int32Value(%s)" % value + + if tag == self.jtc.DOUBLE: + return "$JS::DoubleValue(%s)" % self.box.as_double() + + if tag == self.jtc.STRING: + value = self.box.as_address().cast(self.cache.JSString_ptr_t) + elif tag == self.jtc.OBJECT: + value = self.box.as_address().cast(self.cache.JSObject_ptr_t) + elif tag == self.jtc.SYMBOL: + value = self.box.as_address().cast(self.cache.JSSymbol_ptr_t) + elif tag == self.jtc.BIGINT: + return "$JS::BigIntValue()" + else: + value = "unrecognized!" + return "$JS::Value(%s)" % (value,) + + def is_undefined(self): + return self.box.tag() == self.jtc.UNDEFINED + + def get_string(self): + assert self.box.tag() == self.jtc.STRING + return self.box.as_address().cast(self.cache.JSString_ptr_t) + + def get_private(self): + assert self.box.tag() == self.jtc.DOUBLE + return self.box.asBits diff --git a/js/src/gdb/mozilla/prettyprinters.py b/js/src/gdb/mozilla/prettyprinters.py new file mode 100644 index 0000000000..130e7d847c --- /dev/null +++ b/js/src/gdb/mozilla/prettyprinters.py @@ -0,0 +1,447 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. + +# mozilla/prettyprinters.py --- infrastructure for SpiderMonkey's auto-loaded pretty-printers. + +import re + +import gdb + +# Decorators for declaring pretty-printers. +# +# In each case, the decoratee should be a SpiderMonkey-style pretty-printer +# factory, taking both a gdb.Value instance and a TypeCache instance as +# arguments; see TypeCache, below. + + +def check_for_reused_pretty_printer(fn): + # Check that |fn| hasn't been registered as a pretty-printer under some + # other name already. (The 'enabled' flags used by GDB's + # 'enable/disable/info pretty-printer' commands are simply stored as + # properties of the function objects themselves, so a single function + # object can't carry the 'enabled' flags for two different printers.) + if hasattr(fn, "enabled"): + raise RuntimeError("pretty-printer function %r registered more than once" % fn) + + +# a dictionary mapping gdb.Type tags to pretty-printer functions. +printers_by_tag = {} + +# A decorator: add the decoratee as a pretty-printer lookup function for types +# named |type_name|. + + +def pretty_printer(type_name): + def add(fn): + check_for_reused_pretty_printer(fn) + add_to_subprinter_list(fn, type_name) + printers_by_tag[type_name] = fn + return fn + + return add + + +# a dictionary mapping gdb.Type tags to pretty-printer functions for pointers to +# that type. +ptr_printers_by_tag = {} + +# A decorator: add the decoratee as a pretty-printer lookup function for +# pointers to types named |type_name|. + + +def ptr_pretty_printer(type_name): + def add(fn): + check_for_reused_pretty_printer(fn) + add_to_subprinter_list(fn, "ptr-to-" + type_name) + ptr_printers_by_tag[type_name] = fn + return fn + + return add + + +# a dictionary mapping gdb.Type tags to pretty-printer functions for +# references to that type. +ref_printers_by_tag = {} + +# A decorator: add the decoratee as a pretty-printer lookup function for +# references to instances of types named |type_name|. + + +def ref_pretty_printer(type_name): + def add(fn): + check_for_reused_pretty_printer(fn) + add_to_subprinter_list(fn, "ref-to-" + type_name) + ref_printers_by_tag[type_name] = fn + return fn + + return add + + +# a dictionary mapping the template name portion of gdb.Type tags to +# pretty-printer functions for instantiations of that template. +template_printers_by_tag = {} + +# A decorator: add the decoratee as a pretty-printer lookup function for +# instantiations of templates named |template_name|. + + +def template_pretty_printer(template_name): + def add(fn): + check_for_reused_pretty_printer(fn) + add_to_subprinter_list(fn, "instantiations-of-" + template_name) + template_printers_by_tag[template_name] = fn + return fn + + return add + + +# A list of (REGEXP, PRINTER) pairs, such that if REGEXP (a RegexObject) +# matches the result of converting a gdb.Value's type to a string, then +# PRINTER is a pretty-printer lookup function that will probably like that +# value. +printers_by_regexp = [] + +# A decorator: add the decoratee as a pretty-printer factory for types +# that, when converted to a string, match |pattern|. Use |name| as the +# pretty-printer's name, when listing, enabling and disabling. + + +def pretty_printer_for_regexp(pattern, name): + compiled = re.compile(pattern) + + def add(fn): + check_for_reused_pretty_printer(fn) + add_to_subprinter_list(fn, name) + printers_by_regexp.append((compiled, fn)) + return fn + + return add + + +# Forget all pretty-printer lookup functions defined in the module name +# |module_name|, if any exist. Use this at the top of each pretty-printer +# module like this: +# +# clear_module_printers(__name__) + + +def clear_module_printers(module_name): + global printers_by_tag, ptr_printers_by_tag, ref_printers_by_tag + global template_printers_by_tag, printers_by_regexp + + # Remove all pretty-printers defined in the module named |module_name| + # from d. + def clear_dictionary(d): + # Walk the dictionary, building a list of keys whose entries we + # should remove. (It's not safe to delete entries from a dictionary + # while we're iterating over it.) + to_delete = [] + for (k, v) in d.items(): + if v.__module__ == module_name: + to_delete.append(k) + remove_from_subprinter_list(v) + for k in to_delete: + del d[k] + + clear_dictionary(printers_by_tag) + clear_dictionary(ptr_printers_by_tag) + clear_dictionary(ref_printers_by_tag) + clear_dictionary(template_printers_by_tag) + + # Iterate over printers_by_regexp, deleting entries from the given module. + new_list = [] + for p in printers_by_regexp: + if p.__module__ == module_name: + remove_from_subprinter_list(p) + else: + new_list.append(p) + printers_by_regexp = new_list + + +# Our subprinters array. The 'subprinters' attributes of all lookup +# functions returned by lookup_for_objfile point to this array instance, +# which we mutate as subprinters are added and removed. +subprinters = [] + +# Set up the 'name' and 'enabled' attributes on |subprinter|, and add it to our +# list of all SpiderMonkey subprinters. + + +def add_to_subprinter_list(subprinter, name): + subprinter.name = name + subprinter.enabled = True + subprinters.append(subprinter) + + +# Remove |subprinter| from our list of all SpiderMonkey subprinters. + + +def remove_from_subprinter_list(subprinter): + subprinters.remove(subprinter) + + +# An exception class meaning, "This objfile has no SpiderMonkey in it." + + +class NotSpiderMonkeyObjfileError(TypeError): + pass + + +# TypeCache: a cache for frequently used information about an objfile. +# +# When a new SpiderMonkey objfile is loaded, we construct an instance of +# this class for it. Then, whenever we construct a pretty-printer for some +# gdb.Value, we also pass, as a second argument, the TypeCache for the +# objfile to which that value's type belongs. +# +# if objfile doesn't seem to have SpiderMonkey code in it, the constructor +# raises NotSpiderMonkeyObjfileError. +# +# Pretty-printer modules may add attributes to this to hold their own +# cached values. Such attributes should be named mod_NAME, where the module +# is named mozilla.NAME; for example, mozilla.JSString should store its +# metadata in the TypeCache's mod_JSString attribute. + + +class TypeCache(object): + def __init__(self, objfile): + self.objfile = objfile + + # Unfortunately, the Python interface doesn't allow us to specify + # the objfile in whose scope lookups should occur. But simply + # knowing that we need to lookup the types afresh is probably + # enough. + self.void_t = gdb.lookup_type("void") + self.void_ptr_t = self.void_t.pointer() + self.uintptr_t = gdb.lookup_type("uintptr_t") + try: + self.JSString_ptr_t = gdb.lookup_type("JSString").pointer() + self.JSSymbol_ptr_t = gdb.lookup_type("JS::Symbol").pointer() + self.JSObject_ptr_t = gdb.lookup_type("JSObject").pointer() + except gdb.error: + raise NotSpiderMonkeyObjfileError + + self.mod_GCCellPtr = None + self.mod_Interpreter = None + self.mod_JSObject = None + self.mod_JSOp = None + self.mod_JSString = None + self.mod_JS_Value = None + self.mod_ExecutableAllocator = None + self.mod_IonGraph = None + + +# Yield a series of all the types that |t| implements, by following typedefs +# and iterating over base classes. Specifically: +# - |t| itself is the first value yielded. +# - If we yield a typedef, we later yield its definition. +# - If we yield a type with base classes, we later yield those base classes. +# - If we yield a type with some base classes that are typedefs, +# we yield all the type's base classes before following the typedefs. +# +# This is a hokey attempt to order the implemented types by meaningfulness when +# pretty-printed. Perhaps it is entirely misguided, and we should actually +# collect all applicable pretty-printers, and then use some ordering on the +# pretty-printers themselves. +# +# We may yield a type more than once (say, if it appears more than once in the +# class hierarchy). + + +def implemented_types(t): + + # Yield all types that follow |t|. + def followers(t): + if t.code == gdb.TYPE_CODE_TYPEDEF: + yield t.target() + for t2 in followers(t.target()): + yield t2 + elif is_struct_or_union(t): + base_classes = [] + for f in t.fields(): + if f.is_base_class: + yield f.type + base_classes.append(f.type) + for b in base_classes: + for t2 in followers(b): + yield t2 + + yield t + for t2 in followers(t): + yield t2 + + +template_regexp = re.compile("([\w_:]+)<") + + +def is_struct_or_union(t): + return t.code in (gdb.TYPE_CODE_STRUCT, gdb.TYPE_CODE_UNION) + + +def is_struct_or_union_or_enum(t): + return t.code in (gdb.TYPE_CODE_STRUCT, gdb.TYPE_CODE_UNION, gdb.TYPE_CODE_ENUM) + + +# Construct and return a pretty-printer lookup function for objfile, or +# return None if the objfile doesn't contain SpiderMonkey code +# (specifically, definitions for SpiderMonkey types). + + +def lookup_for_objfile(objfile): + # Create a type cache for this objfile. + try: + cache = TypeCache(objfile) + except NotSpiderMonkeyObjfileError: + if gdb.parameter("verbose"): + gdb.write( + "objfile '%s' has no SpiderMonkey code; not registering pretty-printers\n" + % (objfile.filename,) + ) + return None + + # Return a pretty-printer for |value|, if we have one. This is the lookup + # function object we place in each gdb.Objfile's pretty-printers list, so it + # carries |name|, |enabled|, and |subprinters| attributes. + def lookup(value): + # If |table| has a pretty-printer for |tag|, apply it to |value|. + def check_table(table, tag): + if tag in table: + f = table[tag] + if f.enabled: + return f(value, cache) + return None + + def check_table_by_type_name(table, t): + if t.code == gdb.TYPE_CODE_TYPEDEF: + return check_table(table, str(t)) + elif is_struct_or_union_or_enum(t) and t.tag: + return check_table(table, t.tag) + else: + return None + + for t in implemented_types(value.type): + if t.code == gdb.TYPE_CODE_PTR: + for t2 in implemented_types(t.target()): + p = check_table_by_type_name(ptr_printers_by_tag, t2) + if p: + return p + elif t.code == gdb.TYPE_CODE_REF: + for t2 in implemented_types(t.target()): + p = check_table_by_type_name(ref_printers_by_tag, t2) + if p: + return p + else: + p = check_table_by_type_name(printers_by_tag, t) + if p: + return p + if is_struct_or_union(t) and t.tag: + m = template_regexp.match(t.tag) + if m: + p = check_table(template_printers_by_tag, m.group(1)) + if p: + return p + + # Failing that, look for a printer in printers_by_regexp. We have + # to scan the whole list, so regexp printers should be used + # sparingly. + s = str(value.type) + for (r, f) in printers_by_regexp: + if f.enabled: + m = r.match(s) + if m: + p = f(value, cache) + if p: + return p + + # No luck. + return None + + # Give |lookup| the attributes expected of a pretty-printer with + # subprinters, for enabling and disabling. + lookup.name = "SpiderMonkey" + lookup.enabled = True + lookup.subprinters = subprinters + + return lookup + + +# A base class for pretty-printers for pointer values that handles null +# pointers, by declining to construct a pretty-printer for them at all. +# Derived classes may simply assume that self.value is non-null. +# +# To help share code, this class can also be used with reference types. +# +# This class provides the following methods, which subclasses are free to +# override: +# +# __init__(self, value, cache): Save value and cache as properties by those names +# on the instance. +# +# to_string(self): format the type's name and address, as GDB would, and then +# call a 'summary' method (which the subclass must define) to produce a +# description of the referent. +# +# Note that pretty-printers returning a 'string' display hint must not use +# this default 'to_string' method, as GDB will take everything it returns, +# including the type name and address, as string contents. + + +class Pointer(object): + def __new__(cls, value, cache): + # Don't try to provide pretty-printers for NULL pointers. + if value.type.strip_typedefs().code == gdb.TYPE_CODE_PTR and value == 0: + return None + return super(Pointer, cls).__new__(cls) + + def __init__(self, value, cache): + self.value = value + self.cache = cache + + def to_string(self): + # See comment above. + assert not hasattr(self, "display_hint") or self.display_hint() != "string" + concrete_type = self.value.type.strip_typedefs() + if concrete_type.code == gdb.TYPE_CODE_PTR: + address = self.value.cast(self.cache.void_ptr_t) + elif concrete_type.code == gdb.TYPE_CODE_REF: + address = "@" + str(self.value.address.cast(self.cache.void_ptr_t)) + else: + assert not "mozilla.prettyprinters.Pointer applied to bad value type" + try: + summary = self.summary() + except gdb.MemoryError as r: + summary = str(r) + v = "(%s) %s %s" % (self.value.type, address, summary) + return v + + def summary(self): + raise NotImplementedError + + +field_enum_value = None + +# Given |t|, a gdb.Type instance representing an enum type, return the +# numeric value of the enum value named |name|. +# +# Pre-2012-4-18 versions of GDB store the value of an enum member on the +# gdb.Field's 'bitpos' attribute; later versions store it on the 'enumval' +# attribute. This function retrieves the value from either. + + +def enum_value(t, name): + global field_enum_value + f = t[name] + # Monkey-patching is a-okay in polyfills! Just because. + if not field_enum_value: + if hasattr(f, "enumval"): + + def field_enum_value(f): + return f.enumval + + else: + + def field_enum_value(f): + return f.bitpos + + return field_enum_value(f) diff --git a/js/src/gdb/mozilla/unwind.py b/js/src/gdb/mozilla/unwind.py new file mode 100644 index 0000000000..8ad4393b7a --- /dev/null +++ b/js/src/gdb/mozilla/unwind.py @@ -0,0 +1,596 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. + +# mozilla/unwind.py --- unwinder and frame filter for SpiderMonkey + +import platform + +import gdb +import gdb.types +from gdb.FrameDecorator import FrameDecorator + +from mozilla.JSObject import get_function_name, get_function_script +from mozilla.prettyprinters import TypeCache + +# For ease of use in Python 2, we use "long" instead of "int" +# everywhere. +try: + long +except NameError: + long = int + +# The Python 3 |map| built-in works lazily, but in Python 2 we need +# itertools.imap to get this. +try: + from itertools import imap +except ImportError: + imap = map + +_have_unwinder = True +try: + from gdb.unwinder import Unwinder +except ImportError: + _have_unwinder = False + # We need something here; it doesn't matter what as no unwinder + # will ever be instantiated. + Unwinder = object + + +def debug(something): + # print("@@ " + something) + pass + + +# Maps frametype enum base names to corresponding class. +SizeOfFramePrefix = { + "FrameType::IonJS": "ExitFrameLayout", + "FrameType::BaselineJS": "JitFrameLayout", + "FrameType::BaselineStub": "BaselineStubFrameLayout", + "FrameType::CppToJSJit": "JitFrameLayout", + "FrameType::WasmToJSJit": "JitFrameLayout", + "FrameType::JSJitToWasm": "JitFrameLayout", + "FrameType::Rectifier": "RectifierFrameLayout", + "FrameType::IonAccessorIC": "IonAccessorICFrameLayout", + "FrameType::IonICCall": "IonICCallFrameLayout", + "FrameType::Exit": "ExitFrameLayout", + "FrameType::Bailout": "JitFrameLayout", +} + + +# We cannot have semi-colon as identifier names, so use a colon instead, +# and forward the name resolution to the type cache class. +class UnwinderTypeCacheFrameType(object): + def __init__(self, tc): + self.tc = tc + + def __getattr__(self, name): + return self.tc.__getattr__("FrameType::" + name) + + +class UnwinderTypeCache(TypeCache): + # All types and symbols that we need are attached to an object that we + # can dispose of as needed. + + def __init__(self): + self.d = None + self.frame_enum_names = {} + self.frame_class_types = {} + super(UnwinderTypeCache, self).__init__(None) + + # We take this bizarre approach to defer trying to look up any + # symbols until absolutely needed. Without this, the loading + # approach taken by the gdb-tests would cause spurious exceptions. + def __getattr__(self, name): + if self.d is None: + self.initialize() + if name == "frame_type": + return UnwinderTypeCacheFrameType(self) + if name not in self.d: + return None + return self.d[name] + + def value(self, name): + return long(gdb.lookup_symbol(name)[0].value()) + + def jit_value(self, name): + return self.value("js::jit::" + name) + + def initialize(self): + self.d = {} + self.d["FRAMETYPE_MASK"] = (1 << self.jit_value("FRAMETYPE_BITS")) - 1 + self.d["FRAMESIZE_SHIFT"] = self.jit_value("FRAMESIZE_SHIFT") + self.d["FRAME_HEADER_SIZE_SHIFT"] = self.jit_value("FRAME_HEADER_SIZE_SHIFT") + self.d["FRAME_HEADER_SIZE_MASK"] = self.jit_value("FRAME_HEADER_SIZE_MASK") + + self.compute_frame_info() + commonFrameLayout = gdb.lookup_type("js::jit::CommonFrameLayout") + self.d["typeCommonFrameLayout"] = commonFrameLayout + self.d["typeCommonFrameLayoutPointer"] = commonFrameLayout.pointer() + self.d["per_tls_context"] = gdb.lookup_global_symbol("js::TlsContext") + + self.d["void_starstar"] = gdb.lookup_type("void").pointer().pointer() + + jitframe = gdb.lookup_type("js::jit::JitFrameLayout") + self.d["jitFrameLayoutPointer"] = jitframe.pointer() + + self.d["CalleeToken_Function"] = self.jit_value("CalleeToken_Function") + self.d["CalleeToken_FunctionConstructing"] = self.jit_value( + "CalleeToken_FunctionConstructing" + ) + self.d["CalleeToken_Script"] = self.jit_value("CalleeToken_Script") + self.d["JSScript"] = gdb.lookup_type("JSScript").pointer() + self.d["Value"] = gdb.lookup_type("JS::Value") + + self.d["SOURCE_SLOT"] = self.value("js::ScriptSourceObject::SOURCE_SLOT") + self.d["NativeObject"] = gdb.lookup_type("js::NativeObject").pointer() + self.d["HeapSlot"] = gdb.lookup_type("js::HeapSlot").pointer() + self.d["ScriptSource"] = gdb.lookup_type("js::ScriptSource").pointer() + + # ProcessExecutableMemory, used to identify if a pc is in the section + # pre-allocated by the JIT. + self.d["MaxCodeBytesPerProcess"] = self.jit_value("MaxCodeBytesPerProcess") + self.d["execMemory"] = gdb.lookup_symbol("::execMemory")[0].value() + + # Compute maps related to jit frames. + def compute_frame_info(self): + t = gdb.lookup_type("enum js::jit::FrameType") + for field in t.fields(): + # Strip off "js::jit::", remains: "FrameType::*". + name = field.name[9:] + enumval = long(field.enumval) + self.d[name] = enumval + self.frame_enum_names[enumval] = name + class_type = gdb.lookup_type("js::jit::" + SizeOfFramePrefix[name]) + self.frame_class_types[enumval] = class_type.pointer() + + +class FrameSymbol(object): + "A symbol/value pair as expected from gdb frame decorators." + + def __init__(self, sym, val): + self.sym = sym + self.val = val + + def symbol(self): + return self.sym + + def value(self): + return self.val + + +class JitFrameDecorator(FrameDecorator): + """This represents a single JIT frame for the purposes of display. + That is, the frame filter creates instances of this when it sees a + JIT frame in the stack.""" + + def __init__(self, base, info, cache): + super(JitFrameDecorator, self).__init__(base) + self.info = info + self.cache = cache + + def _decode_jitframe(self, this_frame): + calleetoken = long(this_frame["calleeToken_"]) + tag = calleetoken & 3 + calleetoken = calleetoken ^ tag + function = None + script = None + if ( + tag == self.cache.CalleeToken_Function + or tag == self.cache.CalleeToken_FunctionConstructing + ): + value = gdb.Value(calleetoken) + function = get_function_name(value, self.cache) + script = get_function_script(value, self.cache) + elif tag == self.cache.CalleeToken_Script: + script = gdb.Value(calleetoken).cast(self.cache.JSScript) + return {"function": function, "script": script} + + def function(self): + if self.info["name"] is None: + return FrameDecorator.function(self) + name = self.info["name"] + result = "<<" + name + # If we have a frame, we can extract the callee information + # from it for display here. + this_frame = self.info["this_frame"] + if this_frame is not None: + if gdb.types.has_field(this_frame.type.target(), "calleeToken_"): + function = self._decode_jitframe(this_frame)["function"] + if function is not None: + result = result + " " + function + return result + ">>" + + def filename(self): + this_frame = self.info["this_frame"] + if this_frame is not None: + if gdb.types.has_field(this_frame.type.target(), "calleeToken_"): + script = self._decode_jitframe(this_frame)["script"] + if script is not None: + obj = script["sourceObject_"]["value"] + # Verify that this is a ScriptSource object. + # FIXME should also deal with wrappers here. + nativeobj = obj.cast(self.cache.NativeObject) + # See bug 987069 and despair. At least this + # approach won't give exceptions. + class_name = nativeobj["group_"]["value"]["clasp_"]["name"].string( + "ISO-8859-1" + ) + if class_name != "ScriptSource": + return FrameDecorator.filename(self) + scriptsourceobj = (nativeobj + 1).cast(self.cache.HeapSlot)[ + self.cache.SOURCE_SLOT + ] + scriptsource = scriptsourceobj["value"]["asBits_"] << 1 + scriptsource = scriptsource.cast(self.cache.ScriptSource) + return scriptsource["filename_"]["mTuple"]["mFirstA"].string() + return FrameDecorator.filename(self) + + def frame_args(self): + this_frame = self.info["this_frame"] + if this_frame is None: + return FrameDecorator.frame_args(self) + if not gdb.types.has_field(this_frame.type.target(), "numActualArgs_"): + return FrameDecorator.frame_args(self) + # See if this is a function call. + if self._decode_jitframe(this_frame)["function"] is None: + return FrameDecorator.frame_args(self) + # Construct and return an iterable of all the arguments. + result = [] + num_args = long(this_frame["numActualArgs_"]) + # Sometimes we see very large values here, so truncate it to + # bypass the damage. + if num_args > 10: + num_args = 10 + args_ptr = (this_frame + 1).cast(self.cache.Value.pointer()) + for i in range(num_args + 1): + # Synthesize names, since there doesn't seem to be + # anything better to do. + if i == 0: + name = "this" + else: + name = "arg%d" % i + result.append(FrameSymbol(name, args_ptr[i])) + return result + + +class SpiderMonkeyFrameFilter(object): + "A frame filter for SpiderMonkey." + + # |state_holder| is either None, or an instance of + # SpiderMonkeyUnwinder. If the latter, then this class will + # reference the |unwinder_state| attribute to find the current + # unwinder state. + def __init__(self, cache, state_holder): + self.name = "SpiderMonkey" + self.enabled = True + self.priority = 100 + self.state_holder = state_holder + self.cache = cache + + def maybe_wrap_frame(self, frame): + if self.state_holder is None or self.state_holder.unwinder_state is None: + return frame + base = frame.inferior_frame() + info = self.state_holder.unwinder_state.get_frame(base) + if info is None: + return frame + return JitFrameDecorator(frame, info, self.cache) + + def filter(self, frame_iter): + return imap(self.maybe_wrap_frame, frame_iter) + + +class SpiderMonkeyFrameId(object): + "A frame id class, as specified by the gdb unwinder API." + + def __init__(self, sp, pc): + self.sp = sp + self.pc = pc + + +class UnwinderState(object): + """This holds all the state needed during a given unwind. Each time a + new unwind is done, a new instance of this class is created. It + keeps track of all the state needed to unwind JIT frames. Note that + this class is not directly instantiated. + + This is a base class, and must be specialized for each target + architecture, both because we need to use arch-specific register + names, and because entry frame unwinding is arch-specific. + See https://sourceware.org/bugzilla/show_bug.cgi?id=19286 for info + about the register name issue. + + Each subclass must define SP_REGISTER, PC_REGISTER, and + SENTINEL_REGISTER (see x64UnwinderState for info); and implement + unwind_entry_frame_registers.""" + + def __init__(self, typecache): + self.next_sp = None + self.next_type = None + self.activation = None + # An unwinder instance is specific to a thread. Record the + # selected thread for later verification. + self.thread = gdb.selected_thread() + self.frame_map = {} + self.typecache = typecache + + # If the given gdb.Frame was created by this unwinder, return the + # corresponding informational dictionary for the frame. + # Otherwise, return None. This is used by the frame filter to + # display extra information about the frame. + def get_frame(self, frame): + sp = long(frame.read_register(self.SP_REGISTER)) + if sp in self.frame_map: + return self.frame_map[sp] + return None + + # Add information about a frame to the frame map. This map is + # queried by |self.get_frame|. |sp| is the frame's stack pointer, + # and |name| the frame's type as a string, e.g. "FrameType::Exit". + def add_frame(self, sp, name=None, this_frame=None): + self.frame_map[long(sp)] = {"name": name, "this_frame": this_frame} + + # See whether |pc| is claimed by the Jit. + def is_jit_address(self, pc): + execMem = self.typecache.execMemory + base = long(execMem["base_"]) + length = self.typecache.MaxCodeBytesPerProcess + + # If the base pointer is null, then no memory got allocated yet. + if long(base) == 0: + return False + + # If allocated, then we allocated MaxCodeBytesPerProcess. + return base <= pc and pc < base + length + + # Check whether |self| is valid for the selected thread. + def check(self): + return gdb.selected_thread() is self.thread + + # Essentially js::TlsContext.get(). + def get_tls_context(self): + return self.typecache.per_tls_context.value()["mValue"] + + # |common| is a pointer to a CommonFrameLayout object. Return a + # tuple (local_size, header_size, frame_type), where |size| is the + # integer size of the previous frame's locals; |header_size| is + # the size of this frame's header; and |frame_type| is an integer + # representing the previous frame's type. + def unpack_descriptor(self, common): + value = long(common["descriptor_"]) + local_size = value >> self.typecache.FRAMESIZE_SHIFT + header_size = ( + value >> self.typecache.FRAME_HEADER_SIZE_SHIFT + ) & self.typecache.FRAME_HEADER_SIZE_MASK + header_size = header_size * self.typecache.void_starstar.sizeof + frame_type = long(value & self.typecache.FRAMETYPE_MASK) + if frame_type == self.typecache.frame_type.CppToJSJit: + # Trampoline-x64.cpp pushes a JitFrameLayout object, but + # the stack pointer is actually adjusted as if a + # CommonFrameLayout object was pushed. + header_size = self.typecache.typeCommonFrameLayout.sizeof + return (local_size, header_size, frame_type) + + # Create a new frame for gdb. This makes a new unwind info object + # and fills it in, then returns it. It also registers any + # pertinent information with the frame filter for later display. + # + # |pc| is the PC from the pending frame + # |sp| is the stack pointer to use + # |frame| points to the CommonFrameLayout object + # |frame_type| is a integer, one of the |enum FrameType| values, + # describing the current frame. + # |pending_frame| is the pending frame (see the gdb unwinder + # documentation). + def create_frame(self, pc, sp, frame, frame_type, pending_frame): + # Make a frame_id that claims that |frame| is sort of like a + # frame pointer for this frame. + frame_id = SpiderMonkeyFrameId(frame, pc) + + # Read the frame layout object to find the next such object. + # This lets us unwind the necessary registers for the next + # frame, and also update our internal state to match. + common = frame.cast(self.typecache.typeCommonFrameLayoutPointer) + next_pc = common["returnAddress_"] + (local_size, header_size, next_type) = self.unpack_descriptor(common) + next_sp = frame + header_size + local_size + + # Compute the type of the next oldest frame's descriptor. + this_class_type = self.typecache.frame_class_types[frame_type] + this_frame = frame.cast(this_class_type) + + # Register this frame so the frame filter can find it. This + # is registered using SP because we don't have any other good + # approach -- you can't get the frame id from a gdb.Frame. + # https://sourceware.org/bugzilla/show_bug.cgi?id=19800 + frame_name = self.typecache.frame_enum_names[frame_type] + self.add_frame(sp, name=frame_name, this_frame=this_frame) + + # Update internal state for the next unwind. + self.next_sp = next_sp + self.next_type = next_type + + unwind_info = pending_frame.create_unwind_info(frame_id) + unwind_info.add_saved_register(self.PC_REGISTER, next_pc) + unwind_info.add_saved_register(self.SP_REGISTER, next_sp) + # FIXME it would be great to unwind any other registers here. + return unwind_info + + # Unwind an "ordinary" JIT frame. This is used for JIT frames + # other than enter and exit frames. Returns the newly-created + # unwind info for gdb. + def unwind_ordinary(self, pc, pending_frame): + return self.create_frame( + pc, self.next_sp, self.next_sp, self.next_type, pending_frame + ) + + # Unwind an exit frame. Returns None if this cannot be done; + # otherwise returns the newly-created unwind info for gdb. + def unwind_exit_frame(self, pc, pending_frame): + if self.activation == 0: + # Reached the end of the list. + return None + elif self.activation is None: + cx = self.get_tls_context() + self.activation = cx["jitActivation"]["value"] + else: + self.activation = self.activation["prevJitActivation_"] + + packedExitFP = self.activation["packedExitFP_"] + if packedExitFP == 0: + return None + + exit_sp = pending_frame.read_register(self.SP_REGISTER) + frame_type = self.typecache.frame_type.Exit + return self.create_frame(pc, exit_sp, packedExitFP, frame_type, pending_frame) + + # A wrapper for unwind_entry_frame_registers that handles + # architecture-independent boilerplate. + def unwind_entry_frame(self, pc, pending_frame): + sp = self.next_sp + # Notify the frame filter. + self.add_frame(sp, name="FrameType::CppToJSJit") + # Make an unwind_info for the per-architecture code to fill in. + frame_id = SpiderMonkeyFrameId(sp, pc) + unwind_info = pending_frame.create_unwind_info(frame_id) + self.unwind_entry_frame_registers(sp, unwind_info) + self.next_sp = None + self.next_type = None + return unwind_info + + # The main entry point that is called to try to unwind a JIT frame + # of any type. Returns None if this cannot be done; otherwise + # returns the newly-created unwind info for gdb. + def unwind(self, pending_frame): + pc = pending_frame.read_register(self.PC_REGISTER) + + # If the jit does not claim this address, bail. GDB defers to our + # unwinder by default, but we don't really want that kind of power. + if not self.is_jit_address(long(pc)): + return None + + if self.next_sp is not None: + if self.next_type == self.typecache.frame_type.CppToJSJit: + return self.unwind_entry_frame(pc, pending_frame) + return self.unwind_ordinary(pc, pending_frame) + # Maybe we've found an exit frame. FIXME I currently don't + # know how to identify these precisely, so we'll just hope for + # the time being. + return self.unwind_exit_frame(pc, pending_frame) + + +class x64UnwinderState(UnwinderState): + "The UnwinderState subclass for x86-64." + + SP_REGISTER = "rsp" + PC_REGISTER = "rip" + + # A register unique to this architecture, that is also likely to + # have been saved in any frame. The best thing to use here is + # some arch-specific name for PC or SP. + SENTINEL_REGISTER = "rip" + + # Must be in sync with Trampoline-x64.cpp:generateEnterJIT. Note + # that rip isn't pushed there explicitly, but rather by the + # previous function's call. + PUSHED_REGS = ["r15", "r14", "r13", "r12", "rbx", "rbp", "rip"] + + # Fill in the unwound registers for an entry frame. + def unwind_entry_frame_registers(self, sp, unwind_info): + sp = sp.cast(self.typecache.void_starstar) + # Skip the "result" push. + sp = sp + 1 + for reg in self.PUSHED_REGS: + data = sp.dereference() + sp = sp + 1 + unwind_info.add_saved_register(reg, data) + if reg == "rbp": + unwind_info.add_saved_register(self.SP_REGISTER, sp) + + +class SpiderMonkeyUnwinder(Unwinder): + """The unwinder object. This provides the "user interface" to the JIT + unwinder, and also handles constructing or destroying UnwinderState + objects as needed.""" + + # A list of all the possible unwinders. See |self.make_unwinder|. + UNWINDERS = [x64UnwinderState] + + def __init__(self, typecache): + super(SpiderMonkeyUnwinder, self).__init__("SpiderMonkey") + self.typecache = typecache + self.unwinder_state = None + + # Disabled by default until we figure out issues in gdb. + self.enabled = False + gdb.write( + "SpiderMonkey unwinder is disabled by default, to enable it type:\n" + + "\tenable unwinder .* SpiderMonkey\n" + ) + # Some versions of gdb did not flush the internal frame cache + # when enabling or disabling an unwinder. This was fixed in + # the same release of gdb that added the breakpoint_created + # event. + if not hasattr(gdb.events, "breakpoint_created"): + gdb.write("\tflushregs\n") + + # We need to invalidate the unwinder state whenever the + # inferior starts executing. This avoids having a stale + # cache. + gdb.events.cont.connect(self.invalidate_unwinder_state) + assert self.test_sentinels() + + def test_sentinels(self): + # Self-check. + regs = {} + for unwinder in self.UNWINDERS: + if unwinder.SENTINEL_REGISTER in regs: + return False + regs[unwinder.SENTINEL_REGISTER] = 1 + return True + + def make_unwinder(self, pending_frame): + # gdb doesn't provide a good way to find the architecture. + # See https://sourceware.org/bugzilla/show_bug.cgi?id=19399 + # So, we look at each known architecture and see if the + # corresponding "unique register" is known. + for unwinder in self.UNWINDERS: + try: + pending_frame.read_register(unwinder.SENTINEL_REGISTER) + except Exception: + # Failed to read the register, so let's keep going. + # This is more fragile than it might seem, because it + # fails if the sentinel register wasn't saved in the + # previous frame. + continue + return unwinder(self.typecache) + return None + + def __call__(self, pending_frame): + if self.unwinder_state is None or not self.unwinder_state.check(): + self.unwinder_state = self.make_unwinder(pending_frame) + if not self.unwinder_state: + return None + return self.unwinder_state.unwind(pending_frame) + + def invalidate_unwinder_state(self, *args, **kwargs): + self.unwinder_state = None + + +def register_unwinder(objfile): + """Register the unwinder and frame filter with |objfile|. If |objfile| + is None, register them globally.""" + + type_cache = UnwinderTypeCache() + unwinder = None + # This currently only works on Linux, due to parse_proc_maps. + if _have_unwinder and platform.system() == "Linux": + unwinder = SpiderMonkeyUnwinder(type_cache) + gdb.unwinder.register_unwinder(objfile, unwinder, replace=True) + # We unconditionally register the frame filter, because at some + # point we'll add interpreter frame filtering. + filt = SpiderMonkeyFrameFilter(type_cache, unwinder) + if objfile is None: + objfile = gdb + objfile.frame_filters[filt.name] = filt diff --git a/js/src/gdb/progressbar.py b/js/src/gdb/progressbar.py new file mode 100644 index 0000000000..04be825c95 --- /dev/null +++ b/js/src/gdb/progressbar.py @@ -0,0 +1,54 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +# Text progress bar library, like curl or scp. + +import datetime +import sys +import time + + +class ProgressBar(object): + def __init__(self, label, limit, label_width=12): + self.label = label + self.limit = limit + self.label_width = label_width + self.cur = 0 + self.t0 = datetime.datetime.now() + self.fullwidth = None + + self.barlen = 64 - self.label_width + self.fmt = ( + "\r%-" + str(label_width) + "s %3d%% %-" + str(self.barlen) + "s| %6.1fs" + ) + + def update(self, value): + self.cur = value + pct = int(100.0 * self.cur / self.limit) + barlen = int(1.0 * self.barlen * self.cur / self.limit) - 1 + bar = "=" * barlen + ">" + dt = datetime.datetime.now() - self.t0 + dt = dt.seconds + dt.microseconds * 1e-6 + line = self.fmt % (self.label[: self.label_width], pct, bar, dt) + self.fullwidth = len(line) + sys.stdout.write(line) + sys.stdout.flush() + + # Clear the current bar and leave the cursor at the start of the line. + def clear(self): + if self.fullwidth: + sys.stdout.write("\r" + " " * self.fullwidth + "\r") + self.fullwidth = None + + def finish(self): + self.update(self.limit) + sys.stdout.write("\n") + + +if __name__ == "__main__": + pb = ProgressBar("test", 12) + for i in range(12): + pb.update(i) + time.sleep(0.5) + pb.finish() diff --git a/js/src/gdb/run-tests.py b/js/src/gdb/run-tests.py new file mode 100644 index 0000000000..e1d18dc710 --- /dev/null +++ b/js/src/gdb/run-tests.py @@ -0,0 +1,469 @@ +#!/usr/bin/env python +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +# run-tests.py -- Python harness for GDB SpiderMonkey support + +import os +import re +import subprocess +import sys + +# From this directory: +import progressbar +from taskpool import TaskPool, get_cpu_count + + +def _relpath(path, start=None): + # Backported from Python 3.1 posixpath.py + """Return a relative version of a path""" + + if not path: + raise ValueError("no path specified") + + if start is None: + start = os.curdir + + start_list = os.path.abspath(start).split(os.sep) + path_list = os.path.abspath(path).split(os.sep) + + # Work out how much of the filepath is shared by start and path. + i = len(os.path.commonprefix([start_list, path_list])) + + rel_list = [os.pardir] * (len(start_list) - i) + path_list[i:] + if not rel_list: + return os.curdir + return os.path.join(*rel_list) + + +os.path.relpath = _relpath + +# Characters that need to be escaped when used in shell words. +shell_need_escapes = re.compile("[^\w\d%+,-./:=@'\"]", re.DOTALL) +# Characters that need to be escaped within double-quoted strings. +shell_dquote_escapes = re.compile('[^\w\d%+,-./:=@"]', re.DOTALL) + + +def make_shell_cmd(l): + def quote(s): + if shell_need_escapes.search(s): + if s.find("'") < 0: + return "'" + s + "'" + return '"' + shell_dquote_escapes.sub("\\g<0>", s) + '"' + return s + + return " ".join([quote(_) for _ in l]) + + +# An instance of this class collects the lists of passing, failing, and +# timing-out tests, runs the progress bar, and prints a summary at the end. +class Summary(object): + class SummaryBar(progressbar.ProgressBar): + def __init__(self, limit): + super(Summary.SummaryBar, self).__init__("", limit, 24) + + def start(self): + self.label = "[starting ]" + self.update(0) + + def counts(self, run, failures, timeouts): + self.label = "[%4d|%4d|%4d|%4d]" % (run - failures, failures, timeouts, run) + self.update(run) + + def __init__(self, num_tests): + self.run = 0 + self.failures = [] # kind of judgemental; "unexpecteds"? + self.timeouts = [] + if not OPTIONS.hide_progress: + self.bar = Summary.SummaryBar(num_tests) + + # Progress bar control. + def start(self): + if not OPTIONS.hide_progress: + self.bar.start() + + def update(self): + if not OPTIONS.hide_progress: + self.bar.counts(self.run, len(self.failures), len(self.timeouts)) + + # Call 'thunk' to show some output, while getting the progress bar out of the way. + + def interleave_output(self, thunk): + if not OPTIONS.hide_progress: + self.bar.clear() + thunk() + self.update() + + def passed(self, test): + self.run += 1 + self.update() + + def failed(self, test): + self.run += 1 + self.failures.append(test) + self.update() + + def timeout(self, test): + self.run += 1 + self.timeouts.append(test) + self.update() + + def finish(self): + if not OPTIONS.hide_progress: + self.bar.finish() + + if self.failures: + + print("tests failed:") + for test in self.failures: + test.show(sys.stdout) + + if OPTIONS.worklist: + try: + with open(OPTIONS.worklist) as out: + for test in self.failures: + out.write(test.name + "\n") + except IOError as err: + sys.stderr.write( + "Error writing worklist file '%s': %s" % (OPTIONS.worklist, err) + ) + sys.exit(1) + + if OPTIONS.write_failures: + try: + with open(OPTIONS.write_failures, "w") as out: + for test in self.failures: + test.show(out) + except IOError as err: + sys.stderr.write( + "Error writing worklist file '%s': %s" + % (OPTIONS.write_failures, err) + ) + sys.exit(1) + + if self.timeouts: + print("tests timed out:") + for test in self.timeouts: + test.show(sys.stdout) + + if self.failures or self.timeouts: + sys.exit(2) + + +class Test(TaskPool.Task): + def __init__(self, path, summary): + super(Test, self).__init__() + self.test_path = path # path to .py test file + self.summary = summary + + # test.name is the name of the test relative to the top of the test + # directory. This is what we use to report failures and timeouts, + # and when writing test lists. + self.name = os.path.relpath(self.test_path, OPTIONS.testdir) + + self.stdout = "" + self.stderr = "" + self.returncode = None + + def cmd(self): + testlibdir = os.path.normpath( + os.path.join(OPTIONS.testdir, "..", "lib-for-tests") + ) + return [ + OPTIONS.gdb_executable, + "-nw", # Don't create a window (unnecessary?) + "-nx", # Don't read .gdbinit. + "--ex", + "add-auto-load-safe-path %s" % (OPTIONS.bindir,), + "--ex", + "set env LD_LIBRARY_PATH %s" % (OPTIONS.bindir,), + "--ex", + "file %s" % (os.path.join(OPTIONS.bindir, "gdb-tests"),), + "--eval-command", + "python testlibdir=%r" % (testlibdir,), + "--eval-command", + "python testscript=%r" % (self.test_path,), + "--eval-command", + "python exec(open(%r).read())" % os.path.join(testlibdir, "catcher.py"), + ] + + def start(self, pipe, deadline): + super(Test, self).start(pipe, deadline) + if OPTIONS.show_cmd: + self.summary.interleave_output(lambda: self.show_cmd(sys.stdout)) + + def onStdout(self, text): + self.stdout += text + + def onStderr(self, text): + self.stderr += text + + def onFinished(self, returncode): + self.returncode = returncode + if OPTIONS.show_output: + self.summary.interleave_output(lambda: self.show_output(sys.stdout)) + if returncode != 0: + self.summary.failed(self) + else: + self.summary.passed(self) + + def onTimeout(self): + self.summary.timeout(self) + + def show_cmd(self, out): + out.write("Command: %s\n" % (make_shell_cmd(self.cmd()),)) + + def show_output(self, out): + if self.stdout: + out.write("Standard output:") + out.write("\n" + self.stdout + "\n") + if self.stderr: + out.write("Standard error:") + out.write("\n" + self.stderr + "\n") + + def show(self, out): + out.write(self.name + "\n") + if OPTIONS.write_failure_output: + self.show_cmd(out) + self.show_output(out) + out.write("GDB exit code: %r\n" % (self.returncode,)) + + +def find_tests(dir, substring=None): + ans = [] + for dirpath, _, filenames in os.walk(dir): + if dirpath == ".": + continue + for filename in filenames: + if not filename.endswith(".py"): + continue + test = os.path.join(dirpath, filename) + if substring is None or substring in os.path.relpath(test, dir): + ans.append(test) + return ans + + +def build_test_exec(builddir): + subprocess.check_call(["make"], cwd=builddir) + + +def run_tests(tests, summary): + jobs = OPTIONS.workercount + # python 3.3 fixed a bug with concurrently writing .pyc files. + # https://bugs.python.org/issue13146 + embedded_version = ( + subprocess.check_output( + [ + OPTIONS.gdb_executable, + "--batch", + "--ex", + "python import sys; print(sys.hexversion)", + ] + ) + .decode("ascii") + .strip() + ) + if hex(int(embedded_version)) < "0x3030000": + jobs = 1 + + pool = TaskPool(tests, job_limit=jobs, timeout=OPTIONS.timeout) + pool.run_all() + + +OPTIONS = None + + +def main(argv): + global OPTIONS + script_path = os.path.abspath(__file__) + script_dir = os.path.dirname(script_path) + + # OBJDIR is a standalone SpiderMonkey build directory. This is where we + # find the SpiderMonkey shared library to link against. + # + # The [TESTS] optional arguments are paths of test files relative + # to the jit-test/tests directory. + from optparse import OptionParser + + op = OptionParser(usage="%prog [options] OBJDIR [TESTS...]") + op.add_option( + "-s", + "--show-cmd", + dest="show_cmd", + action="store_true", + help="show GDB shell command run", + ) + op.add_option( + "-o", + "--show-output", + dest="show_output", + action="store_true", + help="show output from GDB", + ) + op.add_option( + "-x", + "--exclude", + dest="exclude", + action="append", + help="exclude given test dir or path", + ) + op.add_option( + "-t", + "--timeout", + dest="timeout", + type=float, + default=150.0, + help="set test timeout in seconds", + ) + op.add_option( + "-j", + "--worker-count", + dest="workercount", + type=int, + help="Run [WORKERCOUNT] tests at a time", + ) + op.add_option( + "--no-progress", + dest="hide_progress", + action="store_true", + help="hide progress bar", + ) + op.add_option( + "--worklist", + dest="worklist", + metavar="FILE", + help="Read tests to run from [FILE] (or run all if [FILE] not found);\n" + "write failures back to [FILE]", + ) + op.add_option( + "-r", + "--read-tests", + dest="read_tests", + metavar="FILE", + help="Run test files listed in [FILE]", + ) + op.add_option( + "-w", + "--write-failures", + dest="write_failures", + metavar="FILE", + help="Write failing tests to [FILE]", + ) + op.add_option( + "--write-failure-output", + dest="write_failure_output", + action="store_true", + help="With --write-failures=FILE, additionally write the output of failed " + "tests to [FILE]", + ) + op.add_option( + "--gdb", + dest="gdb_executable", + metavar="EXECUTABLE", + default="gdb", + help="Run tests with [EXECUTABLE], rather than plain 'gdb'.", + ) + op.add_option( + "--srcdir", + dest="srcdir", + default=os.path.abspath(os.path.join(script_dir, "..")), + help="Use SpiderMonkey sources in [SRCDIR].", + ) + op.add_option( + "--testdir", + dest="testdir", + default=os.path.join(script_dir, "tests"), + help="Find tests in [TESTDIR].", + ) + op.add_option( + "--builddir", dest="builddir", help="Build test executable from [BUILDDIR]." + ) + op.add_option("--bindir", dest="bindir", help="Run test executable from [BINDIR].") + (OPTIONS, args) = op.parse_args(argv) + if len(args) < 1: + op.error("missing OBJDIR argument") + OPTIONS.objdir = os.path.abspath(args[0]) + + test_args = args[1:] + + if not OPTIONS.workercount: + OPTIONS.workercount = get_cpu_count() + + # Compute defaults for OPTIONS.builddir and OPTIONS.bindir now, since we've + # computed OPTIONS.objdir. + if not OPTIONS.builddir: + OPTIONS.builddir = os.path.join(OPTIONS.objdir, "js", "src", "gdb") + if not OPTIONS.bindir: + OPTIONS.bindir = os.path.join(OPTIONS.objdir, "dist", "bin") + + test_set = set() + + # All the various sources of test names accumulate. + if test_args: + for arg in test_args: + test_set.update(find_tests(OPTIONS.testdir, arg)) + if OPTIONS.worklist: + try: + with open(OPTIONS.worklist) as f: + for line in f: + test_set.update(os.path.join(OPTIONS.testdir, line.strip("\n"))) + except IOError: + # With worklist, a missing file means to start the process with + # the complete list of tests. + sys.stderr.write( + "Couldn't read worklist file '%s'; running all tests\n" + % (OPTIONS.worklist,) + ) + test_set = set(find_tests(OPTIONS.testdir)) + if OPTIONS.read_tests: + try: + with open(OPTIONS.read_tests) as f: + for line in f: + test_set.update(os.path.join(OPTIONS.testdir, line.strip("\n"))) + except IOError as err: + sys.stderr.write( + "Error trying to read test file '%s': %s\n" % (OPTIONS.read_tests, err) + ) + sys.exit(1) + + # If none of the above options were passed, and no tests were listed + # explicitly, use the complete set. + if not test_args and not OPTIONS.worklist and not OPTIONS.read_tests: + test_set = set(find_tests(OPTIONS.testdir)) + + if OPTIONS.exclude: + exclude_set = set() + for exclude in OPTIONS.exclude: + exclude_set.update(find_tests(OPTIONS.testdir, exclude)) + test_set -= exclude_set + + if not test_set: + sys.stderr.write("No tests found matching command line arguments.\n") + sys.exit(1) + + summary = Summary(len(test_set)) + test_list = [Test(_, summary) for _ in sorted(test_set)] + + # Build the test executable from all the .cpp files found in the test + # directory tree. + try: + build_test_exec(OPTIONS.builddir) + except subprocess.CalledProcessError as err: + sys.stderr.write("Error building test executable: %s\n" % (err,)) + sys.exit(1) + + # Run the tests. + try: + summary.start() + run_tests(test_list, summary) + summary.finish() + except OSError as err: + sys.stderr.write("Error running tests: %s\n" % (err,)) + sys.exit(1) + + sys.exit(0) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/js/src/gdb/taskpool.py b/js/src/gdb/taskpool.py new file mode 100644 index 0000000000..38254af6b3 --- /dev/null +++ b/js/src/gdb/taskpool.py @@ -0,0 +1,240 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. + +# flake8: noqa: F821 + +import fcntl +import os +import select +import time +from subprocess import PIPE, Popen + + +class TaskPool(object): + # Run a series of subprocesses. Try to keep up to a certain number going in + # parallel at any given time. Enforce time limits. + # + # This is implemented using non-blocking I/O, and so is Unix-specific. + # + # We assume that, if a task closes its standard error, then it's safe to + # wait for it to terminate. So an ill-behaved task that closes its standard + # output and then hangs will hang us, as well. However, as it takes special + # effort to close one's standard output, this seems unlikely to be a + # problem in practice. + + # A task we should run in a subprocess. Users should subclass this and + # fill in the methods as given. + class Task(object): + def __init__(self): + self.pipe = None + self.start_time = None + + # Record that this task is running, with |pipe| as its Popen object, + # and should time out at |deadline|. + def start(self, pipe, deadline): + self.pipe = pipe + self.deadline = deadline + + # Return a shell command (a string or sequence of arguments) to be + # passed to Popen to run the task. The command will be given + # /dev/null as its standard input, and pipes as its standard output + # and error. + def cmd(self): + raise NotImplementedError + + # TaskPool calls this method to report that the process wrote + # |string| to its standard output. + def onStdout(self, string): + raise NotImplementedError + + # TaskPool calls this method to report that the process wrote + # |string| to its standard error. + def onStderr(self, string): + raise NotImplementedError + + # TaskPool calls this method to report that the process terminated, + # yielding |returncode|. + def onFinished(self, returncode): + raise NotImplementedError + + # TaskPool calls this method to report that the process timed out and + # was killed. + def onTimeout(self): + raise NotImplementedError + + # If a task output handler (onStdout, onStderr) throws this, we terminate + # the task. + class TerminateTask(Exception): + pass + + def __init__(self, tasks, cwd=".", job_limit=4, timeout=150): + self.pending = iter(tasks) + self.cwd = cwd + self.job_limit = job_limit + self.timeout = timeout + self.next_pending = next(self.pending, None) + + def run_all(self): + # The currently running tasks: a set of Task instances. + running = set() + with open(os.devnull, "r") as devnull: + while True: + while len(running) < self.job_limit and self.next_pending: + task = self.next_pending + p = Popen( + task.cmd(), + bufsize=16384, + stdin=devnull, + stdout=PIPE, + stderr=PIPE, + cwd=self.cwd, + ) + + # Put the stdout and stderr pipes in non-blocking mode. See + # the post-'select' code below for details. + flags = fcntl.fcntl(p.stdout, fcntl.F_GETFL) + fcntl.fcntl(p.stdout, fcntl.F_SETFL, flags | os.O_NONBLOCK) + flags = fcntl.fcntl(p.stderr, fcntl.F_GETFL) + fcntl.fcntl(p.stderr, fcntl.F_SETFL, flags | os.O_NONBLOCK) + + task.start(p, time.time() + self.timeout) + running.add(task) + self.next_pending = next(self.pending, None) + + # If we have no tasks running, and the above wasn't able to + # start any new ones, then we must be done! + if not running: + break + + # How many seconds do we have until the earliest deadline? + now = time.time() + secs_to_next_deadline = max(min([t.deadline for t in running]) - now, 0) + + # Wait for output or a timeout. + stdouts_and_stderrs = [t.pipe.stdout for t in running] + [ + t.pipe.stderr for t in running + ] + (readable, w, x) = select.select( + stdouts_and_stderrs, [], [], secs_to_next_deadline + ) + finished = set() + terminate = set() + for t in running: + # Since we've placed the pipes in non-blocking mode, these + # 'read's will simply return as many bytes as are available, + # rather than blocking until they have accumulated the full + # amount requested (or reached EOF). The 'read's should + # never throw, since 'select' has told us there was + # something available. + if t.pipe.stdout in readable: + output = t.pipe.stdout.read(16384) + if len(output): + try: + t.onStdout(output.decode("utf-8")) + except TerminateTask: + terminate.add(t) + if t.pipe.stderr in readable: + output = t.pipe.stderr.read(16384) + if len(output): + try: + t.onStderr(output.decode("utf-8")) + except TerminateTask: + terminate.add(t) + else: + # We assume that, once a task has closed its stderr, + # it will soon terminate. If a task closes its + # stderr and then hangs, we'll hang too, here. + t.pipe.wait() + t.onFinished(t.pipe.returncode) + finished.add(t) + # Remove the finished tasks from the running set. (Do this here + # to avoid mutating the set while iterating over it.) + running -= finished + + # Terminate any tasks whose handlers have asked us to do so. + for t in terminate: + t.pipe.terminate() + t.pipe.wait() + running.remove(t) + + # Terminate any tasks which have missed their deadline. + finished = set() + for t in running: + if now >= t.deadline: + t.pipe.terminate() + t.pipe.wait() + t.onTimeout() + finished.add(t) + # Remove the finished tasks from the running set. (Do this here + # to avoid mutating the set while iterating over it.) + running -= finished + return None + + +def get_cpu_count(): + """ + Guess at a reasonable parallelism count to set as the default for the + current machine and run. + """ + # Python 2.6+ + try: + import multiprocessing + + return multiprocessing.cpu_count() + except (ImportError, NotImplementedError): + pass + + # POSIX + try: + res = int(os.sysconf("SC_NPROCESSORS_ONLN")) + if res > 0: + return res + except (AttributeError, ValueError): + pass + + # Windows + try: + res = int(os.environ["NUMBER_OF_PROCESSORS"]) + if res > 0: + return res + except (KeyError, ValueError): + pass + + return 1 + + +if __name__ == "__main__": + # Test TaskPool by using it to implement the unique 'sleep sort' algorithm. + def sleep_sort(ns, timeout): + sorted = [] + + class SortableTask(TaskPool.Task): + def __init__(self, n): + super(SortableTask, self).__init__() + self.n = n + + def start(self, pipe, deadline): + super(SortableTask, self).start(pipe, deadline) + + def cmd(self): + return ["sh", "-c", "echo out; sleep %d; echo err>&2" % (self.n,)] + + def onStdout(self, text): + print("%d stdout: %r" % (self.n, text)) + + def onStderr(self, text): + print("%d stderr: %r" % (self.n, text)) + + def onFinished(self, returncode): + print("%d (rc=%d)" % (self.n, returncode)) + sorted.append(self.n) + + def onTimeout(self): + print("%d timed out" % (self.n,)) + + p = TaskPool([SortableTask(_) for _ in ns], job_limit=len(ns), timeout=timeout) + p.run_all() + return sorted + + print(repr(sleep_sort([1, 1, 2, 3, 5, 8, 13, 21, 34], 15))) diff --git a/js/src/gdb/tests/enum-printers.cpp b/js/src/gdb/tests/enum-printers.cpp new file mode 100644 index 0000000000..dfb1cd7b46 --- /dev/null +++ b/js/src/gdb/tests/enum-printers.cpp @@ -0,0 +1,25 @@ +#include "gdb-tests.h" + +#include <stdint.h> + +enum unscoped_no_storage { EnumValue1 }; + +enum unscoped_with_storage : uint8_t { EnumValue2 }; + +enum class scoped_no_storage { EnumValue3 }; + +enum class scoped_with_storage : uint8_t { EnumValue4 }; + +FRAGMENT(enum_printers, one) { + unscoped_no_storage i1 = EnumValue1; + unscoped_with_storage i2 = EnumValue2; + scoped_no_storage i3 = scoped_no_storage::EnumValue3; + scoped_with_storage i4 = scoped_with_storage::EnumValue4; + + breakpoint(); + + use(i1); + use(i2); + use(i3); + use(i4); +} diff --git a/js/src/gdb/tests/enum-printers.py b/js/src/gdb/tests/enum-printers.py new file mode 100644 index 0000000000..ff2c088dc8 --- /dev/null +++ b/js/src/gdb/tests/enum-printers.py @@ -0,0 +1,47 @@ +# Test that we can find pretty-printers for enums. +# flake8: noqa: F821 + +import mozilla.prettyprinters + + +@mozilla.prettyprinters.pretty_printer("unscoped_no_storage") +class my_typedef(object): + def __init__(self, value, cache): + pass + + def to_string(self): + return "unscoped_no_storage::success" + + +@mozilla.prettyprinters.pretty_printer("unscoped_with_storage") +class my_typedef(object): + def __init__(self, value, cache): + pass + + def to_string(self): + return "unscoped_with_storage::success" + + +@mozilla.prettyprinters.pretty_printer("scoped_no_storage") +class my_typedef(object): + def __init__(self, value, cache): + pass + + def to_string(self): + return "scoped_no_storage::success" + + +@mozilla.prettyprinters.pretty_printer("scoped_with_storage") +class my_typedef(object): + def __init__(self, value, cache): + pass + + def to_string(self): + return "scoped_with_storage::success" + + +run_fragment("enum_printers.one") +assert_pretty("i1", "unscoped_no_storage::success") +assert_pretty("i2", "unscoped_with_storage::success") +assert_pretty("i3", "scoped_no_storage::success") +assert_pretty("i4", "scoped_with_storage::success") diff --git a/js/src/gdb/tests/test-ExecutableAllocator.cpp b/js/src/gdb/tests/test-ExecutableAllocator.cpp new file mode 100644 index 0000000000..2a2ae1cf63 --- /dev/null +++ b/js/src/gdb/tests/test-ExecutableAllocator.cpp @@ -0,0 +1,49 @@ +#include "gdb-tests.h" + +#include "jit/ExecutableAllocator.h" +#include "vm/JSContext.h" + +FRAGMENT(ExecutableAllocator, empty) { + using namespace js::jit; + ExecutableAllocator execAlloc; + + breakpoint(); + + use(execAlloc); +} + +FRAGMENT(ExecutableAllocator, onepool) { + using namespace js::jit; + ExecutablePool* pool = nullptr; + ExecutableAllocator execAlloc; + execAlloc.alloc(cx, 16 * 1024, &pool, CodeKind::Baseline); + + breakpoint(); + + use(pool); + use(execAlloc); +} + +FRAGMENT(ExecutableAllocator, twopools) { + using namespace js::jit; + const size_t INIT_ALLOC_SIZE = 16 * 1024; + const size_t ALLOC_SIZE = 32 * 1024; + ExecutablePool* init = nullptr; + ExecutablePool* pool = nullptr; + ExecutableAllocator execAlloc; + size_t allocated = 0; + + execAlloc.alloc(cx, INIT_ALLOC_SIZE, &init, CodeKind::Baseline); + + do { // Keep allocating until we get a second pool. + execAlloc.alloc(cx, ALLOC_SIZE, &pool, CodeKind::Ion); + allocated += ALLOC_SIZE; + } while (pool == init); + + breakpoint(); + + use(execAlloc); + init->release(INIT_ALLOC_SIZE, CodeKind::Baseline); + init->release(allocated - ALLOC_SIZE, CodeKind::Ion); + pool->release(ALLOC_SIZE, CodeKind::Ion); +} diff --git a/js/src/gdb/tests/test-ExecutableAllocator.py b/js/src/gdb/tests/test-ExecutableAllocator.py new file mode 100644 index 0000000000..bec2dda623 --- /dev/null +++ b/js/src/gdb/tests/test-ExecutableAllocator.py @@ -0,0 +1,22 @@ +# Tests for ExecutableAllocator pretty-printing +# Ignore flake8 errors "undefined name 'assert_regexp_pretty'" +# As it caused by the way we instanciate this file +# flake8: noqa: F821 + +assert_subprinter_registered("SpiderMonkey", "JS::GCCellPtr") + +run_fragment("ExecutableAllocator.empty") + +assert_pretty("execAlloc", "ExecutableAllocator([])") + +run_fragment("ExecutableAllocator.onepool") + +reExecPool = "ExecutablePool [a-f0-9]{8,}-[a-f0-9]{8,}" +assert_regexp_pretty("pool", reExecPool) +assert_regexp_pretty("execAlloc", "ExecutableAllocator\(\[" + reExecPool + "\]\)") + +run_fragment("ExecutableAllocator.twopools") + +assert_regexp_pretty( + "execAlloc", "ExecutableAllocator\(\[" + reExecPool + ", " + reExecPool + "\]\)" +) diff --git a/js/src/gdb/tests/test-GCCellPtr.cpp b/js/src/gdb/tests/test-GCCellPtr.cpp new file mode 100644 index 0000000000..8dcf135be8 --- /dev/null +++ b/js/src/gdb/tests/test-GCCellPtr.cpp @@ -0,0 +1,62 @@ +#include "gdb-tests.h" + +#include "js/CompileOptions.h" +#include "js/CompilationAndEvaluation.h" +#include "js/GlobalObject.h" +#include "js/HeapAPI.h" +#include "js/RegExpFlags.h" +#include "js/SourceText.h" +#include "js/Symbol.h" +#include "js/TypeDecls.h" +#include "vm/BigIntType.h" +#include "vm/JSObject.h" +#include "vm/RegExpObject.h" +#include "vm/Shape.h" + +#include "vm/JSObject-inl.h" + +FRAGMENT(GCCellPtr, simple) { + JS::Rooted<JSObject*> glob(cx, JS::CurrentGlobalOrNull(cx)); + JS::Rooted<JSString*> empty(cx, JS_NewStringCopyN(cx, nullptr, 0)); + JS::Rooted<JS::Symbol*> unique(cx, JS::NewSymbol(cx, nullptr)); + JS::Rooted<JS::BigInt*> zeroBigInt(cx, JS::BigInt::zero(cx)); + JS::Rooted<js::RegExpObject*> regExp( + cx, js::RegExpObject::create(cx, u"", 0, JS::RegExpFlags{}, + js::GenericObject)); + JS::Rooted<js::RegExpShared*> rootedRegExpShared( + cx, js::RegExpObject::getShared(cx, regExp)); + + JS::CompileOptions options(cx); + options.setFileAndLine(__FILE__, __LINE__); + JS::SourceText<char16_t> srcBuf; + (void)srcBuf.init(cx, nullptr, 0, JS::SourceOwnership::Borrowed); + JS::RootedScript emptyScript(cx, JS::Compile(cx, options, srcBuf)); + + // Inline TraceKinds. + JS::GCCellPtr nulll(nullptr); + JS::GCCellPtr object(glob.get()); + JS::GCCellPtr string(empty.get()); + JS::GCCellPtr symbol(unique.get()); + JS::GCCellPtr bigint(zeroBigInt.get()); + JS::GCCellPtr shape(glob->shape()); + + // Out-of-line TraceKinds. + JS::GCCellPtr baseShape(glob->shape()->base()); + // JitCode can't easily be tested here, so skip it. + JS::GCCellPtr script(emptyScript.get()); + JS::GCCellPtr scope(emptyScript->bodyScope()); + JS::GCCellPtr regExpShared(rootedRegExpShared.get()); + + breakpoint(); + + use(nulll); + use(object); + use(string); + use(symbol); + use(bigint); + use(shape); + use(baseShape); + use(script); + use(scope); + use(regExpShared); +} diff --git a/js/src/gdb/tests/test-GCCellPtr.py b/js/src/gdb/tests/test-GCCellPtr.py new file mode 100644 index 0000000000..355ca69806 --- /dev/null +++ b/js/src/gdb/tests/test-GCCellPtr.py @@ -0,0 +1,17 @@ +# Tests for GCCellPtr pretty-printing +# flake8: noqa: F821 + +assert_subprinter_registered("SpiderMonkey", "JS::GCCellPtr") + +run_fragment("GCCellPtr.simple") + +assert_pretty("nulll", "JS::GCCellPtr(nullptr)") +assert_pretty("object", "JS::GCCellPtr((JSObject*) )") +assert_pretty("string", "JS::GCCellPtr((JSString*) )") +assert_pretty("symbol", "JS::GCCellPtr((JS::Symbol*) )") +assert_pretty("bigint", "JS::GCCellPtr((JS::BigInt*) )") +assert_pretty("shape", "JS::GCCellPtr((js::Shape*) )") +assert_pretty("baseShape", "JS::GCCellPtr((js::BaseShape*) )") +assert_pretty("script", "JS::GCCellPtr((js::BaseScript*) )") +assert_pretty("scope", "JS::GCCellPtr((js::Scope*) )") +assert_pretty("regExpShared", "JS::GCCellPtr((js::RegExpShared*) )") diff --git a/js/src/gdb/tests/test-Interpreter.cpp b/js/src/gdb/tests/test-Interpreter.cpp new file mode 100644 index 0000000000..cc844d59a0 --- /dev/null +++ b/js/src/gdb/tests/test-Interpreter.cpp @@ -0,0 +1,88 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=8 sts=2 et sw=2 tw=80: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gdb-tests.h" + +#include "vm/Stack.h" + +namespace js { + +void GDBTestInitInterpreterRegs(InterpreterRegs& regs, + js::InterpreterFrame* fp_, JS::Value* sp, + uint8_t* pc) { + regs.fp_ = fp_; + regs.sp = sp; + regs.pc = pc; +} + +void GDBTestInitAbstractFramePtr(AbstractFramePtr& frame, + InterpreterFrame* ptr) { + MOZ_ASSERT((uintptr_t(ptr) & AbstractFramePtr::TagMask) == 0); + frame.ptr_ = uintptr_t(ptr) | AbstractFramePtr::Tag_InterpreterFrame; +} + +void GDBTestInitAbstractFramePtr(AbstractFramePtr& frame, + jit::BaselineFrame* ptr) { + MOZ_ASSERT((uintptr_t(ptr) & AbstractFramePtr::TagMask) == 0); + frame.ptr_ = uintptr_t(ptr) | AbstractFramePtr::Tag_BaselineFrame; +} + +void GDBTestInitAbstractFramePtr(AbstractFramePtr& frame, + jit::RematerializedFrame* ptr) { + MOZ_ASSERT((uintptr_t(ptr) & AbstractFramePtr::TagMask) == 0); + frame.ptr_ = uintptr_t(ptr) | AbstractFramePtr::Tag_RematerializedFrame; +} + +void GDBTestInitAbstractFramePtr(AbstractFramePtr& frame, + wasm::DebugFrame* ptr) { + MOZ_ASSERT((uintptr_t(ptr) & AbstractFramePtr::TagMask) == 0); + frame.ptr_ = uintptr_t(ptr) | AbstractFramePtr::Tag_WasmDebugFrame; +} + +} // namespace js + +FRAGMENT(Interpreter, Regs) { + struct FakeFrame { + js::InterpreterFrame frame; + JS::Value slot0; + JS::Value slot1; + JS::Value slot2; + } fakeFrame; + uint8_t fakeOpcode = uint8_t(JSOp::True); + + js::InterpreterRegs regs; + js::GDBTestInitInterpreterRegs(regs, &fakeFrame.frame, &fakeFrame.slot2, + &fakeOpcode); + + breakpoint(); + + use(regs); +} + +FRAGMENT(Interpreter, AbstractFramePtr) { + js::AbstractFramePtr ifptr; + GDBTestInitAbstractFramePtr(ifptr, + (js::InterpreterFrame*)uintptr_t(0x8badf00)); + + js::AbstractFramePtr bfptr; + GDBTestInitAbstractFramePtr(bfptr, + (js::jit::BaselineFrame*)uintptr_t(0xbadcafe0)); + + js::AbstractFramePtr rfptr; + GDBTestInitAbstractFramePtr( + rfptr, (js::jit::RematerializedFrame*)uintptr_t(0xdabbad00)); + + js::AbstractFramePtr sfptr; + GDBTestInitAbstractFramePtr(sfptr, + (js::wasm::DebugFrame*)uintptr_t(0xcb98ad00)); + + breakpoint(); + + use(ifptr); + use(bfptr); + use(rfptr); + use(sfptr); +} diff --git a/js/src/gdb/tests/test-Interpreter.py b/js/src/gdb/tests/test-Interpreter.py new file mode 100644 index 0000000000..7a0279dc4d --- /dev/null +++ b/js/src/gdb/tests/test-Interpreter.py @@ -0,0 +1,23 @@ +# Test printing interpreter internal data structures. +# Ignore flake8 errors "undefined name 'assert_pretty'" +# As it caused by the way we instanciate this file +# flake8: noqa: F821 + +assert_subprinter_registered("SpiderMonkey", "js::InterpreterRegs") + +run_fragment("Interpreter.Regs") + +assert_pretty("regs", "{ fp_ = , sp = fp_.slots() + 2, pc = (JSOp::True) }") + +run_fragment("Interpreter.AbstractFramePtr") + +assert_pretty( + "ifptr", "AbstractFramePtr ((js::InterpreterFrame *) ) = {ptr_ = 146464513}" +) +assert_pretty( + "bfptr", "AbstractFramePtr ((js::jit::BaselineFrame *) ) = {ptr_ = 3135025122}" +) +assert_pretty( + "rfptr", + "AbstractFramePtr ((js::jit::RematerializedFrame *) ) = {ptr_ = 3669732611}", +) diff --git a/js/src/gdb/tests/test-JSObject-null.py b/js/src/gdb/tests/test-JSObject-null.py new file mode 100644 index 0000000000..fcda681aea --- /dev/null +++ b/js/src/gdb/tests/test-JSObject-null.py @@ -0,0 +1,8 @@ +# flake8: noqa: F821 + +gdb.execute("set print address on") + +run_fragment("JSObject.null") + +assert_pretty("null", "0x0") +assert_pretty("nullRaw", "0x0") diff --git a/js/src/gdb/tests/test-JSObject.cpp b/js/src/gdb/tests/test-JSObject.cpp new file mode 100644 index 0000000000..b1780a7332 --- /dev/null +++ b/js/src/gdb/tests/test-JSObject.cpp @@ -0,0 +1,54 @@ +#include "gdb-tests.h" +#include "jsapi.h" +#include "js/GlobalObject.h" +#include "js/Object.h" // JS::GetClass + +FRAGMENT(JSObject, simple) { + JS::Rooted<JSObject*> glob(cx, JS::CurrentGlobalOrNull(cx)); + JS::Rooted<JSObject*> plain(cx, JS_NewPlainObject(cx)); + JS::Rooted<JSObject*> objectProto(cx, JS::GetRealmObjectPrototype(cx)); + JS::Rooted<JSObject*> global(cx, JS::CurrentGlobalOrNull(cx)); + JS::Rooted<JSObject*> func( + cx, (JSObject*)JS_NewFunction(cx, (JSNative)1, 0, 0, "dys")); + JS::Rooted<JSObject*> anon( + cx, (JSObject*)JS_NewFunction(cx, (JSNative)1, 0, 0, nullptr)); + JS::Rooted<JSFunction*> funcPtr( + cx, JS_NewFunction(cx, (JSNative)1, 0, 0, "formFollows")); + + // JS_NewObject will now assert if you feed it a bad class name, so mangle + // the name after construction. + char namebuf[20] = "goodname"; + static JSClass cls{namebuf}; + JS::RootedObject badClassName(cx, JS_NewObject(cx, &cls)); + MOZ_RELEASE_ASSERT(badClassName); + strcpy(namebuf, "\xc7X"); + + JSObject& plainRef = *plain; + JSFunction& funcRef = *funcPtr; + JSObject* plainRaw = plain; + JSObject* funcRaw = func; + + breakpoint(); + + use(glob); + use(plain); + use(objectProto); + use(func); + use(anon); + use(funcPtr); + use(&plainRef); + use(&funcRef); + use(JS::GetClass((JSObject*)&funcRef)); + use(plainRaw); + use(funcRaw); +} + +FRAGMENT(JSObject, null) { + JS::Rooted<JSObject*> null(cx, nullptr); + JSObject* nullRaw = null; + + breakpoint(); + + use(null); + use(nullRaw); +} diff --git a/js/src/gdb/tests/test-JSObject.py b/js/src/gdb/tests/test-JSObject.py new file mode 100644 index 0000000000..6f52317d28 --- /dev/null +++ b/js/src/gdb/tests/test-JSObject.py @@ -0,0 +1,24 @@ +# Printing JSObjects. +# flake8: noqa: F821 + +assert_subprinter_registered("SpiderMonkey", "ptr-to-JSObject") +assert_subprinter_registered("SpiderMonkey", "ref-to-JSObject") + +run_fragment("JSObject.simple") + +# These patterns look a little strange because of prologue.py's 'set print +# address off', which avoids putting varying addresses in the output. After +# the '(JSObject *) ', there is a 'void *' value printing as the empty +# string. + +assert_pretty("glob", "(JSObject *) [object global]") +assert_pretty("plain", "(JSObject *) [object Object]") +assert_pretty("objectProto", "(JSObject *) [object Object] used_as_prototype") +assert_pretty("func", '(JSObject *) [object Function "dys"]') +assert_pretty("anon", "(JSObject *) [object Function <unnamed>]") +assert_pretty("funcPtr", '(JSFunction *) [object Function "formFollows"]') + +assert_pretty("badClassName", "(JSObject *) [object \\307X]") + +assert_pretty("plainRef", "(JSObject &) @ [object Object]") +assert_pretty("funcRef", '(JSFunction &) @ [object Function "formFollows"]') diff --git a/js/src/gdb/tests/test-JSString-null.py b/js/src/gdb/tests/test-JSString-null.py new file mode 100644 index 0000000000..26bbfcf428 --- /dev/null +++ b/js/src/gdb/tests/test-JSString-null.py @@ -0,0 +1,8 @@ +# flake8: noqa: F821 + +gdb.execute("set print address on") + +run_fragment("JSString.null") + +assert_pretty("null", "0x0") +assert_pretty("nullRaw", "0x0") diff --git a/js/src/gdb/tests/test-JSString-subclasses.py b/js/src/gdb/tests/test-JSString-subclasses.py new file mode 100644 index 0000000000..b685f46aff --- /dev/null +++ b/js/src/gdb/tests/test-JSString-subclasses.py @@ -0,0 +1,7 @@ +# flake8: noqa: F821 + +# We can print pointers to subclasses of JSString. + +run_fragment("JSString.subclasses") + +assert_pretty("linear", '"Hi!"') diff --git a/js/src/gdb/tests/test-JSString.cpp b/js/src/gdb/tests/test-JSString.cpp new file mode 100644 index 0000000000..9e930814da --- /dev/null +++ b/js/src/gdb/tests/test-JSString.cpp @@ -0,0 +1,65 @@ +#include "gdb-tests.h" + +#include "vm/JSContext.h" +// When JSGC_ANALYSIS is #defined, Rooted<JSLinearString*> needs the definition +// of JSLinearString in order to figure out its ThingRootKind +#include "vm/StringType.h" + +FRAGMENT(JSString, simple) { + JS::Rooted<JSString*> empty(cx, JS_NewStringCopyN(cx, nullptr, 0)); + JS::Rooted<JSString*> x(cx, JS_NewStringCopyN(cx, "x", 1)); + JS::Rooted<JSString*> z(cx, JS_NewStringCopyZ(cx, "z")); + + // I expect this will be a non-inlined string. + JS::Rooted<JSString*> stars(cx, + JS_NewStringCopyZ(cx, + "*************************" + "*************************" + "*************************" + "*************************")); + + // This may well be an inlined string. + JS::Rooted<JSString*> xz(cx, JS_ConcatStrings(cx, x, z)); + + // This will probably be a rope. + JS::Rooted<JSString*> doubleStars(cx, JS_ConcatStrings(cx, stars, stars)); + + // Ensure we're not confused by typedefs for pointer types. + JSString* xRaw = x; + + breakpoint(); + + use(empty); + use(x); + use(z); + use(stars); + use(xz); + use(doubleStars); + use(xRaw); +} + +FRAGMENT(JSString, null) { + JS::Rooted<JSString*> null(cx, nullptr); + JSString* nullRaw = null; + + breakpoint(); + + use(null); + use(nullRaw); +} + +FRAGMENT(JSString, subclasses) { + JS::Rooted<JSLinearString*> linear( + cx, JS_EnsureLinearString(cx, JS_NewStringCopyZ(cx, "Hi!"))); + + breakpoint(); + + use(linear); +} + +FRAGMENT(JSString, atom) { + JSAtom* molybdenum = js::Atomize(cx, "molybdenum", 10); + breakpoint(); + + use(molybdenum); +} diff --git a/js/src/gdb/tests/test-JSString.py b/js/src/gdb/tests/test-JSString.py new file mode 100644 index 0000000000..99cebc4ab5 --- /dev/null +++ b/js/src/gdb/tests/test-JSString.py @@ -0,0 +1,24 @@ +# Printing JSStrings. +# flake8: noqa: F821 + +assert_subprinter_registered("SpiderMonkey", "ptr-to-JSString") +run_fragment("JSString.simple") + +assert_pretty("empty", '""') +assert_pretty("x", '"x"') +assert_pretty("z", '"z"') +assert_pretty("xz", '"xz"') + +stars = gdb.parse_and_eval("stars") +assert_eq(str(stars), "'*' <repeats 100 times>") + +doubleStars = gdb.parse_and_eval("doubleStars") +assert_eq(str(doubleStars), "'*' <repeats 200 times>") + +assert_pretty("xRaw", '"x"') + +# JSAtom * + +run_fragment("JSString.atom") + +assert_pretty("molybdenum", '"molybdenum"') diff --git a/js/src/gdb/tests/test-JSSymbol.cpp b/js/src/gdb/tests/test-JSSymbol.cpp new file mode 100644 index 0000000000..815f2d9e29 --- /dev/null +++ b/js/src/gdb/tests/test-JSSymbol.cpp @@ -0,0 +1,22 @@ +#include "gdb-tests.h" + +#include "js/String.h" +#include "js/Symbol.h" + +FRAGMENT(JSSymbol, simple) { + using namespace JS; + + RootedString hello(cx, JS_NewStringCopyZ(cx, "Hello!")); + + Rooted<Symbol*> unique(cx, NewSymbol(cx, nullptr)); + Rooted<Symbol*> unique_with_desc(cx, NewSymbol(cx, hello)); + Rooted<Symbol*> registry(cx, GetSymbolFor(cx, hello)); + Rooted<Symbol*> well_known(cx, GetWellKnownSymbol(cx, SymbolCode::iterator)); + + breakpoint(); + + use(unique); + use(unique_with_desc); + use(registry); + use(well_known); +} diff --git a/js/src/gdb/tests/test-JSSymbol.py b/js/src/gdb/tests/test-JSSymbol.py new file mode 100644 index 0000000000..a0dba5c036 --- /dev/null +++ b/js/src/gdb/tests/test-JSSymbol.py @@ -0,0 +1,11 @@ +# Printing JS::Symbols. +# flake8: noqa: F821 + +assert_subprinter_registered("SpiderMonkey", "ptr-to-JS::Symbol") + +run_fragment("JSSymbol.simple") + +assert_pretty("unique", "Symbol()") +assert_pretty("unique_with_desc", 'Symbol("Hello!")') +assert_pretty("registry", 'Symbol.for("Hello!")') +assert_pretty("well_known", "Symbol.iterator") diff --git a/js/src/gdb/tests/test-Root-null.py b/js/src/gdb/tests/test-Root-null.py new file mode 100644 index 0000000000..839fa069c5 --- /dev/null +++ b/js/src/gdb/tests/test-Root-null.py @@ -0,0 +1,21 @@ +# Test printing roots that refer to NULL pointers. + +# Since mozilla.prettyprinters.Pointer declines to create pretty-printers +# for null pointers, GDB built-in printing code ends up handling them. But +# as of 2012-11, GDB suppresses printing pointers in replacement values: +# see: http://sourceware.org/ml/gdb/2012-11/msg00055.html +# +# Thus, if the pretty-printer for JS::Rooted simply returns the referent as +# a replacement value (which seems reasonable enough, if you want the +# pretty-printer to be completely transparent), and the referent is a null +# pointer, it prints as nothing at all. +# +# This test ensures that the JS::Rooted pretty-printer doesn't make that +# mistake. +# flake8: noqa: F821 + +gdb.execute("set print address on") + +run_fragment("Root.null") + +assert_pretty("null", "0x0") diff --git a/js/src/gdb/tests/test-Root.cpp b/js/src/gdb/tests/test-Root.cpp new file mode 100644 index 0000000000..f93beba88a --- /dev/null +++ b/js/src/gdb/tests/test-Root.cpp @@ -0,0 +1,67 @@ +#include "gdb-tests.h" + +#include "jsapi.h" + +#include "gc/Barrier.h" +#include "js/Array.h" // JS::NewArrayObject +#include "js/GlobalObject.h" +#include "js/ValueArray.h" +#include "vm/JSFunction.h" + +using namespace js; + +FRAGMENT(Root, null) { + JS::Rooted<JSObject*> null(cx, nullptr); + + breakpoint(); + + use(null); +} + +void callee(JS::Handle<JSObject*> obj, + JS::MutableHandle<JSObject*> mutableObj) { + // Prevent the linker from unifying this function with others that are + // equivalent in machine code but not type. + fprintf(stderr, "Called " __FILE__ ":callee\n"); + breakpoint(); +} + +FRAGMENT(Root, handle) { + JS::Rooted<JSObject*> global(cx, JS::CurrentGlobalOrNull(cx)); + callee(global, &global); + use(global); +} + +FRAGMENT(Root, HeapSlot) { + JS::Rooted<JS::Value> plinth( + cx, JS::StringValue(JS_NewStringCopyZ(cx, "plinth"))); + JS::Rooted<JSObject*> array( + cx, JS::NewArrayObject(cx, JS::HandleValueArray(plinth))); + + breakpoint(); + + use(plinth); + use(array); +} + +FRAGMENT(Root, barriers) { + JSObject* obj = JS_NewPlainObject(cx); + PreBarriered<JSObject*> prebarriered(obj); + GCPtr<JSObject*> heapptr(obj); + HeapPtr<JSObject*> relocatable(obj); + + JS::Value val = JS::ObjectValue(*obj); + PreBarriered<JS::Value> prebarrieredValue(JS::ObjectValue(*obj)); + GCPtr<JS::Value> heapValue(JS::ObjectValue(*obj)); + HeapPtr<JS::Value> relocatableValue(JS::ObjectValue(*obj)); + + breakpoint(); + + use(prebarriered); + use(heapptr); + use(relocatable); + use(val); + use(prebarrieredValue); + use(heapValue); + use(relocatableValue); +} diff --git a/js/src/gdb/tests/test-Root.py b/js/src/gdb/tests/test-Root.py new file mode 100644 index 0000000000..e593a972ab --- /dev/null +++ b/js/src/gdb/tests/test-Root.py @@ -0,0 +1,30 @@ +# Test printing Handles. +# Ignore flake8 errors "undefined name 'assert_pretty'" +# As it caused by the way we instanciate this file +# flake8: noqa: F821 + +assert_subprinter_registered("SpiderMonkey", "instantiations-of-JS::Rooted") +assert_subprinter_registered("SpiderMonkey", "instantiations-of-JS::Handle") +assert_subprinter_registered("SpiderMonkey", "instantiations-of-JS::MutableHandle") +assert_subprinter_registered("SpiderMonkey", "instantiations-of-js::BarrieredBase") + +run_fragment("Root.handle") + +assert_pretty("obj", "(JSObject * const) [object global]") +assert_pretty("mutableObj", "(JSObject *) [object global]") + +run_fragment("Root.HeapSlot") + +# This depends on implementation details of arrays, but since HeapSlot is +# not a public type, I'm not sure how to avoid doing *something* ugly. +assert_pretty("((js::NativeObject *) array.ptr)->elements_[0]", '$JS::Value("plinth")') + +run_fragment("Root.barriers") + +assert_pretty("prebarriered", "(JSObject *) [object Object]") +assert_pretty("heapptr", "(JSObject *) [object Object]") +assert_pretty("relocatable", "(JSObject *) [object Object]") +assert_pretty("val", "$JS::Value((JSObject *) [object Object])") +assert_pretty("heapValue", "$JS::Value((JSObject *) [object Object])") +assert_pretty("prebarrieredValue", "$JS::Value((JSObject *) [object Object])") +assert_pretty("relocatableValue", "$JS::Value((JSObject *) [object Object])") diff --git a/js/src/gdb/tests/test-asmjs.cpp b/js/src/gdb/tests/test-asmjs.cpp new file mode 100644 index 0000000000..8a9fab3b73 --- /dev/null +++ b/js/src/gdb/tests/test-asmjs.cpp @@ -0,0 +1,44 @@ +#include "gdb-tests.h" +#include "js/CompilationAndEvaluation.h" +#include "js/CompileOptions.h" +#include "js/RootingAPI.h" +#include "js/SourceText.h" +#include "js/Value.h" +#include "mozilla/Utf8.h" +#include "util/Text.h" + +#include <string.h> + +FRAGMENT(asmjs, segfault) { + constexpr unsigned line0 = __LINE__; + static const char chars[] = + "function f(glob, ffi, heap) {\n" + " \"use asm\";\n" + " var f32 = new glob.Float32Array(heap);\n" + " function g(n) {\n" + " n = n | 0;\n" + " return +f32[n>>2];\n" + " }\n" + " return g;\n" + "}\n" + "\n" + "var func = f(this, null, new ArrayBuffer(0x10000));\n" + "func(0x10000 << 2);\n" + "'ok'\n"; + + JS::CompileOptions opts(cx); + opts.setFileAndLine(__FILE__, line0 + 1); + opts.asmJSOption = JS::AsmJSOption::Enabled; + + JS::SourceText<mozilla::Utf8Unit> srcBuf; + JS::Rooted<JS::Value> rval(cx); + + bool ok = + srcBuf.init(cx, chars, js_strlen(chars), JS::SourceOwnership::Borrowed) && + JS::Evaluate(cx, opts, srcBuf, &rval); + + breakpoint(); + + use(ok); + use(rval); +} diff --git a/js/src/gdb/tests/test-asmjs.py b/js/src/gdb/tests/test-asmjs.py new file mode 100644 index 0000000000..9e3ed7fc89 --- /dev/null +++ b/js/src/gdb/tests/test-asmjs.py @@ -0,0 +1,16 @@ +# Test for special asmjs SIGSEGV-handling. +# +# Expected behavior is for the asm.js code in the following fragment to trigger +# SIGSEGV. The code in js/src/gdb/mozilla/asmjs.py should prevent GDB from +# handling that signal. +# flake8: noqa: F821 + +run_fragment("asmjs.segfault") + +# If SIGSEGV handling is broken, GDB would have stopped at the SIGSEGV signal. +# The breakpoint would not have hit, and run_fragment would have thrown. +# +# So if we get here, and the asm.js code actually ran, we win. + +assert_pretty("ok", "true") +assert_pretty("rval", '$JS::Value("ok")') diff --git a/js/src/gdb/tests/test-jsbytecode.cpp b/js/src/gdb/tests/test-jsbytecode.cpp new file mode 100644 index 0000000000..c28c6da28a --- /dev/null +++ b/js/src/gdb/tests/test-jsbytecode.cpp @@ -0,0 +1,32 @@ +#include "gdb-tests.h" +#include "js/CompilationAndEvaluation.h" +#include "js/CompileOptions.h" +#include "js/SourceText.h" +#include "util/Text.h" +#include "vm/JSFunction.h" +#include "vm/JSScript.h" +#include "mozilla/Utf8.h" + +FRAGMENT(jsbytecode, simple) { + constexpr unsigned line0 = __LINE__; + static const char chars[] = R"( + debugger; + )"; + + JS::CompileOptions opts(cx); + opts.setFileAndLine(__FILE__, line0 + 1); + + JS::SourceText<mozilla::Utf8Unit> srcBuf; + JS::Rooted<JS::Value> rval(cx); + + bool ok = + srcBuf.init(cx, chars, js_strlen(chars), JS::SourceOwnership::Borrowed); + + JSScript* script = JS::Compile(cx, opts, srcBuf); + jsbytecode* code = script->code(); + + breakpoint(); + + use(ok); + use(code); +} diff --git a/js/src/gdb/tests/test-jsbytecode.py b/js/src/gdb/tests/test-jsbytecode.py new file mode 100644 index 0000000000..e69732d4c7 --- /dev/null +++ b/js/src/gdb/tests/test-jsbytecode.py @@ -0,0 +1,9 @@ +# Basic unit tests for jsbytecode* pretty-printer. +# flake8: noqa: F821 + +assert_subprinter_registered("SpiderMonkey", "ptr-to-jsbytecode") + +run_fragment("jsbytecode.simple") + +assert_pretty("ok", "true") +assert_pretty("code", " (JSOp::Debugger)") diff --git a/js/src/gdb/tests/test-jsid.cpp b/js/src/gdb/tests/test-jsid.cpp new file mode 100644 index 0000000000..8104abcccd --- /dev/null +++ b/js/src/gdb/tests/test-jsid.cpp @@ -0,0 +1,44 @@ +#include "gdb-tests.h" + +#include "js/String.h" +#include "js/Symbol.h" + +FRAGMENT(jsid, simple) { + const char* chars = "moon"; + JS::Rooted<JSString*> string(cx, JS_NewStringCopyZ(cx, chars)); + JS::Rooted<JSString*> interned(cx, JS_AtomizeAndPinString(cx, chars)); + JS::Rooted<jsid> string_id(cx, JS::PropertyKey::fromPinnedString(interned)); + JS::Rooted<jsid> int_id(cx, JS::PropertyKey::Int(1729)); + JS::Rooted<jsid> unique_symbol_id( + cx, JS::PropertyKey::Symbol(JS::NewSymbol(cx, interned))); + JS::Rooted<jsid> registry_symbol_id( + cx, JS::PropertyKey::Symbol(JS::GetSymbolFor(cx, interned))); + JS::Rooted<jsid> well_known_symbol_id( + cx, JS::GetWellKnownSymbolKey(cx, JS::SymbolCode::iterator)); + jsid void_id = JS::PropertyKey::Void(); + + breakpoint(); + + use(string_id); + use(int_id); + use(unique_symbol_id); + use(registry_symbol_id); + use(well_known_symbol_id); + use(void_id); +} + +void jsid_handles(JS::Handle<jsid> jsid_handle, + JS::MutableHandle<jsid> mutable_jsid_handle) { + // Prevent the linker from unifying this function with others that are + // equivalent in machine code but not type. + fprintf(stderr, "Called " __FILE__ ":jsid_handles\n"); + breakpoint(); +} + +FRAGMENT(jsid, handles) { + const char* chars = "shovel"; + JS::Rooted<JSString*> string(cx, JS_NewStringCopyZ(cx, chars)); + JS::Rooted<JSString*> interned(cx, JS_AtomizeAndPinString(cx, chars)); + JS::Rooted<jsid> string_id(cx, JS::PropertyKey::fromPinnedString(interned)); + jsid_handles(string_id, &string_id); +} diff --git a/js/src/gdb/tests/test-jsid.py b/js/src/gdb/tests/test-jsid.py new file mode 100644 index 0000000000..ea43439f2f --- /dev/null +++ b/js/src/gdb/tests/test-jsid.py @@ -0,0 +1,19 @@ +# Tests for jsid pretty-printing +# flake8: noqa: F821 + +assert_subprinter_registered("SpiderMonkey", "JS::PropertyKey") + +run_fragment("jsid.simple") + +assert_pretty("string_id", '$jsid("moon")') +assert_pretty("int_id", "$jsid(1729)") +unique_symbol_pretty = str(gdb.parse_and_eval("unique_symbol_id")).split("@")[0] +assert_eq(unique_symbol_pretty, '$jsid(Symbol("moon"))') +assert_pretty("registry_symbol_id", '$jsid(Symbol.for("moon"))') +assert_pretty("well_known_symbol_id", "$jsid(Symbol.iterator)") +assert_pretty("void_id", "JS::VoidPropertyKey") + +run_fragment("jsid.handles") + +assert_pretty("jsid_handle", '$jsid("shovel")') +assert_pretty("mutable_jsid_handle", '$jsid("shovel")') diff --git a/js/src/gdb/tests/test-jsop.cpp b/js/src/gdb/tests/test-jsop.cpp new file mode 100644 index 0000000000..d92c8f0d2e --- /dev/null +++ b/js/src/gdb/tests/test-jsop.cpp @@ -0,0 +1,13 @@ +#include "gdb-tests.h" + +#include "vm/BytecodeUtil.h" + +FRAGMENT(jsop, simple) { + JSOp undefined = JSOp::Undefined; + JSOp debugger = JSOp::Debugger; + + breakpoint(); + + use(undefined); + use(debugger); +} diff --git a/js/src/gdb/tests/test-jsop.py b/js/src/gdb/tests/test-jsop.py new file mode 100644 index 0000000000..fea0da6059 --- /dev/null +++ b/js/src/gdb/tests/test-jsop.py @@ -0,0 +1,9 @@ +# Basic unit tests for JSOp pretty-printer. +# flake8: noqa: F821 + +assert_subprinter_registered("SpiderMonkey", "JSOp") + +run_fragment("jsop.simple") + +assert_pretty("undefined", "JSOp::Undefined") +assert_pretty("debugger", "JSOp::Debugger") diff --git a/js/src/gdb/tests/test-jsval.cpp b/js/src/gdb/tests/test-jsval.cpp new file mode 100644 index 0000000000..3a89a76a83 --- /dev/null +++ b/js/src/gdb/tests/test-jsval.cpp @@ -0,0 +1,49 @@ +#include "gdb-tests.h" + +#include "js/GlobalObject.h" +#include "js/String.h" +#include "js/Symbol.h" +#include "vm/BigIntType.h" + +FRAGMENT(jsval, simple) { + using namespace JS; + + RootedValue fortytwo(cx, Int32Value(42)); + RootedValue fortytwoD(cx, DoubleValue(42)); + RootedValue negone(cx, Int32Value(-1)); + RootedValue undefined(cx, UndefinedValue()); + RootedValue null(cx, NullValue()); + RootedValue js_true(cx, BooleanValue(true)); + RootedValue js_false(cx, BooleanValue(false)); + RootedValue elements_hole(cx, js::MagicValue(JS_ELEMENTS_HOLE)); + + RootedValue empty_string(cx); + empty_string.setString(JS_NewStringCopyZ(cx, "")); + RootedString hello(cx, JS_NewStringCopyZ(cx, "Hello!")); + RootedValue friendly_string(cx, StringValue(hello)); + RootedValue symbol(cx, SymbolValue(GetSymbolFor(cx, hello))); + RootedValue bi(cx, BigIntValue(BigInt::zero(cx))); + + RootedValue global(cx); + global.setObject(*CurrentGlobalOrNull(cx)); + + // Some interesting value that floating-point won't munge. + RootedValue onehundredthirtysevenonehundredtwentyeighths( + cx, DoubleValue(137.0 / 128.0)); + + breakpoint(); + + use(fortytwo); + use(fortytwoD); + use(negone); + use(undefined); + use(js_true); + use(js_false); + use(null); + use(elements_hole); + use(empty_string); + use(friendly_string); + use(symbol); + use(bi); + use(global); +} diff --git a/js/src/gdb/tests/test-jsval.py b/js/src/gdb/tests/test-jsval.py new file mode 100644 index 0000000000..e5a42e72f4 --- /dev/null +++ b/js/src/gdb/tests/test-jsval.py @@ -0,0 +1,23 @@ +# Basic unit tests for jsval pretty-printer. +# flake8: noqa: F821 + +assert_subprinter_registered("SpiderMonkey", "JS::Value") + +run_fragment("jsval.simple") + +assert_pretty("fortytwo", "$JS::Int32Value(42)") +assert_pretty("fortytwoD", "$JS::DoubleValue(42.0)") +assert_pretty("negone", "$JS::Int32Value(-1)") +assert_pretty("undefined", "$JS::UndefinedValue()") +assert_pretty("null", "$JS::NullValue()") +assert_pretty("js_true", "$JS::BooleanValue(true)") +assert_pretty("js_false", "$JS::BooleanValue(false)") +assert_pretty("elements_hole", "$JS::MagicValue(JS_ELEMENTS_HOLE)") +assert_pretty("empty_string", '$JS::Value("")') +assert_pretty("friendly_string", '$JS::Value("Hello!")') +assert_pretty("symbol", '$JS::Value(Symbol.for("Hello!"))') +assert_pretty("bi", "$JS::BigIntValue()") +assert_pretty("global", "$JS::Value((JSObject *) [object global])") +assert_pretty( + "onehundredthirtysevenonehundredtwentyeighths", "$JS::DoubleValue(1.0703125)" +) diff --git a/js/src/gdb/tests/test-prettyprinters.cpp b/js/src/gdb/tests/test-prettyprinters.cpp new file mode 100644 index 0000000000..af71c52fa3 --- /dev/null +++ b/js/src/gdb/tests/test-prettyprinters.cpp @@ -0,0 +1,38 @@ +#include "gdb-tests.h" + +typedef int A; +typedef A B; + +class C {}; +class D {}; +typedef C C_; +typedef D D_; +class E : C, D {}; +typedef E E_; +class F : C_, D_ {}; +class G {}; +class H : F, G {}; + +FRAGMENT(prettyprinters, implemented_types) { + int i = 0; + A a = 0; + B b = 0; + C c; + C_ c_; + E e; + E_ e_; + F f; + H h; + + breakpoint(); + + use(i); + use(a); + use(b); + use(c); + use(c_); + use(e); + use(e_); + use(f); + use(h); +} diff --git a/js/src/gdb/tests/test-prettyprinters.py b/js/src/gdb/tests/test-prettyprinters.py new file mode 100644 index 0000000000..f3329c0288 --- /dev/null +++ b/js/src/gdb/tests/test-prettyprinters.py @@ -0,0 +1,40 @@ +# Ignore flake8 errors "undefined name 'assert_pretty'" +# As it caused by the way we instanciate this file +# flake8: noqa: F821 + +import mozilla.prettyprinters + +run_fragment("prettyprinters.implemented_types") + + +def implemented_type_names(expr): + v = gdb.parse_and_eval(expr) + it = mozilla.prettyprinters.implemented_types(v.type) + return [str(_) for _ in it] + + +assert_eq(implemented_type_names("i"), ["int"]) +assert_eq(implemented_type_names("a"), ["A", "int"]) +assert_eq(implemented_type_names("b"), ["B", "A", "int"]) +assert_eq(implemented_type_names("c"), ["C"]) +assert_eq(implemented_type_names("c_"), ["C_", "C"]) +assert_eq(implemented_type_names("e"), ["E", "C", "D"]) +assert_eq(implemented_type_names("e_"), ["E_", "E", "C", "D"]) + +# Some compilers strip trivial typedefs in the debuginfo from classes' base +# classes. Sometimes this can be fixed with -fno-eliminate-unused-debug-types, +# but not always. Allow this test to pass if the typedefs are stripped. +# +# It would probably be better to figure out how to make the compiler emit them, +# since I think this test is here for a reason. +if gdb.lookup_type("F").fields()[0].name == "C_": + # We have the typedef info. + assert_eq(implemented_type_names("f"), ["F", "C_", "D_", "C", "D"]) + assert_eq(implemented_type_names("h"), ["H", "F", "G", "C_", "D_", "C", "D"]) +else: + assert_eq(implemented_type_names("f"), ["F", "C", "D"]) + assert_eq(implemented_type_names("h"), ["H", "F", "G", "C", "D"]) + +# Check that our pretty-printers aren't interfering with printing other types. +assert_pretty("10", "10") +assert_pretty("(void*) 0", "") # Because of 'set print address off' diff --git a/js/src/gdb/tests/test-unwind.cpp b/js/src/gdb/tests/test-unwind.cpp new file mode 100644 index 0000000000..a3dc6fe89c --- /dev/null +++ b/js/src/gdb/tests/test-unwind.cpp @@ -0,0 +1,76 @@ +#include "gdb-tests.h" +#include "jsfriendapi.h" // JSFunctionSpecWithHelp + +#include "jit/JitOptions.h" // js::jit::JitOptions +#include "js/CallArgs.h" // JS::CallArgs, JS::CallArgsFromVp +#include "js/CompilationAndEvaluation.h" // JS::Evaluate +#include "js/CompileOptions.h" // JS::CompileOptions +#include "js/GlobalObject.h" // JS::CurrentGlobalOrNull +#include "js/PropertyAndElement.h" // JS_DefineProperty +#include "js/PropertyDescriptor.h" // JSPROP_ENUMERATE +#include "js/RootingAPI.h" // JS::Rooted +#include "js/SourceText.h" // JS::Source{Ownership,Text} +#include "js/Value.h" // JS::Value + +#include "mozilla/Utf8.h" // mozilla::Utf8Unit + +#include <stdint.h> // uint32_t +#include <string.h> // strlen + +static bool Something(JSContext* cx, unsigned argc, JS::Value* vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + args.rval().setInt32(23); + breakpoint(); + return true; +} + +// clang-format off +static const JSFunctionSpecWithHelp unwind_functions[] = { + JS_FN_HELP("something", Something, 0, 0, +"something()", +" Test function for test-unwind."), + JS_FS_HELP_END +}; +// clang-format on + +FRAGMENT(unwind, simple) { + using namespace JS; + + JS::Rooted<JSObject*> global(cx, JS::CurrentGlobalOrNull(cx)); + if (!JS_DefineFunctionsWithHelp(cx, global, unwind_functions)) { + return; + } + + // Define an itercount property and use it to ensure Baseline compilation. + uint32_t threshold = js::jit::JitOptions.baselineJitWarmUpThreshold; + RootedValue val(cx, Int32Value(threshold + 10)); + if (!JS_DefineProperty(cx, global, "itercount", val, 0)) { + return; + } + + int line0 = __LINE__; + const char* bytes = + "\n" + "function unwindFunctionInner() {\n" + " for (var i = 0; i < itercount; i++) {}\n" + " return something();\n" + "}\n" + "\n" + "function unwindFunctionOuter() {\n" + " for (var i = 0; i < itercount; i++) {}\n" + " return unwindFunctionInner();\n" + "}\n" + "\n" + "unwindFunctionOuter();\n"; + + JS::CompileOptions opts(cx); + opts.setFileAndLine(__FILE__, line0 + 1); + + JS::SourceText<mozilla::Utf8Unit> srcBuf; + if (!srcBuf.init(cx, bytes, strlen(bytes), JS::SourceOwnership::Borrowed)) { + return; + } + + JS::Rooted<JS::Value> rval(cx); + JS::Evaluate(cx, opts, srcBuf, &rval); +} diff --git a/js/src/gdb/tests/test-unwind.py b/js/src/gdb/tests/test-unwind.py new file mode 100644 index 0000000000..d12c4b0f6e --- /dev/null +++ b/js/src/gdb/tests/test-unwind.py @@ -0,0 +1,61 @@ +# Test the unwinder and the frame filter. +# flake8: NOQA: F821 +import platform + + +def do_unwinder_test(): + # The unwinder is disabled by default for the moment. Turn it on to check + # that the unwinder works as expected. + import gdb + + gdb.execute("enable unwinder .* SpiderMonkey") + + run_fragment("unwind.simple", "Something") + + first = True + # The unwinder is a bit flaky still but should at least be able to + # recognize one set of entry and exit frames. This also tests to + # make sure we didn't end up solely in the interpreter. + found_entry = False + found_exit = False + found_main = False + found_inner = False + found_outer = False + frames = list(gdb.frames.execute_frame_filters(gdb.newest_frame(), 0, -1)) + for frame in frames: + print("examining " + frame.function()) + if first: + assert_eq(frame.function().startswith("Something"), True) + first = False + elif frame.function() == "<<FrameType::Exit>>": + found_exit = True + elif frame.function() == "<<FrameType::CppToJSJit>>": + found_entry = True + elif frame.function() == "main": + found_main = True + elif "unwindFunctionInner" in frame.function(): + found_inner = True + elif "unwindFunctionOuter" in frame.function(): + found_outer = True + + # Had to have found a frame. + assert_eq(first, False) + # Had to have found main. + assert_eq(found_main, True) + # Had to have found the entry and exit frames. + assert_eq(found_exit, True) + assert_eq(found_entry, True) + # Had to have found the names of the two JS functions. + assert_eq(found_inner, True) + assert_eq(found_outer, True) + + +# Only on the right platforms. +if platform.machine() == "x86_64" and platform.system() == "Linux": + # Only test when gdb has the unwinder feature. + try: + import gdb.unwinder # NOQA: F401 + + do_unwinder_test() + except Exception: + pass diff --git a/js/src/gdb/tests/typedef-printers.cpp b/js/src/gdb/tests/typedef-printers.cpp new file mode 100644 index 0000000000..efb01f82cf --- /dev/null +++ b/js/src/gdb/tests/typedef-printers.cpp @@ -0,0 +1,11 @@ +#include "gdb-tests.h" + +typedef int my_typedef; + +FRAGMENT(typedef_printers, one) { + my_typedef i = 0; + + breakpoint(); + + use(i); +} diff --git a/js/src/gdb/tests/typedef-printers.py b/js/src/gdb/tests/typedef-printers.py new file mode 100644 index 0000000000..888379ee63 --- /dev/null +++ b/js/src/gdb/tests/typedef-printers.py @@ -0,0 +1,18 @@ +# Test that we can find pretty-printers for typedef names, not just for +# struct types and templates. +# flake8: noqa: F821 + +import mozilla.prettyprinters + + +@mozilla.prettyprinters.pretty_printer("my_typedef") +class my_typedef(object): + def __init__(self, value, cache): + pass + + def to_string(self): + return "huzzah" + + +run_fragment("typedef_printers.one") +assert_pretty("i", "huzzah") |