Skip to content

Commit f869d6e

Browse files
authored
[LLDB][Minidump]Update MinidumpFileBuilder to read and write in chunks (#129307)
I recently received an internal error report that LLDB was OOM'ing when creating a Minidump. In my 64b refactor we made a decision to acquire buffers the size of the largest memory region so we could read all of the contents in one call. This made error handling very simple (and simpler coding for me!) but had the trade off of large allocations if huge pages were enabled. This patch is one I've had on the back burner for awhile, but we can read and write the Minidump memory sections in discrete chunks which we already do for writing to disk. I had to refactor the error handling a bit, but it remains the same. We make a best effort attempt to read as much of the memory region as possible, but fail immediately if we receive an error writing to disk. I did not add new tests for this because our existing test suite is quite good, but I did manually verify a few Minidumps couldn't read beyond the red_zone. ``` (lldb) reg read $sp rsp = 0x00007fffffffc3b0 (lldb) p/x 0x00007fffffffc3b0 - 128 (long) 0x00007fffffffc330 (lldb) memory read 0x00007fffffffc330 0x7fffffffc330: 60 c3 ff ff ff 7f 00 00 60 cd ff ff ff 7f 00 00 `.......`....... 0x7fffffffc340: 60 c3 ff ff ff 7f 00 00 65 e6 26 00 00 00 00 00 `.......e.&..... (lldb) memory read 0x00007fffffffc329 error: could not parse memory info (Success!) ``` I'm not sure how to quantify the memory improvement other than we would allocate the largest size regardless of the size. So a 2gb unreadable region would cause a 2gb allocation even if we were reading 4096 kb. Now we will take the range size or the max chunk size of 128 mb.
1 parent e3d114c commit f869d6e

File tree

4 files changed

+176
-34
lines changed

4 files changed

+176
-34
lines changed

lldb/include/lldb/Target/Process.h

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1589,6 +1589,52 @@ class Process : public std::enable_shared_from_this<Process>,
15891589
size_t ReadMemoryFromInferior(lldb::addr_t vm_addr, void *buf, size_t size,
15901590
Status &error);
15911591

1592+
// Callback definition for read Memory in chunks
1593+
//
1594+
// Status, the status returned from ReadMemoryFromInferior
1595+
// addr_t, the bytes_addr, start + bytes read so far.
1596+
// void*, pointer to the bytes read
1597+
// bytes_size, the count of bytes read for this chunk
1598+
typedef std::function<IterationAction(
1599+
lldb_private::Status &error, lldb::addr_t bytes_addr, const void *bytes,
1600+
lldb::offset_t bytes_size)>
1601+
ReadMemoryChunkCallback;
1602+
1603+
/// Read of memory from a process in discrete chunks, terminating
1604+
/// either when all bytes are read, or the supplied callback returns
1605+
/// IterationAction::Stop
1606+
///
1607+
/// \param[in] vm_addr
1608+
/// A virtual load address that indicates where to start reading
1609+
/// memory from.
1610+
///
1611+
/// \param[in] buf
1612+
/// If NULL, a buffer of \a chunk_size will be created and used for the
1613+
/// callback. If non NULL, this buffer must be at least \a chunk_size bytes
1614+
/// and will be used for storing chunked memory reads.
1615+
///
1616+
/// \param[in] chunk_size
1617+
/// The minimum size of the byte buffer, and the chunk size of memory
1618+
/// to read.
1619+
///
1620+
/// \param[in] total_size
1621+
/// The total number of bytes to read.
1622+
///
1623+
/// \param[in] callback
1624+
/// The callback to invoke when a chunk is read from memory.
1625+
///
1626+
/// \return
1627+
/// The number of bytes that were actually read into \a buf and
1628+
/// written to the provided callback.
1629+
/// If the returned number is greater than zero, yet less than \a
1630+
/// size, then this function will get called again with \a
1631+
/// vm_addr, \a buf, and \a size updated appropriately. Zero is
1632+
/// returned in the case of an error.
1633+
lldb::offset_t ReadMemoryInChunks(lldb::addr_t vm_addr, void *buf,
1634+
lldb::addr_t chunk_size,
1635+
lldb::offset_t total_size,
1636+
ReadMemoryChunkCallback callback);
1637+
15921638
/// Read a NULL terminated C string from memory
15931639
///
15941640
/// This function will read a cache page at a time until the NULL

lldb/source/Plugins/ObjectFile/Minidump/MinidumpFileBuilder.cpp

Lines changed: 78 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -969,6 +969,66 @@ Status MinidumpFileBuilder::DumpDirectories() const {
969969
return error;
970970
}
971971

972+
Status MinidumpFileBuilder::ReadWriteMemoryInChunks(
973+
lldb_private::DataBufferHeap &data_buffer,
974+
const lldb_private::CoreFileMemoryRange &range, uint64_t &bytes_read) {
975+
976+
const lldb::addr_t addr = range.range.start();
977+
const lldb::addr_t size = range.range.size();
978+
Log *log = GetLog(LLDBLog::Object);
979+
Status addDataError;
980+
Process::ReadMemoryChunkCallback callback =
981+
[&](Status &error, lldb::addr_t current_addr, const void *buf,
982+
uint64_t bytes_read) -> lldb_private::IterationAction {
983+
if (error.Fail() || bytes_read == 0) {
984+
LLDB_LOGF(log,
985+
"Failed to read memory region at: 0x%" PRIx64
986+
". Bytes read: %" PRIx64 ", error: %s",
987+
current_addr, bytes_read, error.AsCString());
988+
989+
// If we failed in a memory read, we would normally want to skip
990+
// this entire region, if we had already written to the minidump
991+
// file, we can't easily rewind that state.
992+
//
993+
// So if we do encounter an error while reading, we just return
994+
// immediately, any prior bytes read will still be included but
995+
// any bytes partially read before the error are ignored.
996+
return lldb_private::IterationAction::Stop;
997+
}
998+
999+
// Write to the minidump file with the chunk potentially flushing to
1000+
// disk.
1001+
// This error will be captured by the outer scope and is considered fatal.
1002+
// If we get an error writing to disk we can't easily guarauntee that we
1003+
// won't corrupt the minidump.
1004+
addDataError = AddData(buf, bytes_read);
1005+
if (addDataError.Fail())
1006+
return lldb_private::IterationAction::Stop;
1007+
1008+
// If we have a partial read, report it, but only if the partial read
1009+
// didn't finish reading the entire region.
1010+
if (bytes_read != data_buffer.GetByteSize() &&
1011+
current_addr + bytes_read != size) {
1012+
LLDB_LOGF(log,
1013+
"Memory region at: %" PRIx64 " partiall read 0x%" PRIx64
1014+
" bytes out of %" PRIx64 " bytes.",
1015+
current_addr, bytes_read,
1016+
data_buffer.GetByteSize() - bytes_read);
1017+
1018+
// If we've read some bytes, we stop trying to read more and return
1019+
// this best effort attempt
1020+
return lldb_private::IterationAction::Stop;
1021+
}
1022+
1023+
// No problems, keep going!
1024+
return lldb_private::IterationAction::Continue;
1025+
};
1026+
1027+
bytes_read = m_process_sp->ReadMemoryInChunks(
1028+
addr, data_buffer.GetBytes(), data_buffer.GetByteSize(), size, callback);
1029+
return addDataError;
1030+
}
1031+
9721032
static uint64_t
9731033
GetLargestRangeSize(const std::vector<CoreFileMemoryRange> &ranges) {
9741034
uint64_t max_size = 0;
@@ -987,8 +1047,8 @@ MinidumpFileBuilder::AddMemoryList_32(std::vector<CoreFileMemoryRange> &ranges,
9871047

9881048
Log *log = GetLog(LLDBLog::Object);
9891049
size_t region_index = 0;
990-
auto data_up =
991-
std::make_unique<DataBufferHeap>(GetLargestRangeSize(ranges), 0);
1050+
lldb_private::DataBufferHeap data_buffer(
1051+
std::min(GetLargestRangeSize(ranges), MAX_WRITE_CHUNK_SIZE), 0);
9921052
for (const auto &core_range : ranges) {
9931053
// Take the offset before we write.
9941054
const offset_t offset_for_data = GetCurrentDataEndOffset();
@@ -1003,18 +1063,15 @@ MinidumpFileBuilder::AddMemoryList_32(std::vector<CoreFileMemoryRange> &ranges,
10031063
++region_index;
10041064

10051065
progress.Increment(1, "Adding Memory Range " + core_range.Dump());
1006-
const size_t bytes_read =
1007-
m_process_sp->ReadMemory(addr, data_up->GetBytes(), size, error);
1008-
if (error.Fail() || bytes_read == 0) {
1009-
LLDB_LOGF(log, "Failed to read memory region. Bytes read: %zu, error: %s",
1010-
bytes_read, error.AsCString());
1011-
// Just skip sections with errors or zero bytes in 32b mode
1066+
uint64_t bytes_read = 0;
1067+
error = ReadWriteMemoryInChunks(data_buffer, core_range, bytes_read);
1068+
if (error.Fail())
1069+
return error;
1070+
1071+
// If we completely failed to read this range
1072+
// we can just omit any of the book keeping.
1073+
if (bytes_read == 0)
10121074
continue;
1013-
} else if (bytes_read != size) {
1014-
LLDB_LOGF(
1015-
log, "Memory region at: %" PRIx64 " failed to read %" PRIx64 " bytes",
1016-
addr, size);
1017-
}
10181075

10191076
MemoryDescriptor descriptor;
10201077
descriptor.StartOfMemoryRange =
@@ -1026,11 +1083,6 @@ MinidumpFileBuilder::AddMemoryList_32(std::vector<CoreFileMemoryRange> &ranges,
10261083
descriptors.push_back(descriptor);
10271084
if (m_thread_by_range_end.count(end) > 0)
10281085
m_thread_by_range_end[end].Stack = descriptor;
1029-
1030-
// Add the data to the buffer, flush as needed.
1031-
error = AddData(data_up->GetBytes(), bytes_read);
1032-
if (error.Fail())
1033-
return error;
10341086
}
10351087

10361088
// Add a directory that references this list
@@ -1088,6 +1140,8 @@ MinidumpFileBuilder::AddMemoryList_64(std::vector<CoreFileMemoryRange> &ranges,
10881140
list_header.BaseRVA = memory_ranges_base_rva;
10891141
m_data.AppendData(&list_header, sizeof(llvm::minidump::Memory64ListHeader));
10901142

1143+
lldb_private::DataBufferHeap data_buffer(
1144+
std::min(GetLargestRangeSize(ranges), MAX_WRITE_CHUNK_SIZE), 0);
10911145
bool cleanup_required = false;
10921146
std::vector<MemoryDescriptor_64> descriptors;
10931147
// Enumerate the ranges and create the memory descriptors so we can append
@@ -1106,8 +1160,6 @@ MinidumpFileBuilder::AddMemoryList_64(std::vector<CoreFileMemoryRange> &ranges,
11061160

11071161
Log *log = GetLog(LLDBLog::Object);
11081162
size_t region_index = 0;
1109-
auto data_up =
1110-
std::make_unique<DataBufferHeap>(GetLargestRangeSize(ranges), 0);
11111163
for (const auto &core_range : ranges) {
11121164
const addr_t addr = core_range.range.start();
11131165
const addr_t size = core_range.range.size();
@@ -1120,27 +1172,19 @@ MinidumpFileBuilder::AddMemoryList_64(std::vector<CoreFileMemoryRange> &ranges,
11201172
++region_index;
11211173

11221174
progress.Increment(1, "Adding Memory Range " + core_range.Dump());
1123-
const size_t bytes_read =
1124-
m_process_sp->ReadMemory(addr, data_up->GetBytes(), size, error);
1125-
if (error.Fail()) {
1126-
LLDB_LOGF(log, "Failed to read memory region. Bytes read: %zu, error: %s",
1127-
bytes_read, error.AsCString());
1128-
error.Clear();
1175+
uint64_t bytes_read = 0;
1176+
error = ReadWriteMemoryInChunks(data_buffer, core_range, bytes_read);
1177+
if (error.Fail())
1178+
return error;
1179+
1180+
if (bytes_read == 0) {
11291181
cleanup_required = true;
11301182
descriptors[region_index].DataSize = 0;
11311183
}
11321184
if (bytes_read != size) {
1133-
LLDB_LOGF(
1134-
log, "Memory region at: %" PRIx64 " failed to read %" PRIx64 " bytes",
1135-
addr, size);
11361185
cleanup_required = true;
11371186
descriptors[region_index].DataSize = bytes_read;
11381187
}
1139-
1140-
// Add the data to the buffer, flush as needed.
1141-
error = AddData(data_up->GetBytes(), bytes_read);
1142-
if (error.Fail())
1143-
return error;
11441188
}
11451189

11461190
// Early return if there is no cleanup needed.

lldb/source/Plugins/ObjectFile/Minidump/MinidumpFileBuilder.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,14 @@ class MinidumpFileBuilder {
142142
lldb_private::Status AddDirectory(llvm::minidump::StreamType type,
143143
uint64_t stream_size);
144144
lldb::offset_t GetCurrentDataEndOffset() const;
145+
146+
// Read a memory region from the process and write it to the file
147+
// in fixed size chunks.
148+
lldb_private::Status
149+
ReadWriteMemoryInChunks(lldb_private::DataBufferHeap &data_buffer,
150+
const lldb_private::CoreFileMemoryRange &range,
151+
uint64_t &bytes_read);
152+
145153
// Stores directories to fill in later
146154
std::vector<llvm::minidump::Directory> m_directories;
147155
// When we write off the threads for the first time, we need to clean them up

lldb/source/Target/Process.cpp

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2184,6 +2184,50 @@ size_t Process::ReadMemoryFromInferior(addr_t addr, void *buf, size_t size,
21842184
return bytes_read;
21852185
}
21862186

2187+
lldb::offset_t Process::ReadMemoryInChunks(lldb::addr_t vm_addr, void *buf,
2188+
lldb::addr_t chunk_size,
2189+
lldb::offset_t size,
2190+
ReadMemoryChunkCallback callback) {
2191+
// Safety check to prevent an infinite loop.
2192+
if (chunk_size == 0)
2193+
return 0;
2194+
2195+
// Buffer for when a NULL buf is provided, initialized
2196+
// to 0 bytes, we set it to chunk_size and then replace buf
2197+
// with the new buffer.
2198+
DataBufferHeap data_buffer;
2199+
if (!buf) {
2200+
data_buffer.SetByteSize(chunk_size);
2201+
buf = data_buffer.GetBytes();
2202+
}
2203+
2204+
uint64_t bytes_remaining = size;
2205+
uint64_t bytes_read = 0;
2206+
Status error;
2207+
while (bytes_remaining > 0) {
2208+
// Get the next read chunk size as the minimum of the remaining bytes and
2209+
// the write chunk max size.
2210+
const lldb::addr_t bytes_to_read = std::min(bytes_remaining, chunk_size);
2211+
const lldb::addr_t current_addr = vm_addr + bytes_read;
2212+
const lldb::addr_t bytes_read_for_chunk =
2213+
ReadMemoryFromInferior(current_addr, buf, bytes_to_read, error);
2214+
2215+
bytes_read += bytes_read_for_chunk;
2216+
// If the bytes read in this chunk would cause us to overflow, something
2217+
// went wrong and we should fail fast.
2218+
if (bytes_read_for_chunk > bytes_remaining)
2219+
return 0;
2220+
else
2221+
bytes_remaining -= bytes_read_for_chunk;
2222+
2223+
if (callback(error, current_addr, buf, bytes_read_for_chunk) ==
2224+
IterationAction::Stop)
2225+
break;
2226+
}
2227+
2228+
return bytes_read;
2229+
}
2230+
21872231
uint64_t Process::ReadUnsignedIntegerFromMemory(lldb::addr_t vm_addr,
21882232
size_t integer_byte_size,
21892233
uint64_t fail_value,

0 commit comments

Comments
 (0)