This patch adds previews of JS function arguments in backtrace annotations. For example: (gdb) bt #6 ... [test_func_3 @ throw.js:15:21] (this=0x..., [0]=<Smi: 44>, [1]=0x... <Oddball: TrueValue>, ...) This also adds a `v8 args` command to show the full argument list for a frame. (gdb) v8 args 6 #6 test_func_3 @ .../throw.js:15:21 this = 0x... <JSGlobalProxy> [0]= <Smi: 44> [1]= 0x... <Oddball: TrueValue> ... In addition, `v8 inspect` now accepts "this" and "[N]" as argument for inspecting the receiver and arguments of the selected frame. For example: (gdb) v8 inspect this 0x... <JSGlobalProxy> .map=0x... <Map> .properties_or_hash=<Smi: 11106> ... (gdb) v8 inspect [1] 0x... <Oddball: TrueValue> .map=0x... <Map: BooleanMap> ... The debug helper now derives argument offsets from StandardFrameConstants instead of hardcoding frame-pointer offsets, and report them as "receiver" and "arguments" properties in the array, the latter has a size of the argc slot (minus the receiver). The C ABI is unchanged. Change-Id: I8fcac49835cbf412d8959fdb325dc85a0e89ad58 Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/8293838 Commit-Queue: Joyee Cheung <joyee@igalia.com> Reviewed-by: Paolo Severini <paolosev@microsoft.com> Cr-Commit-Position: refs/heads/main@{#110161}
V8 Debug Helper Plugins
This directory contains Python debugger plugins that load
libv8_debug_helper to facilitate post-mortem and live debugging of V8
frames and objects in GDB and LLDB.
How To Build It
Build the shared debug helper library from the repository root:
autoninja -C out/<config> v8_debug_helper_shared
The plugins load the library from the V8_DEBUG_HELPER_LIB_PATH
environment variable.
How To Use It
These plugins are only tested to work on x64 and arm64 Linux and arm64 macOS, where the debuggers support at least Python 3.9+.
First set V8_DEBUG_HELPER_LIB_PATH environment variable to the built shared
library.
For GDB, run the following commands:
source tools/gdbinit
source tools/debug_helper/plugins/gdb_plugin.py
For LLDB:
command script import tools/debug_helper/plugins/lldb_plugin.py
The LLDB plugin installs its own frame-format setting for the session. Import
it in a debugger session where that takeover is acceptable.
Current features
Once loaded, the plugins append JavaScript annotations to candidate V8 frames
when you print a backtrace via bt, and adds a v8 command. Currently, v8 supports the following subcommands:
v8 inspect <addr>|this|[N] [--type T] [--depth N] [--array-length N]
v8 args [frame#]
v8 isolate
v8 source [frame#] [--max-lines N]
Frame annotations
The annotation format for frames displayed in bt is:
[<function_name> @ <script_name>:<line>:<column>] (this=0x..., [0]=<Smi: 1>, ...)
The annotation shows the receiver and a preview of the first few
arguments, labeled positionally as [N]. To see the full list
of arguments, use v8 args <frame#>. If the argument count is
unreadable, the annotation is (this=0x..., argc=?).
If the receiver slot is unreadable, it will be omitted.
If source text cannot be recovered but the script name still can, the annotation degrades to:
[<function_name> @ <script_name>]
For anonymous functions the name is shown as <anonymous>. The line and column
point to the start of the function scope in its definition, which is
normally the ( of the parameter list (or position 1:1 for the top-level
script scope), not where the function is called, which we cannot
reliably recover in the debugger.
v8 inspect <addr>|this|[N]
Inspects a tagged V8 object at <addr> and prints its properties. Instead of
an address, this and [N] refer to the receiver or the Nth argument of the
selected JS frame.
(gdb) v8 inspect 0x34f49880471
0x34f49880471 <JSArray: length=3>
.map=0x34f49880409 <Map: for JSArray>
.properties_or_hash=0x34f49880421 <FixedArray: empty>
.elements=0x34f49880491 <FixedArray: length=3>
.length=<Smi: 3>
Options:
| Flag | Effect |
|---|---|
--type <T> |
Type hint. Useful when the Map is unreadable (e.g. v8::internal::JSArray). |
--depth N |
Inline-recursion depth for child inspection (default: 1). |
--array-length N |
Per-array element cap (default: 16). |
When the object's Map can't be read (e.g. due to memory corruption), the renderer adds
a could be one of ... footer with ready-to-paste --type suggestions.
v8 args [frame#]
Prints the receiver and all the arguments of a JS frame. frame# is the
frame number shown by bt and defaults to the selected frame.
(gdb) v8 args 5
#5 test_func_3 @ throw.js:15:21
this = 0x1d190100497d <JSGlobalProxy>
[0] = <Smi: 44>
[1] = 0x1d1900000071 <Oddball: TrueValue>
[2] = 0x1d19010492dd <JSObject>
[3] = 0x1d19010492ed <SeqOneByteString: "hello!">
v8 isolate
Prints the current Isolate address of the selected thread.
(gdb) v8 isolate
isolate = 0x7f1efd3c8000
If the selected thread has not entered an isolate (e.g. an idle worker
thread), it prints isolate = <none>.
v8 source [frame#] [--max-lines N]
Prints the source span of the function a JS frame is running.
frame# is the frame number shown by bt and defaults to the selected frame.
(gdb) v8 source 5
#5 test_func_3 @ throw.js:15:21 (this=0x1d190100497d, argc=4)
14
+---- frame 5: test_func_3 -----
15 | function test_func_3(n, b, o, s) {
16 | throw new Error("v8dbg bridge test: " + s);
17 | }
+---- frame 5: test_func_3 -----
The span covers the whole function the frame is running, from the start of its scope
to its last character. Long spans (e.g. for top-level frames, whose span is the
whole script) are truncated with a ... (<K> more lines) note after 10 lines,
or after --max-lines N lines when the flag is given.
Examples
To look at the JS frames on the stack and inspect a receiver or argument, paste
an address from the frame annotation into v8 inspect, or select the frame and
use the this/[N] shorthands:
(gdb) bt
...
#5 0x... in Builtins_InterpreterEntryTrampoline [test_func_3 @ throw.js:15:21] (this=0x1d190100497d, [0]=<Smi: 44>, [1]=0x1d1900000071 <Oddball: TrueValue>, [2]=0x1d19010492dd <JSObject>, [3]=0x1d19010492ed <SeqOneByteString: "hello!">)
...
(gdb) v8 inspect 0x1d190100497d
0x1d190100497d <JSGlobalProxy>
.map=0x1d190101c3ed <Map>
.properties_or_hash=<Smi: 940196>
.elements=0x1d19000007e5 <FixedArray: EmptyFixedArray>
.cpp_heap_wrappable=0x00000000
(gdb) frame 5
(gdb) v8 inspect [2]
0x1d19010492dd <JSObject>
.map=0x1d19010c95b5 <Map>
...
(gdb) v8 source
#5 test_func_3 @ throw.js:15:21 (this=0x1d190100497d, argc=4)
14
+---- frame 5: test_func_3 -----
15 | function test_func_3(n, b, o, s) {
16 | throw new Error("v8dbg bridge test: " + s);
17 | }
+---- frame 5: test_func_3 -----
How To Test It
Before running the debugger tests, build the test dependencies:
autoninja -C out/<config> d8 v8_debug_helper_shared corruption_harness
There are several types of targets in the Makefile:
prepare-coresgenerates core files from the test scripts for reuse inrun-core-*and the coretest_*targets. The files are saved toCORE_DIR, which defaults to$(OUT_DIR)/debug_helper.cores/and is kept out of source control.run-live-*targets run a debugging session on a test script in non-interactive mode and print the output. This is useful for debugging the plugins themselves.run-core-*targets run the same sessions but load from a prepared core file instead of a live process.
test_<module>targets run one unittest module and assert the expected output, e.g.test_gdb_liveortest_lldb_core. Append a class to run a single suite, e.g.test_gdb_core.GdbInspectCoreTest.- Modules whose name contains
coreload from prepared cores and depend onprepare-cores.
- Modules whose name contains
- Convenience suites that run several tests at once:
test-liveruns the live tests on both debuggers.test-coreruns the core-file tests on both debuggers.test-gdb/test-lldbrun both tests for one debugger.testruns everything.
On macOS, with lldb installed globally, the output directory is
assumed to be out/arm64.release. Build and run the tests with:
autoninja -C out/arm64.release d8 v8_debug_helper_shared corruption_harness
make -C tools/debug_helper/plugins test_lldb_live
# To run core tests, first prepare cores, this can take a while
make -C tools/debug_helper/plugins prepare-cores
make -C tools/debug_helper/plugins test_lldb_core
On Linux, with gdb and lldb installed, the output directory is assumed
to be out/x64.release. Build and run the tests with:
autoninja -C out/x64.release d8 v8_debug_helper_shared corruption_harness
make -C tools/debug_helper/plugins test-live
# To run core tests, first prepare cores, this can take a while
make -C tools/debug_helper/plugins prepare-cores
make -C tools/debug_helper/plugins test-core
If the output directory is different from the assumed one,
set the OUT_DIR environment variable when running the Makefile.
OUT_DIR="$(pwd)/out/x64.release.test" make -C tools/debug_helper/plugins test_lldb_live
Development Notes
Set V8_DEBUG_HELPER_VERBOSE=1 to enable verbose logging in the plugins.
To see the raw output from the debugger without assertions, use the run-*
targets instead of test-*, for example:
make -C tools/debug_helper/plugins run-live-backtrace-lldb
Directory Layout
gdb_plugin.py: GDB plugin entry point.lldb_plugin.py: LLDB plugin entry point.v8dbg/: shared plugin code.test/fixtures/: test scripts and expected annotation fixtures.helpers/: Python helpers for the teststest_*.py: Unit tests. Run them through the Makefile (see "How To Test It").
Design
libv8_debug_helper is the dynamic library built by v8_debug_helper_shared,
which exposes C APIs whose definitions can be found in
debug_helper.h. The DebuggerBridge class in v8dbg/shared_bridge.py
wraps those APIs in Python via ctypes for the debugger plugins
to call into.
GDB / LLDB UI
|
v
GDB/LLDB Python plugins
|
v
DebuggerBridge API
|
v
libv8_debug_helper
|
v
DebuggerBridge memory accessor callback
|
v
live process or core dump memory
See the DebuggerBridge method docstrings in v8dbg/shared_bridge.py for the
current API.