Skip to content

Conversation

@felipepiovezan
Copy link
Contributor

@felipepiovezan felipepiovezan commented Oct 15, 2025

This commit introduces a base-class implementation for a method that reads memory from multiple ranges at once. This implementation simply calls the underlying ReadMemoryFromInferior method on each requested range, intentionally bypassing the memory caching mechanism (though this may be easily changed in the future).

Process implementations that can be perform this operation more efficiently - e.g. with the MultiMemPacket described in 1 - are expected to override this method.

As an example, this commit changes AppleObjCClassDescriptorV2 to use the new API.

Note about the API

In the RFC, we discussed having the API return some kind of class ReadMemoryRangesResult. However, while writing such a class, it became clear that it was merely wrapping a vector, without providing anything useful. For example, this class:

struct ReadMemoryRangesResult {
  ReadMemoryRangesResult(
      llvm::SmallVector<llvm::MutableArrayRef<uint8_t>> ranges)
      : ranges(std::move(ranges)) {}

  llvm::ArrayRef<llvm::MutableArrayRef<uint8_t>> getRanges() const {
    return ranges;
  }

private:
  llvm::SmallVector<llvm::MutableArrayRef<uint8_t>> ranges;
};

As can be seen in the added test and in the added use-case (AppleObjCClassDescriptorV2), users of this API will just iterate over the vector of memory buffers. So they want a return type that can be iterated over, and the vector seems more natural than creating a new class and defining iterators for it.

Likewise, in the RFC, we discussed wrapping the result into an Expected. Upon experimenting with the code, this feels like it limits what the API is able to do as the base class implementation never needs to fail the entire result, it's the individual reads that may fail and this is expressed through a zero-length result. Any derived classes overriding ReadMemoryRanges should also never produce a top level failure: if they did, they can just fall back to the base class implementation, which would produce a better result.

The choice of having the caller allocate a buffer and pass it to Process::ReadMemoryRanges is done mostly to follow conventions already done in the Process class.

@llvmbot
Copy link
Member

llvmbot commented Oct 15, 2025

@llvm/pr-subscribers-lldb

Author: Felipe de Azevedo Piovezan (felipepiovezan)

Changes

This commit introduces a base-class implementation for a method that reads memory from multiple ranges at once. This implementation simply calls the underlying ReadMemoryFromInferior method on each requested range, intentionally bypassing the memory caching mechanism (though this may be easily changed in the future).

Process implementations that can be perform this operation more efficiently - e.g. with the MultiMemPacket described in 1 - are expected to override this method.

As an example, this commit changes AppleObjCClassDescriptorV2 to use the new API.

Note about the API

In the RFC, we discussed having the API return some kind of class ReadMemoryRangesResult. However, while writing such a class, it became clear that it was merely wrapping a vector, without providing anything useful. For example, this class:

struct ReadMemoryRangesResult {
  ReadMemoryRangesResult(
      llvm::SmallVector&lt;llvm::MutableArrayRef&lt;uint8_t&gt;&gt; ranges)
      : ranges(std::move(ranges)) {}

  llvm::ArrayRef&lt;llvm::MutableArrayRef&lt;uint8_t&gt;&gt; getRanges() const {
    return ranges;
  }

private:
  llvm::SmallVector&lt;llvm::MutableArrayRef&lt;uint8_t&gt;&gt; ranges;
};

As can be seen in the added test and in the added use-case (AppleObjCClassDescriptorV2), users of this API will just iterate over the vector of memory buffers. So they want a return type that can be iterated over, and the vector seems more natural than creating a new class and defining iterators for it.

Likewise, in the RFC, we discussed wrapping the result into an Expected. Upon experimenting with the code, this feels like it limits what the API is able to do as the base class implementation never needs to fail the entire result, it's the individual reads that may fail and this is expressed through a zero-length result. Any derived classes overriding ReadMemoryRanges should also never produce a top level failure: if they did, they can just fall back to the base class implementation, which would produce a better result.


Full diff: https://github.com/llvm/llvm-project/pull/163651.diff

4 Files Affected:

  • (modified) lldb/include/lldb/Target/Process.h (+19)
  • (modified) lldb/source/Plugins/LanguageRuntime/ObjC/AppleObjCRuntime/AppleObjCClassDescriptorV2.cpp (+12-11)
  • (modified) lldb/source/Target/Process.cpp (+28)
  • (modified) lldb/unittests/Target/MemoryTest.cpp (+62)
diff --git a/lldb/include/lldb/Target/Process.h b/lldb/include/lldb/Target/Process.h
index dc75d98acea70..7a260323b5a3d 100644
--- a/lldb/include/lldb/Target/Process.h
+++ b/lldb/include/lldb/Target/Process.h
@@ -1571,6 +1571,25 @@ class Process : public std::enable_shared_from_this<Process>,
   virtual size_t ReadMemory(lldb::addr_t vm_addr, void *buf, size_t size,
                             Status &error);
 
+  /// Read from multiple memory ranges and write the results into buffer.
+  ///
+  /// \param[in] ranges
+  ///     A collection of ranges (base address + size) to read from.
+  ///
+  /// \param[out] buffer
+  ///     A buffer where the read memory will be written to. It must be at least
+  ///     as long as the sum of the sizes of each range.
+  ///
+  /// \return
+  ///     A vector of MutableArrayRef, where each MutableArrayRef is a slice of
+  ///     the input buffer into which the memory contents were copied. The size
+  ///     of the slice indicates how many bytes were read successfully. Partial
+  ///     reads are always performed from the start of the requested range,
+  ///     never from the middle or end.
+  virtual llvm::SmallVector<llvm::MutableArrayRef<uint8_t>>
+  ReadMemoryRanges(llvm::ArrayRef<Range<lldb::addr_t, size_t>> ranges,
+                   llvm::MutableArrayRef<uint8_t> buffer);
+
   /// Read of memory from a process.
   ///
   /// This function has the same semantics of ReadMemory except that it
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/AppleObjCRuntime/AppleObjCClassDescriptorV2.cpp b/lldb/source/Plugins/LanguageRuntime/ObjC/AppleObjCRuntime/AppleObjCClassDescriptorV2.cpp
index 6d8f41aef1ffc..7ce4cbf4c61a4 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/AppleObjCRuntime/AppleObjCClassDescriptorV2.cpp
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/AppleObjCRuntime/AppleObjCClassDescriptorV2.cpp
@@ -279,22 +279,23 @@ ClassDescriptorV2::ReadMethods(llvm::ArrayRef<lldb::addr_t> addresses,
   const size_t num_methods = addresses.size();
 
   llvm::SmallVector<uint8_t, 0> buffer(num_methods * size, 0);
-  llvm::DenseSet<uint32_t> failed_indices;
 
-  for (auto [idx, addr] : llvm::enumerate(addresses)) {
-    Status error;
-    process->ReadMemory(addr, buffer.data() + idx * size, size, error);
-    if (error.Fail())
-      failed_indices.insert(idx);
-  }
+  llvm::SmallVector<Range<addr_t, size_t>> mem_ranges =
+      llvm::to_vector(llvm::map_range(llvm::seq(num_methods), [&](size_t idx) {
+        return Range<addr_t, size_t>(addresses[idx], size);
+      }));
+
+  llvm::SmallVector<llvm::MutableArrayRef<uint8_t>> read_results =
+      process->ReadMemoryRanges(mem_ranges, buffer);
 
   llvm::SmallVector<method_t, 0> methods;
   methods.reserve(num_methods);
-  for (auto [idx, addr] : llvm::enumerate(addresses)) {
-    if (failed_indices.contains(idx))
+  for (auto [addr, memory] : llvm::zip(addresses, read_results)) {
+    // Ignore partial reads.
+    if (memory.size() != size)
       continue;
-    DataExtractor extractor(buffer.data() + idx * size, size,
-                            process->GetByteOrder(),
+
+    DataExtractor extractor(memory.data(), size, process->GetByteOrder(),
                             process->GetAddressByteSize());
     methods.push_back(method_t());
     methods.back().Read(extractor, process, addr, relative_selector_base_addr,
diff --git a/lldb/source/Target/Process.cpp b/lldb/source/Target/Process.cpp
index 3176852f0b724..9692a7fece7e5 100644
--- a/lldb/source/Target/Process.cpp
+++ b/lldb/source/Target/Process.cpp
@@ -1971,6 +1971,34 @@ size_t Process::ReadMemory(addr_t addr, void *buf, size_t size, Status &error) {
   }
 }
 
+llvm::SmallVector<llvm::MutableArrayRef<uint8_t>>
+Process::ReadMemoryRanges(llvm::ArrayRef<Range<lldb::addr_t, size_t>> ranges,
+                          llvm::MutableArrayRef<uint8_t> buffer) {
+  llvm::SmallVector<llvm::MutableArrayRef<uint8_t>> results;
+
+  for (auto [addr, len] : ranges) {
+    // This is either a programmer error, or a protocol violation.
+    // In production builds, gracefully fail.
+    assert(buffer.size() >= len);
+    if (buffer.size() < len) {
+      results.push_back(buffer.take_front(0));
+      continue;
+    }
+
+    Status status;
+    size_t num_bytes_read =
+        ReadMemoryFromInferior(addr, buffer.data(), len, status);
+    // FIXME: ReadMemoryFromInferior promises to return 0 in case of errors, but
+    // it doesn't; it never checks for errors.
+    if (status.Fail())
+      num_bytes_read = 0;
+    results.push_back(buffer.take_front(num_bytes_read));
+    buffer = buffer.drop_front(num_bytes_read);
+  }
+
+  return results;
+}
+
 void Process::DoFindInMemory(lldb::addr_t start_addr, lldb::addr_t end_addr,
                              const uint8_t *buf, size_t size,
                              AddressRanges &matches, size_t alignment,
diff --git a/lldb/unittests/Target/MemoryTest.cpp b/lldb/unittests/Target/MemoryTest.cpp
index 4a96730e00464..1317dd27b256e 100644
--- a/lldb/unittests/Target/MemoryTest.cpp
+++ b/lldb/unittests/Target/MemoryTest.cpp
@@ -17,6 +17,8 @@
 #include "lldb/Utility/ArchSpec.h"
 #include "lldb/Utility/DataBufferHeap.h"
 #include "gtest/gtest.h"
+#include <cstdint>
+#include <random>
 
 using namespace lldb_private;
 using namespace lldb;
@@ -225,3 +227,63 @@ TEST_F(MemoryTest, TesetMemoryCacheRead) {
                                                        // instead of using an
                                                        // old cache
 }
+
+/// A process class that reads `lower_byte(address)` for each `address` it
+/// reads.
+class DummyReaderProcess : public Process {
+public:
+  size_t DoReadMemory(lldb::addr_t vm_addr, void *buf, size_t size,
+                      Status &error) override {
+    uint8_t *buffer = static_cast<uint8_t*>(buf);
+    for(size_t addr = vm_addr; addr < vm_addr + size; addr++)
+      buffer[addr - vm_addr] = addr;
+    return size;
+  }
+  // Boilerplate, nothing interesting below.
+  DummyReaderProcess(lldb::TargetSP target_sp, lldb::ListenerSP listener_sp)
+      : Process(target_sp, listener_sp) {}
+  bool CanDebug(lldb::TargetSP, bool) override { return true; }
+  Status DoDestroy() override { return {}; }
+  void RefreshStateAfterStop() override {}
+  bool DoUpdateThreadList(ThreadList &, ThreadList &) override { return false; }
+  llvm::StringRef GetPluginName() override { return "Dummy"; }
+};
+
+TEST_F(MemoryTest, TestReadMemoryRanges) {
+  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_sp =
+      std::make_shared<DummyReaderProcess>(target_sp, listener_sp);
+  ASSERT_TRUE(process_sp);
+
+  DummyProcess *process = static_cast<DummyProcess *>(process_sp.get());
+  process->SetMaxReadSize(1024);
+
+  llvm::SmallVector<uint8_t, 0> buffer(1024, 0);
+
+  // Read 8 ranges of 128 bytes, starting at random addresses
+  std::mt19937 rng(42);
+  std::uniform_int_distribution<addr_t> distribution(1, 100000);
+  llvm::SmallVector<Range<addr_t, size_t>> ranges;
+  for (unsigned i = 0; i < 1024; i += 128)
+    ranges.emplace_back(distribution(rng), 128);
+
+  llvm::SmallVector<llvm::MutableArrayRef<uint8_t>> read_results =
+      process->ReadMemoryRanges(ranges, buffer);
+
+  for (auto [range, memory] : llvm::zip(ranges, read_results)) {
+    ASSERT_EQ(memory.size(), 128u);
+    addr_t range_base = range.GetRangeBase();
+    for (auto [idx, byte] : llvm::enumerate(memory))
+      ASSERT_EQ(byte, static_cast<uint8_t>(range_base + idx));
+  }
+}

@github-actions
Copy link

github-actions bot commented Oct 15, 2025

✅ With the latest revision this PR passed the C/C++ code formatter.

@felipepiovezan felipepiovezan force-pushed the felipe/multimem_baseline_process_impl branch from 4f47055 to d6f54b4 Compare October 15, 2025 22:35
Copy link
Collaborator

@DavidSpickett DavidSpickett left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the thinking for doing this in "ptrace style" where the caller allocates a buffer and gives it to the function, vs. allocating the buffer on behalf of the caller?

The latter would make it impossible to get the buffer size wrong, but on the other hand, the caller would know best how to allocate a given size buffer (stack, heap, and so on).

If the main use case (and the only one right now) is a small number of bytes from a large set of addresses, we're probably talking stack sized allocations not heap. So allowing the caller to set that up would be logical.

I also thought what about a MultiMemWrite, would it make sense to have a symmetric API for that hypothetical packet. If the caller allocates the buffer, the two would be symmetric. And you could do a "gather read" from N places and "scatter write" to N other places.

(no real use case for this, just theory crafting)

It's an internal API so we can always swap later but please include your thinking in the PR description even if it's an arbitrary choice, might be interesting in future.

for (auto [addr, len] : ranges) {
// This is either a programmer error, or a protocol violation.
// In production builds, gracefully fail.
assert(buffer.size() >= len);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think your algorithm here is:

  • For each range
  • Take that much from the front of buffer, making buffer smaller
  • So if at any point, there aren't "len" bytes left to take, the caller made a mistake and didn't provide enough buffer

I was confused at first because I read the assert backwards. We expect that the remaining buffer size will be >= len of the current range, if it's not then it fails.

But then I see below, take_front(0). And now I'm confused again.

I think some comments would help like:
when the remaining buffer is > range we do this
when it's exactly the same we do this
when it's less than we decide that's an error so we do this

// In production builds, gracefully fail.
assert(buffer.size() >= len);
if (buffer.size() < len) {
results.push_back(buffer.take_front(0));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For instance, this is a bit cryptic without a comment.

I suppose it's because there has to be some pointer into the original buffer, but you are returning 0 bytes for this. Perhaps you can use buffer.data instead to make it clear we do not intend to move the start of the buffer at all?

Seeing 0 here makes me wonder if you meant to type another number.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here I should have just done emplace_back(nullptr, 0) to be honest, would have made the point more evident.


llvm::SmallVector<uint8_t, 0> buffer(1024, 0);

// Read 8 ranges of 128 bytes, starting at random addresses
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Random in tests rings alarm bells for me but hopefully once I read the rest I'll understand why you need this.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After reading this, I think I understand. The addresses and sizes you put in are somewhat arbitrary.

I think I would prefer to see a few "carefully chosen" sequences instead, or as well. Boundary conditions at least.

This is a relatively simple algorithm to test but if it did fail, I'm not sure the failure reporting would be good enough to know what inputs it was given. Maybe GoogleTest will surprise me though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Random in tests rings alarm bells for me but hopefully once I read the rest I'll understand why you need this.

Worth mentioning that this is deterministic, as it uses a fixed seed. So every platform and every run will use the exact same data.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can write some arbitrary sequences instead, it was just simpler to generate the sequence this way

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok part of this is me being lazy, but future me and anyone else will be even more lazy, so add a comment that it is in fact a fixed seed.

A fixed arbitrary seed that is.

// old cache
}

/// A process class that reads `lower_byte(address)` for each `address` it
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is lower_byte? Do you mean literally it returns the least significant byte of the address? This looks like a function call the way you wrote it.

(the idea is fine overall just the way it's written)

Status &error) override {
uint8_t *buffer = static_cast<uint8_t *>(buf);
for (size_t addr = vm_addr; addr < vm_addr + size; addr++)
buffer[addr - vm_addr] = addr;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a truncation, right? I'd add a static cast to make that clear and keep the compiler happy.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would also move the comment about the lower byte to here, or repeat it.

@felipepiovezan
Copy link
Contributor Author

What's the thinking for doing this in "ptrace style" where the caller allocates a buffer and gives it to the function, vs. allocating the buffer on behalf of the caller?

No real motivation other than "the other APIs in Process do this, so this must have been discussed in the past", I'll add this to the commit/PR messages. (I'm assuming that's fine, as nobody objected to it in the RFC, but I have no strong opinions).

@DavidSpickett
Copy link
Collaborator

(I'm assuming that's fine, as nobody objected to it in the RFC, but I have no strong opinions)

I'm fine with it. "when in Rome" is a decent argument when there's no particular reason to go one way or the other.

This commit introduces a base-class implementation for a method that
reads memory from multiple ranges at once. This implementation simply
calls the underlying `ReadMemoryFromInferior` method on each requested
range, intentionally bypassing the memory caching mechanism (though this
may be easily changed in the future).

`Process` implementations that can be perform this operation more
efficiently - e.g. with the MultiMemPacket described in [1] - are
expected to override this method.

As an example, this commit changes AppleObjCClassDescriptorV2 to use the
new API.

Note about the API
------------------

In the RFC, we discussed having the API return some kind of class
`ReadMemoryRangesResult`. However, while writing such a class, it became
clear that it was merely wrapping a vector, without providing anything
useful. For example, this class:

```
struct ReadMemoryRangesResult {
  ReadMemoryRangesResult(
      llvm::SmallVector<llvm::MutableArrayRef<uint8_t>> ranges)
      : ranges(std::move(ranges)) {}

  llvm::ArrayRef<llvm::MutableArrayRef<uint8_t>> getRanges() const {
    return ranges;
  }

private:
  llvm::SmallVector<llvm::MutableArrayRef<uint8_t>> ranges;
};
```

As can be seen in the added test and in the added use-case
(AppleObjCClassDescriptorV2), users of this API will just iterate over
the vector of memory buffers. So they want a return type that can be
iterated over, and the vector seems more natural than creating a new
class and defining iterators for it.

Likewise, in the RFC, we discussed wrapping the result into an
`Expected`. Upon experimenting with the code, this feels like it limits
what the API is able to do as the base class implementation never needs
to fail the entire result, it's the individual reads that may fail and
this is expressed through a zero-length result. Any derived classes
overriding `ReadMemoryRanges` should also never produce a top level
failure: if they did, they can just fall back to the base class
implementation, which would produce a better result.

The choice of having the caller allocate a buffer and pass it to
`Process::ReadMemoryRanges` is done mostly to follow conventions already
done in the Process class.

[1]: https://discourse.llvm.org/t/rfc-a-new-vectorized-memory-read-packet/
@felipepiovezan felipepiovezan force-pushed the felipe/multimem_baseline_process_impl branch from d6f54b4 to 58d7eda Compare October 20, 2025 16:52
@felipepiovezan
Copy link
Contributor Author

felipepiovezan commented Oct 20, 2025

Addressed review comments

@felipepiovezan
Copy link
Contributor Author

I moved some of the assertions out of the main loop to simplify the algorithm a bit.

@felipepiovezan
Copy link
Contributor Author

Btw, the ProcessGDBRemote impl is here: #164311
I haven't added any reviews over there yet because that PR depends on this one, I'll do it once I merge this PR.

@felipepiovezan
Copy link
Contributor Author

The self-hosted runner lost communication with the server. Verify the machine is running and has a healthy network connection. Anything in your workflow that terminates the runner process, starves it for CPU/Memory, or blocks its network access can cause this error.

The AArch64 bot seems to be having some infra issues.

Copy link
Collaborator

@jasonmolenda jasonmolenda left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, I think you addressed David's feedback in the commit today.

Copy link
Collaborator

@DavidSpickett DavidSpickett left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with all the behaviour as is, needs a few more tests.

// In production builds, gracefully fail by returning empty chunks.
assert(buffer.size() >= total_ranges_len);
if (buffer.size() < total_ranges_len)
return llvm::SmallVector<llvm::MutableArrayRef<uint8_t>>(ranges.size());
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add an explicit zero value here, it took me ages to parse what this meant (which is a "C++ be like" sort of issue, but still, make it clearer).

Also you say "chunks" but these are not chunks, we don't return chunks as such we return a description of a chunk. I suggest "returning a length of 0 for all ranges".

@felipepiovezan felipepiovezan force-pushed the felipe/multimem_baseline_process_impl branch from a10d92a to 1b470fb Compare October 21, 2025 15:11
@felipepiovezan
Copy link
Contributor Author

Added death tests, removed references to "chunks" in the comments, rewrote return llvm::SmallVector<llvm::MutableArrayRef<uint8_t>>(ranges.size()); into something more palatable


llvm::SmallVector<uint8_t, 0> too_short_buffer(10, 0);
llvm::SmallVector<Range<addr_t, size_t>> ranges = {{0x12345, 128}};
ASSERT_DEATH(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure whether ASSERT_DEATH incorporates a check whether assertions are on, so please check that if you didn't already.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Though I would like to see the non-assert path anyway. So you'll need an ifdef for that.

Copy link
Collaborator

@DavidSpickett DavidSpickett left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to see the non-assert side of the ASSERT_DEATH paths tested (in non-asserts builds, not in the same build of course).

Otherwise LGTM so in the interests of time I'm approving and I'm sure you'll figure it out.

@felipepiovezan
Copy link
Contributor Author

Added release-mode testing. I found out that gtest has a ASSERT_DEBUG_DEATH that just executes statements in release builds (without expecting failures), that's how they recommend doing it (followed by some ifdefs)

@felipepiovezan
Copy link
Contributor Author

The AArch bot is still failing with the message below, so I am going to bypass it.

The self-hosted runner lost communication with the server. Verify the machine is running and has a healthy network connection. Anything in your workflow that terminates the runner process, starves it for CPU/Memory, or blocks its network access can cause this error.

@felipepiovezan felipepiovezan merged commit f2cb997 into llvm:main Oct 21, 2025
9 of 10 checks passed
@felipepiovezan felipepiovezan deleted the felipe/multimem_baseline_process_impl branch October 21, 2025 19:34
@DavidSpickett
Copy link
Collaborator

The unit tests are failing on Arm 32-bit, I'll figure it out.

DavidSpickett added a commit that referenced this pull request Oct 22, 2025
Tests added by #163651.

Use lldb::addr_t (which is always 64-bit) for all addresses
so that we don't calculate an invalid address on 32-bit
and segfault.

As happened on Linaro's Arm 32-bit buildbot.
felipepiovezan added a commit that referenced this pull request Oct 22, 2025
This commit makes use of the newly created MultiMemRead packet to
provide an efficient implementation of MultiMemRead inside
ProcessGDBRemote.

Testing is tricky, but it is accomplished two ways:

1. Some Objective-C tests would fail if this were implemented incorrectly,
as there is already an in-tree use of the base class implementation of
MultiMemRead, which is now getting replaced by the derived class.

2. One Objective-C test is modified so that we ensure the packet is
being sent by looking at the packet logs. While not the most elegant
solution, it is a strategy adopted in other tests as well. This gets
around the fact that we cannot instantiate / unittest a mock
ProcessGDBRemote.

Depends on #163651
llvm-sync bot pushed a commit to arm/arm-toolchain that referenced this pull request Oct 22, 2025
…nges (#164311)

This commit makes use of the newly created MultiMemRead packet to
provide an efficient implementation of MultiMemRead inside
ProcessGDBRemote.

Testing is tricky, but it is accomplished two ways:

1. Some Objective-C tests would fail if this were implemented incorrectly,
as there is already an in-tree use of the base class implementation of
MultiMemRead, which is now getting replaced by the derived class.

2. One Objective-C test is modified so that we ensure the packet is
being sent by looking at the packet logs. While not the most elegant
solution, it is a strategy adopted in other tests as well. This gets
around the fact that we cannot instantiate / unittest a mock
ProcessGDBRemote.

Depends on llvm/llvm-project#163651
@felipepiovezan
Copy link
Contributor Author

The unit tests are failing on Arm 32-bit, I'll figure it out.

thank you!

felipepiovezan added a commit to felipepiovezan/llvm-project that referenced this pull request Oct 22, 2025
This commit introduces a base-class implementation for a method that
reads memory from multiple ranges at once. This implementation simply
calls the underlying `ReadMemoryFromInferior` method on each requested
range, intentionally bypassing the memory caching mechanism (though this
may be easily changed in the future).

`Process` implementations that can be perform this operation more
efficiently - e.g. with the MultiMemPacket described in [1] - are
expected to override this method.

As an example, this commit changes AppleObjCClassDescriptorV2 to use the
new API.

Note about the API
------------------

In the RFC, we discussed having the API return some kind of class
`ReadMemoryRangesResult`. However, while writing such a class, it became
clear that it was merely wrapping a vector, without providing anything
useful. For example, this class:

```
struct ReadMemoryRangesResult {
  ReadMemoryRangesResult(
      llvm::SmallVector<llvm::MutableArrayRef<uint8_t>> ranges)
      : ranges(std::move(ranges)) {}

  llvm::ArrayRef<llvm::MutableArrayRef<uint8_t>> getRanges() const {
    return ranges;
  }

private:
  llvm::SmallVector<llvm::MutableArrayRef<uint8_t>> ranges;
};
```

As can be seen in the added test and in the added use-case
(AppleObjCClassDescriptorV2), users of this API will just iterate over
the vector of memory buffers. So they want a return type that can be
iterated over, and the vector seems more natural than creating a new
class and defining iterators for it.

Likewise, in the RFC, we discussed wrapping the result into an
`Expected`. Upon experimenting with the code, this feels like it limits
what the API is able to do as the base class implementation never needs
to fail the entire result, it's the individual reads that may fail and
this is expressed through a zero-length result. Any derived classes
overriding `ReadMemoryRanges` should also never produce a top level
failure: if they did, they can just fall back to the base class
implementation, which would produce a better result.

The choice of having the caller allocate a buffer and pass it to
`Process::ReadMemoryRanges` is done mostly to follow conventions already
done in the Process class.

[1]:
https://discourse.llvm.org/t/rfc-a-new-vectorized-memory-read-packet/

(cherry picked from commit f2cb997)
felipepiovezan added a commit to felipepiovezan/llvm-project that referenced this pull request Oct 22, 2025
…164311)

This commit makes use of the newly created MultiMemRead packet to
provide an efficient implementation of MultiMemRead inside
ProcessGDBRemote.

Testing is tricky, but it is accomplished two ways:

1. Some Objective-C tests would fail if this were implemented incorrectly,
as there is already an in-tree use of the base class implementation of
MultiMemRead, which is now getting replaced by the derived class.

2. One Objective-C test is modified so that we ensure the packet is
being sent by looking at the packet logs. While not the most elegant
solution, it is a strategy adopted in other tests as well. This gets
around the fact that we cannot instantiate / unittest a mock
ProcessGDBRemote.

Depends on llvm#163651

(cherry picked from commit 276bccd)
felipepiovezan added a commit to felipepiovezan/llvm-project that referenced this pull request Oct 24, 2025
This commit introduces a base-class implementation for a method that
reads memory from multiple ranges at once. This implementation simply
calls the underlying `ReadMemoryFromInferior` method on each requested
range, intentionally bypassing the memory caching mechanism (though this
may be easily changed in the future).

`Process` implementations that can be perform this operation more
efficiently - e.g. with the MultiMemPacket described in [1] - are
expected to override this method.

As an example, this commit changes AppleObjCClassDescriptorV2 to use the
new API.

Note about the API
------------------

In the RFC, we discussed having the API return some kind of class
`ReadMemoryRangesResult`. However, while writing such a class, it became
clear that it was merely wrapping a vector, without providing anything
useful. For example, this class:

```
struct ReadMemoryRangesResult {
  ReadMemoryRangesResult(
      llvm::SmallVector<llvm::MutableArrayRef<uint8_t>> ranges)
      : ranges(std::move(ranges)) {}

  llvm::ArrayRef<llvm::MutableArrayRef<uint8_t>> getRanges() const {
    return ranges;
  }

private:
  llvm::SmallVector<llvm::MutableArrayRef<uint8_t>> ranges;
};
```

As can be seen in the added test and in the added use-case
(AppleObjCClassDescriptorV2), users of this API will just iterate over
the vector of memory buffers. So they want a return type that can be
iterated over, and the vector seems more natural than creating a new
class and defining iterators for it.

Likewise, in the RFC, we discussed wrapping the result into an
`Expected`. Upon experimenting with the code, this feels like it limits
what the API is able to do as the base class implementation never needs
to fail the entire result, it's the individual reads that may fail and
this is expressed through a zero-length result. Any derived classes
overriding `ReadMemoryRanges` should also never produce a top level
failure: if they did, they can just fall back to the base class
implementation, which would produce a better result.

The choice of having the caller allocate a buffer and pass it to
`Process::ReadMemoryRanges` is done mostly to follow conventions already
done in the Process class.

[1]:
https://discourse.llvm.org/t/rfc-a-new-vectorized-memory-read-packet/

(cherry picked from commit f2cb997)
(cherry picked from commit bccb899)
felipepiovezan added a commit to felipepiovezan/llvm-project that referenced this pull request Oct 24, 2025
…164311)

This commit makes use of the newly created MultiMemRead packet to
provide an efficient implementation of MultiMemRead inside
ProcessGDBRemote.

Testing is tricky, but it is accomplished two ways:

1. Some Objective-C tests would fail if this were implemented incorrectly,
as there is already an in-tree use of the base class implementation of
MultiMemRead, which is now getting replaced by the derived class.

2. One Objective-C test is modified so that we ensure the packet is
being sent by looking at the packet logs. While not the most elegant
solution, it is a strategy adopted in other tests as well. This gets
around the fact that we cannot instantiate / unittest a mock
ProcessGDBRemote.

Depends on llvm#163651

(cherry picked from commit 276bccd)
(cherry picked from commit 8e1255f)
dvbuka pushed a commit to dvbuka/llvm-project that referenced this pull request Oct 27, 2025
Tests added by llvm#163651.

Use lldb::addr_t (which is always 64-bit) for all addresses
so that we don't calculate an invalid address on 32-bit
and segfault.

As happened on Linaro's Arm 32-bit buildbot.
dvbuka pushed a commit to dvbuka/llvm-project that referenced this pull request Oct 27, 2025
…164311)

This commit makes use of the newly created MultiMemRead packet to
provide an efficient implementation of MultiMemRead inside
ProcessGDBRemote.

Testing is tricky, but it is accomplished two ways:

1. Some Objective-C tests would fail if this were implemented incorrectly,
as there is already an in-tree use of the base class implementation of
MultiMemRead, which is now getting replaced by the derived class.

2. One Objective-C test is modified so that we ensure the packet is
being sent by looking at the packet logs. While not the most elegant
solution, it is a strategy adopted in other tests as well. This gets
around the fact that we cannot instantiate / unittest a mock
ProcessGDBRemote.

Depends on llvm#163651
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants