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:

committed by
Chromium LUCI CQ

parent
809a881bda
commit
8a06ac5b73
@ -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.
|
||||
|
245
tools/lldb/chromium_visualizers.py
Normal file
245
tools/lldb/chromium_visualizers.py
Normal file
@ -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<.*>$"'
|
||||
)
|
Reference in New Issue
Block a user