feat(debug): enhance debug graph functionality with auto-discovery of local and global variables
This commit is contained in:
@@ -17,6 +17,21 @@ box:
|
||||
|
||||
$debug_graph(list)
|
||||
$debug_graph(list, "top", top) # extra name/pointer marker pairs
|
||||
|
||||
Besides EXPR and any explicit name/pointer pairs, every call also walks and
|
||||
labels, as additional roots:
|
||||
- every other local variable (and argument) visible at the current PC,
|
||||
from the innermost lexical block out to function scope -- kind "local"
|
||||
- every file-scope global/static visible from the current source file --
|
||||
kind "global"
|
||||
the explicit name/pointer pairs are labeled kind "watched". This is what
|
||||
lets e.g. $debug_graph(top) -- which VS Code's expressionTemplate produces
|
||||
when you just type "top" in the Watch box -- still show the list: `top` is
|
||||
a bare `int*` with no struct to recurse into on its own, but `list` (a
|
||||
sibling local at that point) gets auto-walked too, so `top`'s address lands
|
||||
on a node already present in the combined graph. Each root's "kind" rides
|
||||
along in the output JSON so the visualizer can offer show/hide toggles per
|
||||
category (see vis-plugins/node-table-visualizer.js).
|
||||
"""
|
||||
|
||||
import gdb
|
||||
@@ -135,41 +150,178 @@ def _describe_struct(val, node_id, nodes, edges, visited):
|
||||
|
||||
|
||||
def _visit_pointer(ptr_val, ptr_int, nodes, edges, visited):
|
||||
"""Cycle-guards only on the struct/union branch (via `visited`), and
|
||||
only fills in a scalar leaf if nothing has described this address yet
|
||||
(via `nodes`). With auto-discovered roots (see build_graph_json), the
|
||||
same address can be reached both as a struct-typed pointer (e.g. a
|
||||
linked-list node) and, separately, as some other variable's plain
|
||||
scalar pointer into one of that struct's fields (e.g. a `T*` pointing
|
||||
at a `Node<T>`'s first member, which shares its address). Whichever
|
||||
order those walks happen in, the richer struct description must win --
|
||||
a scalar walk must never downgrade/overwrite an already-described
|
||||
struct node, and a struct walk must always be able to upgrade a
|
||||
previously-recorded scalar leaf at the same address."""
|
||||
node_id = _addr_str(ptr_int)
|
||||
if node_id in visited:
|
||||
return
|
||||
visited.add(node_id)
|
||||
|
||||
pointee_type = ptr_val.type.strip_typedefs().target().strip_typedefs()
|
||||
pointee = ptr_val.dereference()
|
||||
|
||||
if pointee_type.code in (gdb.TYPE_CODE_STRUCT, gdb.TYPE_CODE_UNION):
|
||||
if node_id in visited:
|
||||
return
|
||||
visited.add(node_id)
|
||||
pointee = ptr_val.dereference()
|
||||
_describe_struct(pointee, node_id, nodes, edges, visited)
|
||||
else:
|
||||
# Pointer to a scalar (e.g. int*): still worth a leaf node.
|
||||
if node_id in nodes:
|
||||
return # already described (richly or otherwise) via another path
|
||||
pointee = ptr_val.dereference()
|
||||
nodes[node_id] = [
|
||||
{"name": "value", "value": _format_scalar(pointee), "isPointer": False}
|
||||
]
|
||||
|
||||
|
||||
def build_graph_json(root_val, roots):
|
||||
nodes = {}
|
||||
edges = []
|
||||
visited = set()
|
||||
|
||||
def _walk_primary_root(root_val, nodes, edges, visited):
|
||||
"""Walks EXPR itself (the first argument to $debug_graph). Returns the
|
||||
address string of EXPR's own node, if any, so callers can skip re-adding
|
||||
it as a redundant self-pointing root when auto-discovery finds the same
|
||||
variable again as a local."""
|
||||
t = root_val.type.strip_typedefs()
|
||||
if t.code == gdb.TYPE_CODE_PTR:
|
||||
ptr_int = int(root_val)
|
||||
if ptr_int:
|
||||
_visit_pointer(root_val, ptr_int, nodes, edges, visited)
|
||||
return _addr_str(ptr_int)
|
||||
return None
|
||||
elif t.code in (gdb.TYPE_CODE_STRUCT, gdb.TYPE_CODE_UNION):
|
||||
node_id = _addr_str(int(root_val.address))
|
||||
visited.add(node_id)
|
||||
_describe_struct(root_val, node_id, nodes, edges, visited)
|
||||
return node_id
|
||||
else:
|
||||
nodes["root"] = [
|
||||
{"name": "value", "value": _format_scalar(root_val), "isPointer": False}
|
||||
]
|
||||
return None
|
||||
|
||||
|
||||
def _add_named_root(name, val, kind, nodes, edges, visited, roots_out, skip_addrs):
|
||||
"""Walks `val` (a local/global/watched variable) same as a primary root,
|
||||
then records it as a (name, value, kind) root entry -- unless its
|
||||
address is in skip_addrs (already covered by the primary root)."""
|
||||
t = val.type.strip_typedefs()
|
||||
if t.code == gdb.TYPE_CODE_PTR:
|
||||
try:
|
||||
ptr_int = int(val)
|
||||
except gdb.error:
|
||||
return
|
||||
if not ptr_int:
|
||||
return
|
||||
addr = _addr_str(ptr_int)
|
||||
if addr in skip_addrs:
|
||||
return
|
||||
_visit_pointer(val, ptr_int, nodes, edges, visited)
|
||||
roots_out.append((name, addr, kind))
|
||||
elif t.code in (gdb.TYPE_CODE_STRUCT, gdb.TYPE_CODE_UNION):
|
||||
try:
|
||||
addr = _addr_str(int(val.address))
|
||||
except gdb.error:
|
||||
return
|
||||
if addr in skip_addrs:
|
||||
return
|
||||
if addr not in visited:
|
||||
visited.add(addr)
|
||||
_describe_struct(val, addr, nodes, edges, visited)
|
||||
roots_out.append((name, addr, kind))
|
||||
else:
|
||||
roots_out.append((name, _format_scalar(val), kind))
|
||||
|
||||
|
||||
def _frame_local_symbols(frame):
|
||||
"""Yields every local variable/argument gdb.Symbol visible at the
|
||||
current PC, walking from the innermost lexical block out to (but not
|
||||
including) the enclosing file/global scope."""
|
||||
seen = set()
|
||||
block = frame.block()
|
||||
while block is not None and not block.is_global and not block.is_static:
|
||||
for sym in block:
|
||||
if not (sym.is_variable or sym.is_argument):
|
||||
continue
|
||||
if sym.name is None or sym.name in seen:
|
||||
continue
|
||||
seen.add(sym.name)
|
||||
yield sym
|
||||
block = block.superblock
|
||||
|
||||
|
||||
def _file_global_symbols(frame):
|
||||
"""Yields every file-scope global/static gdb.Symbol visible from the
|
||||
current frame's source file."""
|
||||
seen = set()
|
||||
try:
|
||||
symtab = frame.find_sal().symtab
|
||||
except gdb.error:
|
||||
return
|
||||
if symtab is None:
|
||||
return
|
||||
for block in (symtab.global_block(), symtab.static_block()):
|
||||
if block is None:
|
||||
continue
|
||||
for sym in block:
|
||||
if not sym.is_variable:
|
||||
continue
|
||||
if sym.name is None or sym.name in seen:
|
||||
continue
|
||||
seen.add(sym.name)
|
||||
yield sym
|
||||
|
||||
|
||||
def build_graph_json(root_val, watched_roots):
|
||||
nodes = {}
|
||||
edges = []
|
||||
visited = set()
|
||||
roots = []
|
||||
skip_addrs = set()
|
||||
used_names = set()
|
||||
|
||||
primary_addr = _walk_primary_root(root_val, nodes, edges, visited)
|
||||
if primary_addr is not None:
|
||||
skip_addrs.add(primary_addr)
|
||||
|
||||
for name, ptr_val in watched_roots:
|
||||
used_names.add(name)
|
||||
try:
|
||||
ptr_int = int(ptr_val)
|
||||
except gdb.error:
|
||||
ptr_int = 0
|
||||
addr = _addr_str(ptr_int)
|
||||
roots.append((name, addr, "watched"))
|
||||
if ptr_int and addr not in skip_addrs:
|
||||
_visit_pointer(ptr_val, ptr_int, nodes, edges, visited)
|
||||
|
||||
try:
|
||||
frame = gdb.selected_frame()
|
||||
except gdb.error:
|
||||
frame = None
|
||||
|
||||
if frame is not None:
|
||||
for sym in _frame_local_symbols(frame):
|
||||
if sym.name in used_names:
|
||||
continue
|
||||
used_names.add(sym.name)
|
||||
try:
|
||||
val = sym.value(frame)
|
||||
except gdb.error:
|
||||
continue
|
||||
_add_named_root(sym.name, val, "local", nodes, edges, visited, roots, skip_addrs)
|
||||
|
||||
for sym in _file_global_symbols(frame):
|
||||
if sym.name in used_names:
|
||||
continue
|
||||
used_names.add(sym.name)
|
||||
try:
|
||||
val = sym.value(frame)
|
||||
except gdb.error:
|
||||
continue
|
||||
_add_named_root(sym.name, val, "global", nodes, edges, visited, roots, skip_addrs)
|
||||
|
||||
nodes_json = [{"id": nid, "fields": fields} for nid, fields in nodes.items()]
|
||||
edges_json = [
|
||||
@@ -177,7 +329,7 @@ def build_graph_json(root_val, roots):
|
||||
for (src, dst, label) in edges
|
||||
if dst in nodes
|
||||
]
|
||||
roots_json = [{"name": name, "value": value} for (name, value) in roots]
|
||||
roots_json = [{"name": name, "value": value, "kind": kind} for (name, value, kind) in roots]
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
@@ -260,7 +412,10 @@ gdb.pretty_printers.append(_pretty_print_lookup)
|
||||
class DebugGraphFunction(gdb.Function):
|
||||
"""$debug_graph(EXPR[, name, ptr]...) -- render EXPR's pointer-linked
|
||||
structure as Debug Visualizer JSON. Extra name/pointer argument pairs
|
||||
are drawn as external marker pointers into the graph."""
|
||||
are drawn as external "watched" marker pointers into the graph; every
|
||||
other local (and file-scope global) visible at the current PC is also
|
||||
auto-walked and added as a "local"/"global" root -- see module
|
||||
docstring for why."""
|
||||
|
||||
def __init__(self):
|
||||
super(DebugGraphFunction, self).__init__("debug_graph")
|
||||
@@ -271,7 +426,7 @@ class DebugGraphFunction(gdb.Function):
|
||||
|
||||
root = args[0]
|
||||
extra = args[1:]
|
||||
roots = []
|
||||
watched_roots = []
|
||||
i = 0
|
||||
while i + 1 < len(extra):
|
||||
name_val, ptr_val = extra[i], extra[i + 1]
|
||||
@@ -279,14 +434,10 @@ class DebugGraphFunction(gdb.Function):
|
||||
name = name_val.string()
|
||||
except (gdb.error, TypeError):
|
||||
name = str(name_val)
|
||||
try:
|
||||
ptr_int = int(ptr_val)
|
||||
except gdb.error:
|
||||
ptr_int = 0
|
||||
roots.append((name, _addr_str(ptr_int)))
|
||||
watched_roots.append((name, ptr_val))
|
||||
i += 2
|
||||
|
||||
return _as_debugger_string(build_graph_json(root, roots))
|
||||
return _as_debugger_string(build_graph_json(root, watched_roots))
|
||||
|
||||
|
||||
DebugGraphFunction()
|
||||
|
||||
Reference in New Issue
Block a user