Skip to content

[lldb-dap] Use protocol types for ReadMemory request #144552

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jun 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions lldb/test/API/tools/lldb-dap/memory/TestDAP_memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,17 @@ def test_readMemory(self):
# VS Code sends those in order to check if a `memoryReference` can actually be dereferenced.
mem = self.dap_server.request_readMemory(memref, 0, 0)
self.assertEqual(mem["success"], True)
self.assertEqual(mem["body"]["data"], "")
self.assertNotIn(
"data", mem["body"], f"expects no data key in response: {mem!r}"
)

# Reads at offset 0x0 return unreadable bytes
bytes_to_read = 6
mem = self.dap_server.request_readMemory("0x0", 0, bytes_to_read)
self.assertEqual(mem["body"]["unreadableBytes"], bytes_to_read)

# Reads with invalid address fails.
mem = self.dap_server.request_readMemory("-3204", 0, 10)
self.assertFalse(mem["success"], "expect fail on reading memory.")

# Reads at offset 0x0 fail
mem = self.dap_server.request_readMemory("0x0", 0, 6)
self.assertEqual(mem["success"], False)
self.continue_to_exit()
143 changes: 27 additions & 116 deletions lldb/tools/lldb-dap/Handler/ReadMemoryRequestHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,136 +7,47 @@
//===----------------------------------------------------------------------===//

#include "DAP.h"
#include "EventHelper.h"
#include "JSONUtils.h"
#include "RequestHandler.h"
#include "llvm/ADT/StringExtras.h"
#include "llvm/Support/Base64.h"

namespace lldb_dap {

// "ReadMemoryRequest": {
// "allOf": [ { "$ref": "#/definitions/Request" }, {
// "type": "object",
// "description": "Reads bytes from memory at the provided location. Clients
// should only call this request if the corresponding
// capability `supportsReadMemoryRequest` is true.",
// "properties": {
// "command": {
// "type": "string",
// "enum": [ "readMemory" ]
// },
// "arguments": {
// "$ref": "#/definitions/ReadMemoryArguments"
// }
// },
// "required": [ "command", "arguments" ]
// }]
// },
// "ReadMemoryArguments": {
// "type": "object",
// "description": "Arguments for `readMemory` request.",
// "properties": {
// "memoryReference": {
// "type": "string",
// "description": "Memory reference to the base location from which data
// should be read."
// },
// "offset": {
// "type": "integer",
// "description": "Offset (in bytes) to be applied to the reference
// location before reading data. Can be negative."
// },
// "count": {
// "type": "integer",
// "description": "Number of bytes to read at the specified location and
// offset."
// }
// },
// "required": [ "memoryReference", "count" ]
// },
// "ReadMemoryResponse": {
// "allOf": [ { "$ref": "#/definitions/Response" }, {
// "type": "object",
// "description": "Response to `readMemory` request.",
// "properties": {
// "body": {
// "type": "object",
// "properties": {
// "address": {
// "type": "string",
// "description": "The address of the first byte of data returned.
// Treated as a hex value if prefixed with `0x`, or
// as a decimal value otherwise."
// },
// "unreadableBytes": {
// "type": "integer",
// "description": "The number of unreadable bytes encountered after
// the last successfully read byte.\nThis can be
// used to determine the number of bytes that should
// be skipped before a subsequent
// `readMemory` request succeeds."
// },
// "data": {
// "type": "string",
// "description": "The bytes read from memory, encoded using base64.
// If the decoded length of `data` is less than the
// requested `count` in the original `readMemory`
// request, and `unreadableBytes` is zero or
// omitted, then the client should assume it's
// reached the end of readable memory."
// }
// },
// "required": [ "address" ]
// }
// }
// }]
// },
void ReadMemoryRequestHandler::operator()(
const llvm::json::Object &request) const {
llvm::json::Object response;
FillResponse(request, response);
auto *arguments = request.getObject("arguments");
// Reads bytes from memory at the provided location.
//
// Clients should only call this request if the corresponding capability
// `supportsReadMemoryRequest` is true
llvm::Expected<protocol::ReadMemoryResponseBody>
ReadMemoryRequestHandler::Run(const protocol::ReadMemoryArguments &args) const {
const lldb::addr_t raw_address = args.memoryReference + args.offset;

llvm::StringRef memoryReference =
GetString(arguments, "memoryReference").value_or("");
auto addr_opt = DecodeMemoryReference(memoryReference);
if (!addr_opt.has_value()) {
response["success"] = false;
response["message"] =
"Malformed memory reference: " + memoryReference.str();
dap.SendJSON(llvm::json::Value(std::move(response)));
return;
}
lldb::addr_t addr_int = *addr_opt;
addr_int += GetInteger<uint64_t>(arguments, "offset").value_or(0);
const uint64_t count_requested =
GetInteger<uint64_t>(arguments, "count").value_or(0);
lldb::SBProcess process = dap.target.GetProcess();
if (!lldb::SBDebugger::StateIsStoppedState(process.GetState()))
return llvm::make_error<NotStoppedError>();

const uint64_t count_read = std::max<uint64_t>(args.count, 1);
// We also need support reading 0 bytes
// VS Code sends those requests to check if a `memoryReference`
// can be dereferenced.
const uint64_t count_read = std::max<uint64_t>(count_requested, 1);
std::vector<uint8_t> buf;
buf.resize(count_read);
protocol::ReadMemoryResponseBody response;
std::vector<std::byte> &buffer = response.data;
buffer.resize(count_read);

lldb::SBError error;
lldb::SBAddress addr{addr_int, dap.target};
size_t count_result =
dap.target.ReadMemory(addr, buf.data(), count_read, error);
if (count_result == 0) {
response["success"] = false;
EmplaceSafeString(response, "message", error.GetCString());
dap.SendJSON(llvm::json::Value(std::move(response)));
return;
const size_t memory_count = dap.target.GetProcess().ReadMemory(
raw_address, buffer.data(), buffer.size(), error);

response.address = "0x" + llvm::utohexstr(raw_address);

// reading memory may fail for multiple reasons. memory not readable,
// reading out of memory range and gaps in memory. return from
// the last readable byte.
if (error.Fail() && (memory_count < count_read)) {
response.unreadableBytes = count_read - memory_count;
}
buf.resize(std::min<size_t>(count_result, count_requested));

llvm::json::Object body;
std::string formatted_addr = "0x" + llvm::utohexstr(addr_int);
body.try_emplace("address", formatted_addr);
body.try_emplace("data", llvm::encodeBase64(buf));
response.try_emplace("body", std::move(body));
dap.SendJSON(llvm::json::Value(std::move(response)));
buffer.resize(std::min<size_t>(memory_count, args.count));
return response;
}

} // namespace lldb_dap
9 changes: 6 additions & 3 deletions lldb/tools/lldb-dap/Handler/RequestHandler.h
Original file line number Diff line number Diff line change
Expand Up @@ -564,14 +564,17 @@ class DisassembleRequestHandler final
Run(const protocol::DisassembleArguments &args) const override;
};

class ReadMemoryRequestHandler : public LegacyRequestHandler {
class ReadMemoryRequestHandler final
: public RequestHandler<protocol::ReadMemoryArguments,
llvm::Expected<protocol::ReadMemoryResponseBody>> {
public:
using LegacyRequestHandler::LegacyRequestHandler;
using RequestHandler::RequestHandler;
static llvm::StringLiteral GetCommand() { return "readMemory"; }
FeatureSet GetSupportedFeatures() const override {
return {protocol::eAdapterFeatureReadMemoryRequest};
}
void operator()(const llvm::json::Object &request) const override;
llvm::Expected<protocol::ReadMemoryResponseBody>
Run(const protocol::ReadMemoryArguments &args) const override;
};

class CancelRequestHandler : public RequestHandler<protocol::CancelArguments,
Expand Down
37 changes: 37 additions & 0 deletions lldb/tools/lldb-dap/Protocol/ProtocolRequests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
//===----------------------------------------------------------------------===//

#include "Protocol/ProtocolRequests.h"
#include "JSONUtils.h"
#include "llvm/ADT/DenseMap.h"
#include "llvm/ADT/StringMap.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/Support/Base64.h"
#include "llvm/Support/JSON.h"
#include <utility>

Expand Down Expand Up @@ -480,4 +482,39 @@ json::Value toJSON(const DisassembleResponseBody &DRB) {
return json::Object{{"instructions", DRB.instructions}};
}

bool fromJSON(const json::Value &Params, ReadMemoryArguments &RMA,
json::Path P) {
json::ObjectMapper O(Params, P);

const json::Object *rma_obj = Params.getAsObject();
constexpr llvm::StringRef ref_key = "memoryReference";
const std::optional<llvm::StringRef> memory_ref = rma_obj->getString(ref_key);
if (!memory_ref) {
P.field(ref_key).report("missing value");
return false;
}

const std::optional<lldb::addr_t> addr_opt =
DecodeMemoryReference(*memory_ref);
if (!addr_opt) {
P.field(ref_key).report("Malformed memory reference");
return false;
}

RMA.memoryReference = *addr_opt;

return O && O.map("count", RMA.count) && O.mapOptional("offset", RMA.offset);
}

json::Value toJSON(const ReadMemoryResponseBody &RMR) {
json::Object result{{"address", RMR.address}};

if (RMR.unreadableBytes != 0)
result.insert({"unreadableBytes", RMR.unreadableBytes});
if (!RMR.data.empty())
result.insert({"data", llvm::encodeBase64(RMR.data)});

return result;
}

} // namespace lldb_dap::protocol
36 changes: 36 additions & 0 deletions lldb/tools/lldb-dap/Protocol/ProtocolRequests.h
Original file line number Diff line number Diff line change
Expand Up @@ -839,6 +839,42 @@ bool fromJSON(const llvm::json::Value &, DisassembleResponseBody &,
llvm::json::Path);
llvm::json::Value toJSON(const DisassembleResponseBody &);

/// Arguments for `readMemory` request.
struct ReadMemoryArguments {
/// Memory reference to the base location from which data should be read.
lldb::addr_t memoryReference;

/// Offset (in bytes) to be applied to the reference location before reading
/// data. Can be negative.
int64_t offset = 0;

/// Number of bytes to read at the specified location and offset.
uint64_t count;
};
bool fromJSON(const llvm::json::Value &, ReadMemoryArguments &,
llvm::json::Path);

/// Response to `readMemory` request.
struct ReadMemoryResponseBody {
/// The address of the first byte of data returned.
/// Treated as a hex value if prefixed with `0x`, or as a decimal value
/// otherwise.
std::string address;

/// The number of unreadable bytes encountered after the last successfully
/// read byte.
/// This can be used to determine the number of bytes that should be skipped
/// before a subsequent `readMemory` request succeeds.
uint64_t unreadableBytes = 0;

/// The bytes read from memory, encoded using base64. If the decoded length
/// of `data` is less than the requested `count` in the original `readMemory`
/// request, and `unreadableBytes` is zero or omitted, then the client should
/// assume it's reached the end of readable memory.
std::vector<std::byte> data;
Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry for making you go back and forth on this. Reading more of the helpers in llvm/ADT/StringExtras.h and the like, it looks like in llvm raw data bytes are are often either represented as llvm::ArrayRef<uint8_t> (for non-owning cases) or a std::string (for cases where you'd want to own the underlying data buffer).

I'm just used to swift where there is a separate Data type (from Foundation or Array<Uint8> used in the swift stdlib) for this and I was trying to figure out the c++ equivalent. It seems that std::string is the common way to handle this.

I guess we can go back to std::string data; here and then still convert the value to hex in from/toJSON or if you want to keep using the std::vector<std::byte> thats also fine.

In the SB API there is a lldb::SBData type for this use case and in lldb_private its built on top of lldb_private::DataExtractor but for the protocol representation, I am not sure either of those types make sense.

I'm open to suggestions for this, I just was worried about mishandling the data since it looked like a string.

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 am happy with std::vector<std::byte> since it feels more representative of the actual data.

};
llvm::json::Value toJSON(const ReadMemoryResponseBody &);

} // namespace lldb_dap::protocol

#endif
33 changes: 33 additions & 0 deletions lldb/unittests/DAP/ProtocolTypesTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -765,3 +765,36 @@ TEST(ProtocolTypesTest, StepInTarget) {
EXPECT_EQ(target.endLine, deserialized_target->endLine);
EXPECT_EQ(target.endColumn, deserialized_target->endColumn);
}

TEST(ProtocolTypesTest, ReadMemoryArguments) {
ReadMemoryArguments args;
args.count = 20;
args.memoryReference = 43962;
args.offset = 0;

llvm::Expected<ReadMemoryArguments> expected =
parse<ReadMemoryArguments>(R"({"memoryReference":"-4000", "count": 20})");
ASSERT_THAT_EXPECTED(expected, llvm::Failed());
expected = parse<ReadMemoryArguments>(
R"({"memoryReference":"0xabba", "count": 20})");
ASSERT_THAT_EXPECTED(expected, llvm::Succeeded());

EXPECT_EQ(args.count, expected->count);
EXPECT_EQ(args.memoryReference, expected->memoryReference);
EXPECT_EQ(args.offset, expected->offset);
}

TEST(ProtocolTypesTest, ReadMemoryResponseBody) {
ReadMemoryResponseBody response;
response.address = "0xdeadbeef";
const std::string data_str = "hello world!";
std::transform(data_str.begin(), data_str.end(),
std::back_inserter(response.data),
[](char letter) { return std::byte(letter); });
response.unreadableBytes = 1;

Expected<Value> expected = json::parse(
R"({ "address": "0xdeadbeef", "data": "aGVsbG8gd29ybGQh", "unreadableBytes": 1})");
ASSERT_THAT_EXPECTED(expected, llvm::Succeeded());
EXPECT_EQ(pp(*expected), pp(response));
}
Loading