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}
284 lines
8.7 KiB
Python
284 lines
8.7 KiB
Python
# Copyright 2026 the V8 project authors. All rights reserved.
|
|
# Use of this source code is governed by a BSD-style license that can be
|
|
# found in the LICENSE file.
|
|
"""GDB integration for the V8 debugger bridge."""
|
|
|
|
# GDB does not add the directory of the current script to the module
|
|
# search path, so we need to do it ourselves to load the v8dbg/ package.
|
|
import os
|
|
import sys
|
|
import traceback
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
# The gdb module only exists inside gdb's embedded Python, where typeshed
|
|
# provides stubs but no importable source.
|
|
import gdb # pyright: ignore[reportMissingModuleSource]
|
|
from gdb.FrameDecorator import ( # pyright: ignore[reportMissingModuleSource]
|
|
FrameDecorator)
|
|
|
|
from v8dbg.commands import debug_helper_lib_warning
|
|
from v8dbg.commands import dispatch_v8_command
|
|
from v8dbg.hints import resolve_heap_hints
|
|
from v8dbg.shared_bridge import get_bridge
|
|
|
|
_VERBOSE = os.environ.get("V8_DEBUG_HELPER_VERBOSE", "") != ""
|
|
|
|
|
|
def _read_memory_callback(address, byte_count):
|
|
"""Read target memory through the currently selected inferior."""
|
|
return bytes(gdb.selected_inferior().read_memory(address, byte_count))
|
|
|
|
|
|
def _ptr_size():
|
|
"""Best-effort target pointer size."""
|
|
try:
|
|
return gdb.lookup_type("void").pointer().sizeof
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
class _GdbResolver:
|
|
"""GDB adapter for resolving symbols, pointers, offsets, etc."""
|
|
|
|
def __init__(self, ptr_size):
|
|
self._ptr_size = ptr_size
|
|
|
|
def read_pointer(self, addr):
|
|
try:
|
|
data = bytes(gdb.selected_inferior().read_memory(addr, self._ptr_size))
|
|
except Exception:
|
|
return None
|
|
if len(data) != self._ptr_size:
|
|
return None
|
|
return int.from_bytes(data, "little")
|
|
|
|
def symbol_address(self, name):
|
|
"""Resolve the address of the variable `name` for the selected thread.
|
|
|
|
For a TLS variable this is the address of the selected thread's copy.
|
|
"""
|
|
try:
|
|
addr = None
|
|
sym = gdb.lookup_global_symbol(name)
|
|
if sym is not None:
|
|
try:
|
|
addr = sym.value().address
|
|
except Exception:
|
|
# If we cannot locate the TLS symbol, fall back to evaluation.
|
|
pass
|
|
if addr is None:
|
|
# `&name` via parse_and_eval resolves TLS for the selected thread and,
|
|
# unlike lookup_global_symbol, also sees minimal symbols from release
|
|
# builds.
|
|
addr = gdb.parse_and_eval(f"&'{name}'")
|
|
if addr is None:
|
|
return None
|
|
return int(addr)
|
|
except Exception:
|
|
if _VERBOSE:
|
|
traceback.print_exc()
|
|
return None
|
|
|
|
# gdb resolves TLS natively.
|
|
debug_symbol_address = symbol_address
|
|
|
|
def field_offset(self, type_name, field_name):
|
|
"""Return the byte offset of `field_name` inside `type_name`."""
|
|
try:
|
|
t = gdb.lookup_type(type_name)
|
|
except Exception:
|
|
return None
|
|
try:
|
|
field = t[field_name]
|
|
except Exception:
|
|
return None
|
|
# https://sourceware.org/gdb/current/onlinedocs/gdb.html/Types-In-Python.html
|
|
bitpos = getattr(field, "bitpos", None)
|
|
if bitpos is None:
|
|
return None
|
|
return int(bitpos) // 8
|
|
|
|
|
|
def _evaluate_address_in_gdb(text):
|
|
"""Evaluate a debugger expression as an integer address."""
|
|
try:
|
|
return int(gdb.parse_and_eval(text))
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
def _frame_pointer(frame):
|
|
"""Read the frame pointer of a gdb.Frame, or 0 when unavailable."""
|
|
# Extend this register list as the plugin grows support for more
|
|
# architectures. Today the tested targets are x64 and arm64.
|
|
for register_name in ("rbp", "fp", "x29"):
|
|
try:
|
|
value = frame.read_register(register_name)
|
|
except Exception:
|
|
continue
|
|
try:
|
|
frame_pointer = int(value)
|
|
except Exception:
|
|
frame_pointer = 0
|
|
if frame_pointer:
|
|
return frame_pointer
|
|
return 0
|
|
|
|
|
|
def _resolve_frame_fp(index):
|
|
"""Map a frame number (None = selected frame) to (level, frame pointer)."""
|
|
try:
|
|
if index is None:
|
|
frame = gdb.selected_frame()
|
|
else:
|
|
frame = gdb.newest_frame()
|
|
while frame is not None and frame.level() != index:
|
|
frame = frame.older()
|
|
if frame is None:
|
|
return None
|
|
frame_pointer = _frame_pointer(frame)
|
|
if not frame_pointer:
|
|
return None
|
|
return (frame.level(), frame_pointer)
|
|
except Exception:
|
|
if _VERBOSE:
|
|
traceback.print_exc()
|
|
return None
|
|
|
|
|
|
class V8DbgFrameDecorator(FrameDecorator):
|
|
"""Appends V8 JS frame annotations to backtraces."""
|
|
|
|
def __init__(self, frame_obj, hints):
|
|
super().__init__(frame_obj)
|
|
self._hints = hints
|
|
|
|
def function(self):
|
|
"""Keep the native frame name when it is useful, else append JS context."""
|
|
base_name = super().function()
|
|
frame = self.inferior_frame()
|
|
if not base_name:
|
|
try:
|
|
base_name = frame.name() or ""
|
|
except Exception:
|
|
if _VERBOSE:
|
|
traceback.print_exc()
|
|
base_name = ""
|
|
# On non-pointer-compressed builds with short builtin calls enabled, the
|
|
# builtins are remapped and no longer sit at the in-binary address range
|
|
# registered in the symbol table, so gdb cannot symbolize them and renders
|
|
# them as '???'. Strip the placeholder so an annotated frame does not read
|
|
# '??? [func @ ...]'.
|
|
if base_name == "???":
|
|
base_name = ""
|
|
# This is a heuristic to early skip frames that clearly not builtin frames.
|
|
# We only try to symbolicate frames that are either not symbolicated, or have a
|
|
# name that starts with "Builtins_". The call frame_suffix() below will check
|
|
# if it's a genuine JS frame.
|
|
if base_name and not base_name.startswith("Builtins_"):
|
|
return base_name
|
|
frame_pointer = _frame_pointer(frame)
|
|
if not frame_pointer:
|
|
return base_name
|
|
|
|
# frame_suffix() consults debug_helper, which returns a non-empty
|
|
# annotation only for genuine JS frames, otherwise it returns an empty string.
|
|
bridge = get_bridge(_ptr_size())
|
|
suffix = bridge.frame_suffix(frame_pointer, _read_memory_callback,
|
|
self._hints)
|
|
if not suffix:
|
|
return base_name
|
|
if not base_name:
|
|
return suffix.strip()
|
|
return f"{base_name}{suffix}"
|
|
|
|
|
|
class V8DbgFrameFilter:
|
|
"""Registers the V8 frame decorator."""
|
|
|
|
def __init__(self):
|
|
self.name = "v8dbg_bridge"
|
|
self.priority = 100
|
|
self.enabled = True
|
|
progspace = gdb.current_progspace()
|
|
if progspace is not None:
|
|
progspace.frame_filters[self.name] = self
|
|
else:
|
|
gdb.frame_filters[self.name] = self
|
|
|
|
def filter(self, iterator):
|
|
"""Wrap each frame with the V8-aware decorator."""
|
|
hints = resolve_heap_hints(_GdbResolver(_ptr_size()))
|
|
return (V8DbgFrameDecorator(frame_obj, hints) for frame_obj in iterator)
|
|
|
|
|
|
class V8Command(gdb.Command):
|
|
"""V8 debug helpers.
|
|
Syntax: v8 <subcommand> [options]
|
|
|
|
Available subcommands:
|
|
|
|
- inspect -- Inspect the heap object at <addr> with the V8 inspector.
|
|
|
|
v8 inspect <addr>|this|[N]|<param-name> [--type T] [--depth N] [--array-length N]
|
|
|
|
* this / [N] / <param-name>: inspect the receiver or an argument of
|
|
the selected JS frame instead of a raw address.
|
|
* --type T: treat the object as if it has type T (e.g. `v8::internal::JSArray`).
|
|
* --depth N: limit recursive inspection depth to N (default: 1).
|
|
* --array-length N: for arrays, inspect up to N elements (default: 16).
|
|
|
|
- args -- Print the receiver and the arguments of a JS frame.
|
|
|
|
v8 args [frame#]
|
|
|
|
* frame#: the frame to show (default: the selected frame).
|
|
|
|
- isolate -- Print the current Isolate address of the selected thread.
|
|
|
|
v8 isolate
|
|
|
|
- source -- Print the source span of the function a JS frame is running.
|
|
|
|
v8 source [frame#] [--max-lines N]
|
|
|
|
* frame#: the frame to show (default: the selected frame).
|
|
* --max-lines N: cap the number of span lines shown (default: 10).
|
|
|
|
The span covers the whole function; the exact executing line is not
|
|
recovered.
|
|
"""
|
|
|
|
def __init__(self):
|
|
super().__init__("v8", gdb.COMMAND_USER, prefix=False)
|
|
|
|
def invoke(self, argument, from_tty):
|
|
"""Dispatch the command and stream output back to the gdb console."""
|
|
try:
|
|
argv = gdb.string_to_argv(argument)
|
|
except gdb.error:
|
|
argv = argument.split()
|
|
bridge = get_bridge(_ptr_size())
|
|
ok, text = dispatch_v8_command(
|
|
bridge,
|
|
argv,
|
|
read_memory=_read_memory_callback,
|
|
eval_address=_evaluate_address_in_gdb,
|
|
resolver=_GdbResolver(bridge.ptr_size),
|
|
verbose=_VERBOSE,
|
|
frame_fp=_resolve_frame_fp,
|
|
)
|
|
if not ok:
|
|
gdb.write(text + "\n", gdb.STDERR)
|
|
elif text:
|
|
gdb.write(text)
|
|
|
|
|
|
_lib_warning = debug_helper_lib_warning()
|
|
if _lib_warning is not None:
|
|
gdb.write(_lib_warning + "\n", gdb.STDERR)
|
|
else:
|
|
V8DbgFrameFilter()
|
|
V8Command()
|