0

Visualizers for lldb

Adds a script to create lldb visualizers for common string,
vector, and pointer types, so that elements can be expanded
when debugging in vscode.

Bug: None
Change-Id: If410e9f34277ca0f6d2f277228a1f067544cb0d8
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6330096
Commit-Queue: Dirk Pranke <dpranke@google.com>
Reviewed-by: Nico Weber <thakis@chromium.org>
Auto-Submit: Eric Leese <leese@chromium.org>
Reviewed-by: Dirk Pranke <dpranke@google.com>
Cr-Commit-Position: refs/heads/main@{#1431627}
This commit is contained in:
Eric Leese
2025-03-12 11:02:15 -07:00
committed by Chromium LUCI CQ
parent 809a881bda
commit 8a06ac5b73
2 changed files with 251 additions and 0 deletions

@ -16,6 +16,12 @@ script import lldbinit
Make sure the build configurations include `is_debug=true`, this will set `symbol_level=2` by default, which is required if need to view the content of frame-level local variables.
If you want visualizer support for common pointer, string, and vector types in the Chromium codebase, you can also add the following:
```
script import chromium_visualizers
```
## How to attach to a process with lldb and start debugging
- Follow the instructions above to create your `~/.lldbinit` file, don't forget to put the correct path to Chromium source in there.

@ -0,0 +1,245 @@
# Copyright 2025 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""
Provides visualizers for common types for debugging in LLDB.
To make these available, add the following to your ~/.lldbinit or your
.vscode/launch.json, or run after launching lldb:
command script import {Path to SRC Root}/tools/lldb/chromium_visualizers.py
"""
import traceback
import lldb
def lazy_unsigned_global(name, child=None):
sb_value = None
uint_value = 0
def getter(fromValue):
nonlocal sb_value
nonlocal uint_value
if sb_value is None:
sb_value = fromValue.GetTarget().FindFirstGlobalVariable(name)
if child:
sb_value = sb_value.GetChildMemberWithName(child)
uint_value = sb_value.GetValueAsUnsigned()
return uint_value
return getter
kPointerCompressionShift_getter = lazy_unsigned_global(
'cppgc::internal::api_constants::kPointerCompressionShift')
CageBaseGlobal_getter = lazy_unsigned_global(
'cppgc::internal::CageBaseGlobal::g_base_', 'base')
PLACEHOLDER_VALUE = None
class SingleChildProvider:
"""A base class for providers that create one child."""
def __init__(self, valobj):
self.child = None
self.valobj = valobj
def update(self):
# By default, reevaluate
self.child = None
def ensure_populated(self):
if self.child is None:
try:
self.populate()
except:
print(traceback.format_exc())
if self.child is None or not self.child.IsValid():
global PLACEHOLDER_VALUE
if PLACEHOLDER_VALUE is None:
PLACEHOLDER_VALUE = self.valobj.CreateValueFromExpression(
'<failed to load child>', 'nullptr')
self.child = PLACEHOLDER_VALUE
def populate(self):
raise NotImplementedError()
def num_children(self):
self.ensure_populated()
return 1
def has_children(self):
return True
def get_child_index(self, name):
self.ensure_populated()
return 0
def get_child_at_index(self, index):
self.ensure_populated()
return self.child
class VectorProvider(SingleChildProvider):
"""Provides children for a vector."""
def __init__(self, valobj, internal_dict):
super().__init__(valobj)
def populate(self):
lldbtype = self.valobj.GetType()
addr = self.valobj.GetValueAsAddress() if lldbtype.IsPointerType(
) else self.valobj.GetLoadAddress()
data_pointer = self.valobj.GetChildMemberWithName('buffer_')
size = self.valobj.GetChildMemberWithName('size_').GetValueAsUnsigned()
self.child = data_pointer.Cast(
data_pointer.GetType().GetPointeeType().GetArrayType(
size).GetPointerType())
class SmartPointerProvider(SingleChildProvider):
"""
A base class for providers that create one child generated by calling a
method on the object.
"""
def __init__(self,
raw_name,
valobj,
internal_dict,
child_name='$$dereference$$'):
super().__init__(valobj)
self.raw_name = raw_name
self.child_name = child_name
def update(self):
global PLACEHOLDER_VALUE
if self.child is PLACEHOLDER_VALUE:
self.child = None
def populate(self):
self.child = self.valobj.GetChildMemberWithName(self.raw_name).Cast(
self.valobj.GetType().GetTemplateArgumentType(0).GetPointerType())
class ScopedRefProvider(SmartPointerProvider):
def __init__(self, valobj, internal_dict):
super().__init__('ptr_', valobj, internal_dict)
class MemberProvider(SingleChildProvider):
def __init__(self, valobj, internal_dict):
super().__init__(valobj)
self.last_child = None
self.last_addr = None
self.raw_storage = None
self.pointer_type = None
self.compressed = None
def update(self):
global PLACEHOLDER_VALUE
if self.compressed is not None or self.child is PLACEHOLDER_VALUE:
self.child = None
def populate(self):
if self.raw_storage is None:
self.raw_storage = self.valobj.GetChildMemberWithName('raw_')
self.pointer_type = self.raw_storage.GetType().GetCanonicalType().GetName(
)
pointee_type = self.valobj.GetType().GetTemplateArgumentType(0)
if self.pointer_type == 'cppgc::internal::RawPointer':
data_pointer = self.raw_storage.GetChildMemberWithName('ptr_').Cast(
pointee_type.GetPointerType())
elif self.pointer_type == 'cppgc::internal::CompressedPointer':
# Need to reproduce the behavior of CompressedPointer::Decompress()
# because it is optimized away.
if self.compressed is None:
self.compressed = self.raw_storage.GetChildMemberWithName('value_')
compressed = self.compressed.GetValueAsUnsigned()
if self.last_child and self.last_addr == compressed:
# Last child is still valid
self.child = self.last_child
return
pointer_shift = kPointerCompressionShift_getter(self.raw_storage)
cage_base = CageBaseGlobal_getter(self.raw_storage)
sign_bit = 0x80000000
decompressed = ((
(compressed ^ sign_bit) - sign_bit) << pointer_shift) & cage_base
data_pointer = self.valobj.CreateValueFromAddress('$$dereference$$',
decompressed,
pointee_type)
self.last_addr = compressed
else:
# This seems to happen when the pointer is null. Ignore.
return
self.child = data_pointer
self.last_child = self.child
class MethodProvider(SingleChildProvider):
"""
A base class for providers that create one child generated by calling a
method on the object.
"""
def __init__(self,
method,
valobj,
internal_dict,
child_name='$$dereference$$'):
super().__init__(valobj)
self.method = method
self.child_name = child_name
self.last_child = None
self.last_addr = None
def populate(self):
lldbtype = self.valobj.GetType()
addr = self.valobj.GetValueAsAddress() if lldbtype.IsPointerType(
) else self.valobj.GetLoadAddress()
if self.last_child and self.last_addr == addr:
# Last child is still valid
self.child = self.last_child
return
self.child = self.valobj.CreateValueFromExpression(
self.child_name,
f'(({lldbtype.GetCanonicalType().GetName()} *){addr})->{self.method}')
self.last_child = self.child
self.last_addr = addr
class WTFStringProvider(MethodProvider):
def __init__(self, valobj, internal_dict):
super().__init__('Utf8(WTF::Utf8ConversionMode::kLenient)', valobj,
internal_dict, 'utf8_')
def __lldb_init_module(debugger, unused_dict):
debugger.HandleCommand(
'type synthetic add -l chromium_visualizers.ScopedRefProvider -x "^scoped_refptr<.*>$"'
)
debugger.HandleCommand(
'type synthetic add -p -r -l chromium_visualizers.MemberProvider -x "^cppgc::internal::BasicMember<.*>$"'
)
debugger.HandleCommand(
'type synthetic add -l chromium_visualizers.WTFStringProvider -x "^WTF::String$"'
)
debugger.HandleCommand(
'type synthetic add -l chromium_visualizers.VectorProvider -x "^WTF::Vector<.*>$"'
)
debugger.HandleCommand(
'type summary add --summary-string "${svar.utf8_}" WTF::String')
debugger.HandleCommand(
'type summary add --summary-string "${var.string_}" WTF::AtomicString')
debugger.HandleCommand(
'type summary add --summary-string "size = ${var.size_}" -x "^WTF::Vector<.*>$"'
)
debugger.HandleCommand(
'type summary add --summary-string "${var.raw_}" -x "^cppgc::internal::BasicMember<.*>$"'
)