Files
Joyee Cheung 8bda32a987 [tools] recover parameter names from ScopeInfo in debug helper plugins
Previously `v8 args` only shows arguments of the functions in the stack
frames. This patch makes GetStackFrame in the debug helper forward the
context local information stored in ScopeInfo in the returned
properties, so the plugins can recover the parameter names in `bt` and
allow `v8 inspect` to resolve by parameter names.

The previews show the arguments as passed at the call site. Only the
names captured in the closures can be recovered, the rest still gets
positional [N] names. For example:

   function outer(a, b, c) {
     function capture() {
       return a + c;   // captures a and c, not b
     }
   }

  (gdb) bt
  #6 ... [outer @ captured-args.js:9:15]
  (this=0x..., a=<Smi: 1>, [1]=<Smi: 2>, c=<Smi: 3>)

  (gdb) v8 args 6
  #6  outer @ captured-args.js:9:15
  this = 0x... <JSGlobalProxy>
  a = <Smi: 1>
  [1] = <Smi: 2>
  c = <Smi: 3>

  (gdb) v8 inspect c
  <Smi: 3>

To implement this, we expose the ScopeInfo::VariableProperties bitfield
aliases for the helper.

In order to support builds without pointer compression/cages, this also
includes isolate pointer in HeapAddresses in order to fill the
read_only_space_first_page and old_space_first_page of a HeapAddress
from the isolate root table, since without pointer compression, the
helper cannot derive these pages by itself.

Change-Id: Ie12b1e4e159836f30071cb009aadb46117064865
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/8545535
Reviewed-by: Paolo Severini <paolosev@microsoft.com>
Reviewed-by: Leszek Swirski <leszeks@chromium.org>
Commit-Queue: Joyee Cheung <joyee@igalia.com>
Cr-Commit-Position: refs/heads/main@{#111626}
2026-11-04 08:52:16 -08:00
..

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]|<param-name> [--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>, name=..., ...)

The annotation shows the this value and a preview of the first few arguments. Arguments are labeled with their parameter names when those can be recovered, usually when a closure captures them, and with the positional [N] index otherwise. The previews show the arguments as passed at the call site, not the value in the function scope if the function reassigns them.

To see the full list of arguments, use v8 args <frame#>. If the argument count is unreadable, the annotation displays (this=0x..., argc=?). If the this value 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]|<param-name>

Inspects a tagged V8 object at <addr> and prints its properties. If this is used, it refers to the this value of the selected JS frame. If [N] or a recovered parameter name is used, it refers to one of the arguments of the selected JS frame. If the name does not match a recovered parameter, the plugin falls back to the debugger's own expression evaluation.

(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  outer @ captured-args.js:5:15
this = 0x1d190100497d <JSGlobalProxy>
a = <Smi: 1>
[1] = <Smi: 2>
c = <Smi: 3>

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-cores generates core files from the test scripts for reuse in run-core-* and the core test_* targets. The files are saved to CORE_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_live or test_lldb_core. Append a class to run a single suite, e.g. test_gdb_core.GdbInspectCoreTest.
    • Modules whose name contains core load from prepared cores and depend on prepare-cores.
  • Convenience suites that run several tests at once:
    • test-live runs the live tests on both debuggers.
    • test-core runs the core-file tests on both debuggers.
    • test-gdb / test-lldb run both tests for one debugger.
    • test runs 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 tests
    • test_*.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.