[LLDB] Hide unresolvable children from ObjC tagged pointers (#211936)
Foundation's small value inline representations have valid summaries and sometimed synthetic children, but LLDB cannot access any ivars from their base classes (such as [NSObject isa]). Use a synthetic child provider to hide them. rdar://182434208 Assisted-by: claude
This commit is contained in:
parent
8a49f8839c
commit
4f02e1e45c
8 changed files with 231 additions and 0 deletions
|
|
@ -881,6 +881,45 @@ lldb::TypeCategoryImplSP ObjCLanguage::GetFormatters() {
|
|||
return g_category;
|
||||
}
|
||||
|
||||
HardcodedFormatters::HardcodedSyntheticFinder
|
||||
ObjCLanguage::GetHardcodedSynthetics() {
|
||||
static llvm::once_flag g_initialize;
|
||||
static HardcodedFormatters::HardcodedSyntheticFinder g_formatters;
|
||||
|
||||
llvm::call_once(g_initialize, []() -> void {
|
||||
// An Objective-C tagged pointer (e.g. a single-index NSIndexSet
|
||||
// or a small NSNumber) packs its whole state into the pointer
|
||||
// bits and has no object in memory. Since its declared base
|
||||
// classes and ivars cannot be read, this synthtic child provider
|
||||
// hides them. Other synthetic child providers take precedence of
|
||||
// this.
|
||||
g_formatters.push_back(
|
||||
[](lldb_private::ValueObject &valobj, lldb::DynamicValueType,
|
||||
FormatManager &) -> SyntheticChildren::SharedPointer {
|
||||
static CXXSyntheticChildren::SharedPointer formatter_sp(
|
||||
new CXXSyntheticChildren(
|
||||
SyntheticChildren::Flags()
|
||||
.SetCascades(true)
|
||||
.SetSkipPointers(false)
|
||||
.SetSkipReferences(false)
|
||||
.SetNonCacheable(true),
|
||||
"tagged pointer synthetic children",
|
||||
lldb_private::formatters::ObjCClassSyntheticFrontEndCreator));
|
||||
|
||||
ProcessSP process_sp = valobj.GetProcessSP();
|
||||
if (!process_sp)
|
||||
return nullptr;
|
||||
if (ObjCLanguageRuntime *runtime =
|
||||
ObjCLanguageRuntime::Get(*process_sp))
|
||||
if (runtime->IsTaggedPointerValue(valobj))
|
||||
return formatter_sp;
|
||||
return nullptr;
|
||||
});
|
||||
});
|
||||
|
||||
return g_formatters;
|
||||
}
|
||||
|
||||
std::vector<FormattersMatchCandidate>
|
||||
ObjCLanguage::GetPossibleFormattersMatches(ValueObject &valobj,
|
||||
lldb::DynamicValueType use_dynamic) {
|
||||
|
|
|
|||
|
|
@ -146,6 +146,9 @@ public:
|
|||
|
||||
lldb::TypeCategoryImplSP GetFormatters() override;
|
||||
|
||||
HardcodedFormatters::HardcodedSyntheticFinder
|
||||
GetHardcodedSynthetics() override;
|
||||
|
||||
std::vector<FormattersMatchCandidate>
|
||||
GetPossibleFormattersMatches(ValueObject &valobj,
|
||||
lldb::DynamicValueType use_dynamic) override;
|
||||
|
|
|
|||
|
|
@ -290,6 +290,23 @@ ObjCLanguageRuntime::GetClassDescriptor(ValueObject &valobj) {
|
|||
return objc_class_sp;
|
||||
}
|
||||
|
||||
bool ObjCLanguageRuntime::IsTaggedPointerValue(ValueObject &in_value) {
|
||||
TaggedPointerVendor *tagged_pointer_vendor = GetTaggedPointerVendor();
|
||||
if (!tagged_pointer_vendor)
|
||||
return false;
|
||||
|
||||
// Only Objective-C object values can be tagged pointers.
|
||||
if (!(in_value.GetTypeInfo() & lldb::eTypeIsObjC))
|
||||
return false;
|
||||
|
||||
addr_t ptr = in_value.IsPointerType() ? in_value.GetPointerValue().address
|
||||
: in_value.GetAddressOf().address;
|
||||
if (ptr == LLDB_INVALID_ADDRESS)
|
||||
return false;
|
||||
|
||||
return tagged_pointer_vendor->IsPossibleTaggedPointer(ptr);
|
||||
}
|
||||
|
||||
ObjCLanguageRuntime::ClassDescriptorSP
|
||||
ObjCLanguageRuntime::GetNonKVOClassDescriptor(ValueObject &valobj) {
|
||||
ObjCLanguageRuntime::ClassDescriptorSP objc_class_sp(
|
||||
|
|
|
|||
|
|
@ -233,6 +233,8 @@ public:
|
|||
|
||||
virtual TaggedPointerVendor *GetTaggedPointerVendor() { return nullptr; }
|
||||
|
||||
bool IsTaggedPointerValue(ValueObject &in_value);
|
||||
|
||||
typedef std::shared_ptr<EncodingToType> EncodingToTypeSP;
|
||||
|
||||
virtual EncodingToTypeSP GetEncodingToType();
|
||||
|
|
|
|||
4
lldb/test/API/lang/objc/tagged-pointer-children/Makefile
Normal file
4
lldb/test/API/lang/objc/tagged-pointer-children/Makefile
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
OBJC_SOURCES := main.m
|
||||
LD_EXTRAS := -lobjc -framework Foundation
|
||||
|
||||
include Makefile.rules
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
import lldb
|
||||
from lldbsuite.test.decorators import *
|
||||
from lldbsuite.test.lldbtest import *
|
||||
from lldbsuite.test import lldbutil
|
||||
|
||||
|
||||
class TaggedPointerChildrenTestCase(TestBase):
|
||||
def is_tagged(self, valobj):
|
||||
"""Return True if valobj holds an Objective-C tagged pointer."""
|
||||
res = lldb.SBCommandReturnObject()
|
||||
ci = self.dbg.GetCommandInterpreter()
|
||||
ci.HandleCommand(
|
||||
"language objc tagged-pointer info %s" % valobj.GetValue(), res
|
||||
)
|
||||
return res.Succeeded() and "is tagged" in res.GetOutput()
|
||||
|
||||
def assert_no_unreadable_children(self, valobj):
|
||||
"""Recursively assert no child of valobj fails with a memory-read error."""
|
||||
for i in range(valobj.GetNumChildren()):
|
||||
child = valobj.GetChildAtIndex(i)
|
||||
err = child.GetError()
|
||||
self.assertFalse(
|
||||
err.Fail() and "read memory" in (err.GetCString() or ""),
|
||||
"child '%s' of '%s' has a memory-read error: %s"
|
||||
% (child.GetName(), valobj.GetName(), err.GetCString()),
|
||||
)
|
||||
self.assert_no_unreadable_children(child)
|
||||
|
||||
@skipUnlessDarwin
|
||||
@skipIf(archs=["i386", "i686"])
|
||||
def test(self):
|
||||
"""
|
||||
Test that Objective-C tagged pointers (inline values) do not present phantom,
|
||||
memory-backed children.
|
||||
"""
|
||||
self.build()
|
||||
_, _, thread, _ = lldbutil.run_to_source_breakpoint(
|
||||
self, "// break here", lldb.SBFileSpec("main.m")
|
||||
)
|
||||
frame = thread.GetSelectedFrame()
|
||||
|
||||
tagged = frame.FindVariable("tagged")
|
||||
self.assertTrue(tagged.IsValid(), "found 'tagged'")
|
||||
|
||||
# If this platform does not represent a single-index NSIndexSet as a
|
||||
# tagged pointer there is nothing to test.
|
||||
if not self.is_tagged(tagged):
|
||||
self.skipTest("NSIndexSet is not a tagged pointer on this platform")
|
||||
|
||||
# The tagged pointer is still summarized.
|
||||
self.assertIsNotNone(tagged.GetSummary())
|
||||
self.assertIn("index", tagged.GetSummary())
|
||||
|
||||
# But it must not present any unresolvable children.
|
||||
self.assertEqual(
|
||||
tagged.GetNumChildren(),
|
||||
0,
|
||||
"tagged pointer must not have children",
|
||||
)
|
||||
self.assertFalse(tagged.MightHaveChildren())
|
||||
self.assert_no_unreadable_children(tagged)
|
||||
|
||||
# Sanity check.
|
||||
heap = frame.FindVariable("heap")
|
||||
self.assertTrue(heap.IsValid(), "found 'heap'")
|
||||
self.assertFalse(self.is_tagged(heap), "'heap' is a real object")
|
||||
self.assertGreater(heap.GetNumChildren(), 0, "real object still has children")
|
||||
self.assert_no_unreadable_children(heap)
|
||||
|
||||
@skipUnlessDarwin
|
||||
@skipIf(archs=["i386", "i686"])
|
||||
def test_registered_synthetic_takes_precedence(self):
|
||||
"""
|
||||
A synthetic child provider registered for the type must still fire for
|
||||
a tagged pointer. ObjCLanguage's hardcoded "no children" provider is
|
||||
only a fallback and must be consulted after any registered synthetic.
|
||||
"""
|
||||
self.build()
|
||||
_, _, thread, _ = lldbutil.run_to_source_breakpoint(
|
||||
self, "// break here", lldb.SBFileSpec("main.m")
|
||||
)
|
||||
frame = thread.GetSelectedFrame()
|
||||
|
||||
tagged = frame.FindVariable("tagged")
|
||||
self.assertTrue(tagged.IsValid(), "found 'tagged'")
|
||||
if not self.is_tagged(tagged):
|
||||
self.skipTest("NSIndexSet is not a tagged pointer on this platform")
|
||||
|
||||
# Without a registered synthetic, the hardcoded fallback reports no
|
||||
# children for the tagged pointer.
|
||||
self.assertEqual(tagged.GetNumChildren(), 0)
|
||||
|
||||
# Register a synthetic child provider for NSIndexSet.
|
||||
import os
|
||||
|
||||
provider = os.path.join(os.path.dirname(__file__), "tagged_synth_provider.py")
|
||||
self.runCmd("command script import " + provider)
|
||||
self.runCmd(
|
||||
'type synthetic add -x "NSIndexSet" '
|
||||
"-l tagged_synth_provider.TaggedSyntheticProvider"
|
||||
)
|
||||
self.addTearDownHook(lambda: self.runCmd("type synthetic clear", check=False))
|
||||
|
||||
# The registered provider must now win over the hardcoded fallback.
|
||||
tagged = frame.FindVariable("tagged")
|
||||
self.assertEqual(
|
||||
tagged.GetNumChildren(),
|
||||
1,
|
||||
"registered synthetic must take precedence over the hardcoded "
|
||||
"tagged-pointer fallback",
|
||||
)
|
||||
self.assertTrue(tagged.MightHaveChildren())
|
||||
child = tagged.GetChildAtIndex(0)
|
||||
self.assertEqual(child.GetName(), "synthetic_child")
|
||||
self.assertEqual(child.GetValueAsSigned(), 42)
|
||||
19
lldb/test/API/lang/objc/tagged-pointer-children/main.m
Normal file
19
lldb/test/API/lang/objc/tagged-pointer-children/main.m
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
#import <Foundation/Foundation.h>
|
||||
|
||||
int main(int argc, const char *argv[]) {
|
||||
@autoreleasepool {
|
||||
// A single-index NSIndexSet is stored as an Objective-C tagged pointer.
|
||||
// It has a summary but no ivars or base classes LLDB could materialize
|
||||
// from memory.
|
||||
NSIndexSet *tagged = [NSIndexSet indexSetWithIndex:1];
|
||||
|
||||
// A discontiguous set cannot be tagged, so it is a real heap object with a
|
||||
// readable isa and ivar layout.
|
||||
NSMutableIndexSet *heap = [NSMutableIndexSet indexSet];
|
||||
[heap addIndex:1];
|
||||
[heap addIndex:1000000];
|
||||
|
||||
NSLog(@"%@ %@", tagged, heap); // break here
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import lldb
|
||||
|
||||
|
||||
class TaggedSyntheticProvider:
|
||||
"""A custom synthetic child provider that produces a single child that
|
||||
does not depend on reading the tagged pointer's (nonexistent) backing
|
||||
memory, so it works for tagged pointers.
|
||||
"""
|
||||
|
||||
def __init__(self, valobj, internal_dict):
|
||||
self.valobj = valobj
|
||||
|
||||
def num_children(self):
|
||||
return 1
|
||||
|
||||
def get_child_index(self, name):
|
||||
return 0 if name == "synthetic_child" else -1
|
||||
|
||||
def get_child_at_index(self, index):
|
||||
if index != 0:
|
||||
return lldb.SBValue()
|
||||
# Build the child from raw data rather than by reading the object, so it
|
||||
# works for a tagged pointer (which has no backing memory).
|
||||
target = self.valobj.GetTarget()
|
||||
int_type = target.GetBasicType(lldb.eBasicTypeInt)
|
||||
data = lldb.SBData.CreateDataFromSInt32Array(
|
||||
target.GetByteOrder(), target.GetAddressByteSize(), [42]
|
||||
)
|
||||
return self.valobj.CreateValueFromData("synthetic_child", data, int_type)
|
||||
|
||||
def update(self):
|
||||
return False
|
||||
Loading…
Reference in a new issue