Files
Joyee Cheung 4c1f4c0541 [tools] add argument previews and v8 args to debug helper plugins
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}
2026-09-29 07:22:41 -07: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] [--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-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.