[lldb] Upstream and adopt ReadUnsignedIntegersFromMemory in AppleObjCRuntimeV2 (#190564)

This PR upstreams ReadUnsignedIntegersFromMemory, which uses the
MultiMemRead packet to speed up reading unsigned numbers from memory.
Felipe landed this on Github because we didn't have a use-case for it
upstream, until now, which uses it to batch up reading ObjC runtime
symbols.
This commit is contained in:
Jonas Devlieghere
2026-04-08 14:15:35 +01:00
committed by GitHub
parent 1f1ea1ae41
commit de0cb1cd04
4 changed files with 343 additions and 112 deletions

View File

@@ -1695,11 +1695,22 @@ public:
size_t byte_size, uint64_t fail_value,
Status &error);
/// Use Process::ReadMemoryRanges to efficiently read multiple unsigned
/// integers from memory at once.
llvm::SmallVector<std::optional<uint64_t>>
ReadUnsignedIntegersFromMemory(llvm::ArrayRef<lldb::addr_t> addresses,
unsigned byte_size);
int64_t ReadSignedIntegerFromMemory(lldb::addr_t load_addr, size_t byte_size,
int64_t fail_value, Status &error);
lldb::addr_t ReadPointerFromMemory(lldb::addr_t vm_addr, Status &error);
/// Use Process::ReadMemoryRanges to efficiently read multiple pointers from
/// memory at once.
llvm::SmallVector<std::optional<lldb::addr_t>>
ReadPointersFromMemory(llvm::ArrayRef<lldb::addr_t> ptr_locs);
bool WritePointerToMemory(lldb::addr_t vm_addr, lldb::addr_t ptr_value,
Status &error);

View File

@@ -59,6 +59,7 @@
#include "clang/AST/ASTContext.h"
#include "clang/AST/DeclObjC.h"
#include "clang/Basic/TargetInfo.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/ADT/ScopeExit.h"
#include <cstdint>
@@ -69,6 +70,23 @@
using namespace lldb;
using namespace lldb_private;
namespace {
struct RuntimeGlobalSymbolSpec {
ConstString name;
/// Whether to only return the address or also the value.
bool read_value = true;
/// A byte size of 0 means use the process pointer size.
uint8_t byte_size = 0;
};
struct RuntimeGlobalSymbolResult {
uint64_t value = LLDB_INVALID_ADDRESS;
bool success = false;
};
} // namespace
char AppleObjCRuntimeV2::ID = 0;
static const char *g_get_dynamic_class_info_name =
@@ -759,6 +777,75 @@ ExtractRuntimeGlobalSymbol(Process *process, ConstString name,
return symbol_load_addr;
}
// Batched version of ExtractRuntimeGlobalSymbol. Resolves symbols and reads
// their values in a single batch using ReadUnsignedIntegersFromMemory.
static llvm::SmallVector<RuntimeGlobalSymbolResult>
ExtractRuntimeGlobalSymbolsBatched(
Process *process, const ModuleSP &module_sp,
llvm::ArrayRef<RuntimeGlobalSymbolSpec> specs) {
// Start out with all results in a failed state.
llvm::SmallVector<RuntimeGlobalSymbolResult> results(specs.size());
if (!process || !module_sp)
return results;
const uint8_t ptr_size = process->GetAddressByteSize();
// Phase 1: Resolve all symbols to addresses. Build a work list of entries
// that need their values read in Phase 2.
struct ReadEntry {
size_t result_idx;
lldb::addr_t addr;
uint8_t byte_size;
};
llvm::SmallVector<ReadEntry> work_list;
for (auto [i, spec] : llvm::enumerate(specs)) {
const uint8_t size = spec.byte_size ? spec.byte_size : ptr_size;
const Symbol *symbol = module_sp->FindFirstSymbolWithNameAndType(
spec.name, lldb::eSymbolTypeData);
if (!symbol || !symbol->ValueIsAddress())
continue;
lldb::addr_t symbol_load_addr =
symbol->GetAddressRef().GetLoadAddress(&process->GetTarget());
if (symbol_load_addr == LLDB_INVALID_ADDRESS)
continue;
if (!spec.read_value)
results[i] = {symbol_load_addr, true};
else
work_list.push_back({i, symbol_load_addr, size});
}
// Phase 2: Batch read values, grouping consecutive entries with the same
// byte size into a single ReadUnsignedIntegersFromMemory call.
llvm::stable_sort(work_list, [](const ReadEntry &a, const ReadEntry &b) {
return a.byte_size < b.byte_size;
});
for (size_t i = 0; i < work_list.size();) {
const uint8_t byte_size = work_list[i].byte_size;
const size_t group_start = i;
llvm::SmallVector<lldb::addr_t> addrs;
while (i < work_list.size() && work_list[i].byte_size == byte_size)
addrs.push_back(work_list[i++].addr);
auto read_values =
process->ReadUnsignedIntegersFromMemory(addrs, byte_size);
for (size_t j = 0; j < addrs.size(); ++j) {
size_t idx = work_list[group_start + j].result_idx;
if (read_values[j].has_value())
results[idx] = {*read_values[j], true};
}
}
return results;
}
static void RegisterObjCExceptionRecognizer(Process *process);
AppleObjCRuntimeV2::AppleObjCRuntimeV2(Process *process,
@@ -2835,56 +2922,61 @@ AppleObjCRuntimeV2::NonPointerISACache::CreateInstance(
AppleObjCRuntimeV2 &runtime, const lldb::ModuleSP &objc_module_sp) {
Process *process(runtime.GetProcess());
Status error;
Log *log = GetLog(LLDBLog::Types);
auto objc_debug_isa_magic_mask = ExtractRuntimeGlobalSymbol(
process, ConstString("objc_debug_isa_magic_mask"), objc_module_sp, error);
if (error.Fail())
// Batch read all the ISA-related symbols in one go.
enum ISASymbol {
kMagicMask,
kMagicValue,
kClassMask,
kIndexedMagicMask,
kIndexedMagicValue,
kIndexedIndexMask,
kIndexedIndexShift,
kIndexedClasses,
kISASymbolCount,
};
llvm::SmallVector<RuntimeGlobalSymbolSpec> specs = {
{ConstString("objc_debug_isa_magic_mask")},
{ConstString("objc_debug_isa_magic_value")},
{ConstString("objc_debug_isa_class_mask")},
{ConstString("objc_debug_indexed_isa_magic_mask")},
{ConstString("objc_debug_indexed_isa_magic_value")},
{ConstString("objc_debug_indexed_isa_index_mask")},
{ConstString("objc_debug_indexed_isa_index_shift")},
{ConstString("objc_indexed_classes"), /*read_value=*/false},
};
assert(specs.size() == kISASymbolCount);
auto results =
ExtractRuntimeGlobalSymbolsBatched(process, objc_module_sp, specs);
// Check required symbols.
if (!results[kMagicMask].success || !results[kMagicValue].success ||
!results[kClassMask].success)
return nullptr;
auto objc_debug_isa_magic_value = ExtractRuntimeGlobalSymbol(
process, ConstString("objc_debug_isa_magic_value"), objc_module_sp,
error);
if (error.Fail())
return nullptr;
auto objc_debug_isa_class_mask = ExtractRuntimeGlobalSymbol(
process, ConstString("objc_debug_isa_class_mask"), objc_module_sp, error);
if (error.Fail())
return nullptr;
auto objc_debug_isa_magic_mask = results[kMagicMask].value;
auto objc_debug_isa_magic_value = results[kMagicValue].value;
auto objc_debug_isa_class_mask = results[kClassMask].value;
if (log)
log->PutCString("AOCRT::NPI: Found all the non-indexed ISA masks");
bool foundError = false;
auto objc_debug_indexed_isa_magic_mask = ExtractRuntimeGlobalSymbol(
process, ConstString("objc_debug_indexed_isa_magic_mask"), objc_module_sp,
error);
foundError |= error.Fail();
// Check optional indexed ISA symbols.
bool foundError = !results[kIndexedMagicMask].success ||
!results[kIndexedMagicValue].success ||
!results[kIndexedIndexMask].success ||
!results[kIndexedIndexShift].success ||
!results[kIndexedClasses].success;
auto objc_debug_indexed_isa_magic_value = ExtractRuntimeGlobalSymbol(
process, ConstString("objc_debug_indexed_isa_magic_value"),
objc_module_sp, error);
foundError |= error.Fail();
auto objc_debug_indexed_isa_magic_mask = results[kIndexedMagicMask].value;
auto objc_debug_indexed_isa_magic_value = results[kIndexedMagicValue].value;
auto objc_debug_indexed_isa_index_mask = results[kIndexedIndexMask].value;
auto objc_debug_indexed_isa_index_shift = results[kIndexedIndexShift].value;
auto objc_indexed_classes = results[kIndexedClasses].value;
auto objc_debug_indexed_isa_index_mask = ExtractRuntimeGlobalSymbol(
process, ConstString("objc_debug_indexed_isa_index_mask"), objc_module_sp,
error);
foundError |= error.Fail();
auto objc_debug_indexed_isa_index_shift = ExtractRuntimeGlobalSymbol(
process, ConstString("objc_debug_indexed_isa_index_shift"),
objc_module_sp, error);
foundError |= error.Fail();
auto objc_indexed_classes =
ExtractRuntimeGlobalSymbol(process, ConstString("objc_indexed_classes"),
objc_module_sp, error, false);
foundError |= error.Fail();
if (log)
if (log && !foundError)
log->PutCString("AOCRT::NPI: Found all the indexed ISA masks");
// we might want to have some rules to outlaw these other values (e.g if the
@@ -2903,84 +2995,72 @@ AppleObjCRuntimeV2::TaggedPointerVendorV2::CreateInstance(
AppleObjCRuntimeV2 &runtime, const lldb::ModuleSP &objc_module_sp) {
Process *process(runtime.GetProcess());
Status error;
// Batch read all tagged pointer symbols in one go.
enum TaggedPtrSymbol {
kMask,
kSlotShift,
kSlotMask,
kPayloadLshift,
kPayloadRshift,
kClasses,
kExtMask,
kExtSlotShift,
kExtSlotMask,
kExtClasses,
kExtPayloadLshift,
kExtPayloadRshift,
kTaggedPtrSymbolCount,
};
llvm::SmallVector<RuntimeGlobalSymbolSpec> specs = {
{ConstString("objc_debug_taggedpointer_mask")},
{ConstString("objc_debug_taggedpointer_slot_shift"), true, 4},
{ConstString("objc_debug_taggedpointer_slot_mask"), true, 4},
{ConstString("objc_debug_taggedpointer_payload_lshift"), true, 4},
{ConstString("objc_debug_taggedpointer_payload_rshift"), true, 4},
{ConstString("objc_debug_taggedpointer_classes"), false},
{ConstString("objc_debug_taggedpointer_ext_mask")},
{ConstString("objc_debug_taggedpointer_ext_slot_shift"), true, 4},
{ConstString("objc_debug_taggedpointer_ext_slot_mask"), true, 4},
{ConstString("objc_debug_taggedpointer_ext_classes"), false},
{ConstString("objc_debug_taggedpointer_ext_payload_lshift"), true, 4},
{ConstString("objc_debug_taggedpointer_ext_payload_rshift"), true, 4},
};
assert(specs.size() == kTaggedPtrSymbolCount);
auto objc_debug_taggedpointer_mask = ExtractRuntimeGlobalSymbol(
process, ConstString("objc_debug_taggedpointer_mask"), objc_module_sp,
error);
if (error.Fail())
auto results =
ExtractRuntimeGlobalSymbolsBatched(process, objc_module_sp, specs);
// Check required symbols.
bool required_success =
results[kMask].success && results[kSlotShift].success &&
results[kSlotMask].success && results[kPayloadLshift].success &&
results[kPayloadRshift].success && results[kClasses].success;
if (!required_success)
return new TaggedPointerVendorLegacy(runtime);
auto objc_debug_taggedpointer_slot_shift = ExtractRuntimeGlobalSymbol(
process, ConstString("objc_debug_taggedpointer_slot_shift"),
objc_module_sp, error, true, 4);
if (error.Fail())
return new TaggedPointerVendorLegacy(runtime);
auto objc_debug_taggedpointer_mask = results[kMask].value;
auto objc_debug_taggedpointer_slot_shift = results[kSlotShift].value;
auto objc_debug_taggedpointer_slot_mask = results[kSlotMask].value;
auto objc_debug_taggedpointer_payload_lshift = results[kPayloadLshift].value;
auto objc_debug_taggedpointer_payload_rshift = results[kPayloadRshift].value;
auto objc_debug_taggedpointer_classes = results[kClasses].value;
auto objc_debug_taggedpointer_slot_mask = ExtractRuntimeGlobalSymbol(
process, ConstString("objc_debug_taggedpointer_slot_mask"),
objc_module_sp, error, true, 4);
if (error.Fail())
return new TaggedPointerVendorLegacy(runtime);
auto objc_debug_taggedpointer_payload_lshift = ExtractRuntimeGlobalSymbol(
process, ConstString("objc_debug_taggedpointer_payload_lshift"),
objc_module_sp, error, true, 4);
if (error.Fail())
return new TaggedPointerVendorLegacy(runtime);
auto objc_debug_taggedpointer_payload_rshift = ExtractRuntimeGlobalSymbol(
process, ConstString("objc_debug_taggedpointer_payload_rshift"),
objc_module_sp, error, true, 4);
if (error.Fail())
return new TaggedPointerVendorLegacy(runtime);
auto objc_debug_taggedpointer_classes = ExtractRuntimeGlobalSymbol(
process, ConstString("objc_debug_taggedpointer_classes"), objc_module_sp,
error, false);
if (error.Fail())
return new TaggedPointerVendorLegacy(runtime);
// try to detect the "extended tagged pointer" variables - if any are
// missing, use the non-extended vendor
do {
auto objc_debug_taggedpointer_ext_mask = ExtractRuntimeGlobalSymbol(
process, ConstString("objc_debug_taggedpointer_ext_mask"),
objc_module_sp, error);
if (error.Fail())
break;
auto objc_debug_taggedpointer_ext_slot_shift = ExtractRuntimeGlobalSymbol(
process, ConstString("objc_debug_taggedpointer_ext_slot_shift"),
objc_module_sp, error, true, 4);
if (error.Fail())
break;
auto objc_debug_taggedpointer_ext_slot_mask = ExtractRuntimeGlobalSymbol(
process, ConstString("objc_debug_taggedpointer_ext_slot_mask"),
objc_module_sp, error, true, 4);
if (error.Fail())
break;
auto objc_debug_taggedpointer_ext_classes = ExtractRuntimeGlobalSymbol(
process, ConstString("objc_debug_taggedpointer_ext_classes"),
objc_module_sp, error, false);
if (error.Fail())
break;
// Check if extended symbols are all present.
bool extended_success =
results[kExtMask].success && results[kExtSlotShift].success &&
results[kExtSlotMask].success && results[kExtClasses].success &&
results[kExtPayloadLshift].success && results[kExtPayloadRshift].success;
if (extended_success) {
auto objc_debug_taggedpointer_ext_mask = results[kExtMask].value;
auto objc_debug_taggedpointer_ext_slot_shift = results[kExtSlotShift].value;
auto objc_debug_taggedpointer_ext_slot_mask = results[kExtSlotMask].value;
auto objc_debug_taggedpointer_ext_classes = results[kExtClasses].value;
auto objc_debug_taggedpointer_ext_payload_lshift =
ExtractRuntimeGlobalSymbol(
process, ConstString("objc_debug_taggedpointer_ext_payload_lshift"),
objc_module_sp, error, true, 4);
if (error.Fail())
break;
results[kExtPayloadLshift].value;
auto objc_debug_taggedpointer_ext_payload_rshift =
ExtractRuntimeGlobalSymbol(
process, ConstString("objc_debug_taggedpointer_ext_payload_rshift"),
objc_module_sp, error, true, 4);
if (error.Fail())
break;
results[kExtPayloadRshift].value;
return new TaggedPointerVendorExtended(
runtime, objc_debug_taggedpointer_mask,
@@ -2993,7 +3073,7 @@ AppleObjCRuntimeV2::TaggedPointerVendorV2::CreateInstance(
objc_debug_taggedpointer_ext_payload_lshift,
objc_debug_taggedpointer_ext_payload_rshift,
objc_debug_taggedpointer_classes, objc_debug_taggedpointer_ext_classes);
} while (false);
}
// we might want to have some rules to outlaw these values (e.g if the
// table's address is zero)

View File

@@ -2317,6 +2317,45 @@ uint64_t Process::ReadUnsignedIntegerFromMemory(lldb::addr_t vm_addr,
return fail_value;
}
llvm::SmallVector<std::optional<uint64_t>>
Process::ReadUnsignedIntegersFromMemory(llvm::ArrayRef<addr_t> addresses,
unsigned integer_byte_size) {
if (addresses.empty())
return {};
// Like ReadUnsignedIntegerFromMemory, this only supports a handful
// of widths.
if (!llvm::is_contained({1u, 2u, 4u, 8u}, integer_byte_size))
return llvm::SmallVector<std::optional<uint64_t>>(addresses.size(),
std::nullopt);
llvm::SmallVector<Range<addr_t, size_t>> ranges{
llvm::map_range(addresses, [=](addr_t ptr) {
return Range<addr_t, size_t>(ptr, integer_byte_size);
})};
std::vector<uint8_t> buffer(integer_byte_size * addresses.size(), 0);
llvm::SmallVector<llvm::MutableArrayRef<uint8_t>> memory =
ReadMemoryRanges(ranges, buffer);
llvm::SmallVector<std::optional<uint64_t>> result;
result.reserve(addresses.size());
const uint32_t addr_size = GetAddressByteSize();
const ByteOrder byte_order = GetByteOrder();
for (llvm::MutableArrayRef<uint8_t> range : memory) {
if (range.size() != integer_byte_size) {
result.push_back(std::nullopt);
continue;
}
DataExtractor data(range.data(), integer_byte_size, byte_order, addr_size);
offset_t offset = 0;
result.push_back(data.GetMaxU64(&offset, integer_byte_size));
assert(offset == integer_byte_size);
}
return result;
}
int64_t Process::ReadSignedIntegerFromMemory(lldb::addr_t vm_addr,
size_t integer_byte_size,
int64_t fail_value,
@@ -2336,6 +2375,12 @@ addr_t Process::ReadPointerFromMemory(lldb::addr_t vm_addr, Status &error) {
return LLDB_INVALID_ADDRESS;
}
llvm::SmallVector<std::optional<addr_t>>
Process::ReadPointersFromMemory(llvm::ArrayRef<addr_t> ptr_locs) {
const size_t ptr_size = GetAddressByteSize();
return ReadUnsignedIntegersFromMemory(ptr_locs, ptr_size);
}
bool Process::WritePointerToMemory(lldb::addr_t vm_addr, lldb::addr_t ptr_value,
Status &error) {
Scalar scalar;

View File

@@ -521,3 +521,98 @@ TEST_F(MemoryTest, TestReadCStringsFromMemory) {
llvm::zip(expected_valid_strings, expected_answers))
EXPECT_EQ(maybe_str, expected_answer);
}
TEST_F(MemoryTest, TestReadPointersFromMemory) {
ArchSpec arch("x86_64-apple-macosx-");
Platform::SetHostPlatform(PlatformRemoteMacOSX::CreateInstance(true, &arch));
DebuggerSP debugger_sp = Debugger::CreateInstance();
ASSERT_TRUE(debugger_sp);
TargetSP target_sp = CreateTarget(debugger_sp, arch);
ASSERT_TRUE(target_sp);
ListenerSP listener_sp(Listener::MakeListener("dummy"));
ProcessSP process =
std::make_shared<DummyReaderProcess>(target_sp, listener_sp);
ASSERT_TRUE(process);
// Read pointers at arbitrary addresses.
llvm::SmallVector<addr_t> ptr_locs = {0x0, 0x100, 0x2000, 0x123400};
// Because of how DummyReaderProcess works, each byte of a memory read result
// is its address modulo 256:
constexpr addr_t expected_result = 0x0706050403020100;
llvm::SmallVector<std::optional<addr_t>> read_results =
process->ReadPointersFromMemory(ptr_locs);
for (std::optional<addr_t> maybe_ptr : read_results) {
ASSERT_TRUE(maybe_ptr.has_value());
EXPECT_EQ(*maybe_ptr, expected_result);
}
}
TEST_F(MemoryTest, TestReadUnsignedIntegersFromMemory) {
ArchSpec arch("x86_64-apple-macosx-");
Platform::SetHostPlatform(PlatformRemoteMacOSX::CreateInstance(true, &arch));
DebuggerSP debugger_sp = Debugger::CreateInstance();
ASSERT_TRUE(debugger_sp);
TargetSP target_sp = CreateTarget(debugger_sp, arch);
ASSERT_TRUE(target_sp);
ListenerSP listener_sp(Listener::MakeListener("dummy"));
ProcessSP process =
std::make_shared<DummyReaderProcess>(target_sp, listener_sp);
ASSERT_TRUE(process);
{ // Test reads of size 1
llvm::SmallVector<addr_t> locs = {0x0, 0x101, 0x2002, 0x123403};
llvm::SmallVector<std::optional<addr_t>> read_results =
process->ReadUnsignedIntegersFromMemory(locs, /*byte_size=*/1);
for (auto [maybe_int, loc] : llvm::zip(read_results, locs)) {
ASSERT_TRUE(maybe_int.has_value());
EXPECT_EQ(*maybe_int, static_cast<uint8_t>(loc));
}
}
{ // Test reads of size 2
llvm::SmallVector<addr_t> locs = {0x0, 0x101, 0x2002, 0x123403};
llvm::SmallVector<std::optional<addr_t>> read_results =
process->ReadUnsignedIntegersFromMemory(locs, /*byte_size=*/2);
for (auto [maybe_int, loc] : llvm::zip(read_results, locs)) {
ASSERT_TRUE(maybe_int.has_value());
uint64_t lsb = static_cast<uint8_t>(loc);
uint64_t expected_result = ((lsb + 1) << 8) | lsb;
EXPECT_EQ(*maybe_int, expected_result);
}
}
{ // Test reads of size 4
llvm::SmallVector<addr_t> locs = {0x0, 0x101, 0x2002, 0x123403};
llvm::SmallVector<std::optional<addr_t>> read_results =
process->ReadUnsignedIntegersFromMemory(locs, /*byte_size=*/4);
for (auto [maybe_int, loc] : llvm::zip(read_results, locs)) {
ASSERT_TRUE(maybe_int.has_value());
uint64_t lsb = static_cast<uint8_t>(loc);
uint64_t expected_result =
((lsb + 3) << 24) | ((lsb + 2) << 16) | ((lsb + 1) << 8) | lsb;
EXPECT_EQ(*maybe_int, expected_result);
}
}
{ // Test reads of size 8
llvm::SmallVector<addr_t> locs = {0x0, 0x101, 0x2002, 0x123403};
llvm::SmallVector<std::optional<addr_t>> read_results =
process->ReadUnsignedIntegersFromMemory(locs, /*byte_size=*/8);
for (auto [maybe_int, loc] : llvm::zip(read_results, locs)) {
ASSERT_TRUE(maybe_int.has_value());
uint64_t lsb = static_cast<uint8_t>(loc);
uint64_t expected_result = ((lsb + 7) << 56) | ((lsb + 6) << 48) |
((lsb + 5) << 40) | ((lsb + 4) << 32) |
((lsb + 3) << 24) | ((lsb + 2) << 16) |
((lsb + 1) << 8) | lsb;
EXPECT_EQ(*maybe_int, expected_result);
}
}
}