Skip to content

Commit 181cc75

Browse files
authored
[lldb/linux] Make truncated reads work (#106532)
Previously, we were returning an error if we couldn't read the whole region. This doesn't matter most of the time, because lldb caches memory reads, and in that process it aligns them to cache line boundaries. As (LLDB) cache lines are smaller than pages, the reads are unlikely to cross page boundaries. Nonetheless, this can cause a problem for large reads (which bypass the cache), where we're unable to read anything even if just a single byte of the memory is unreadable. This patch fixes the lldb-server to do that, and also changes the linux implementation, to reuse any partial results it got from the process_vm_readv call (to avoid having to re-read everything again using ptrace, only to find that it stopped at the same place). This matches debugserver behavior. It is also consistent with the gdb remote protocol documentation, but -- notably -- not with actual gdbserver behavior (which returns errors instead of partial results). We filed a [clarification bug](https://sourceware.org/bugzilla/show_bug.cgi?id=24751) several years ago. Though we did not really reach a conclusion there, I think this is the most logical behavior. The associated test does not currently pass on windows, because the windows memory read APIs don't support partial reads (I have a WIP patch to work around that).
1 parent 0c0bac9 commit 181cc75

File tree

5 files changed

+177
-38
lines changed

5 files changed

+177
-38
lines changed

lldb/source/Plugins/Process/Linux/NativeProcessLinux.cpp

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1641,6 +1641,10 @@ NativeProcessLinux::GetSoftwareBreakpointTrapOpcode(size_t size_hint) {
16411641

16421642
Status NativeProcessLinux::ReadMemory(lldb::addr_t addr, void *buf, size_t size,
16431643
size_t &bytes_read) {
1644+
Log *log = GetLog(POSIXLog::Memory);
1645+
LLDB_LOG(log, "addr = {0}, buf = {1}, size = {2}", addr, buf, size);
1646+
1647+
bytes_read = 0;
16441648
if (ProcessVmReadvSupported()) {
16451649
// The process_vm_readv path is about 50 times faster than ptrace api. We
16461650
// want to use this syscall if it is supported.
@@ -1651,44 +1655,37 @@ Status NativeProcessLinux::ReadMemory(lldb::addr_t addr, void *buf, size_t size,
16511655
remote_iov.iov_base = reinterpret_cast<void *>(addr);
16521656
remote_iov.iov_len = size;
16531657

1654-
bytes_read = process_vm_readv(GetCurrentThreadID(), &local_iov, 1,
1655-
&remote_iov, 1, 0);
1656-
const bool success = bytes_read == size;
1658+
ssize_t read_result = process_vm_readv(GetCurrentThreadID(), &local_iov, 1,
1659+
&remote_iov, 1, 0);
1660+
int error = 0;
1661+
if (read_result < 0)
1662+
error = errno;
1663+
else
1664+
bytes_read = read_result;
16571665

1658-
Log *log = GetLog(POSIXLog::Process);
16591666
LLDB_LOG(log,
1660-
"using process_vm_readv to read {0} bytes from inferior "
1661-
"address {1:x}: {2}",
1662-
size, addr, success ? "Success" : llvm::sys::StrError(errno));
1663-
1664-
if (success)
1665-
return Status();
1666-
// else the call failed for some reason, let's retry the read using ptrace
1667-
// api.
1667+
"process_vm_readv({0}, [iovec({1}, {2})], [iovec({3:x}, {2})], 1, "
1668+
"0) => {4} ({5})",
1669+
GetCurrentThreadID(), buf, size, addr, read_result,
1670+
error > 0 ? llvm::sys::StrError(errno) : "sucesss");
16681671
}
16691672

16701673
unsigned char *dst = static_cast<unsigned char *>(buf);
16711674
size_t remainder;
16721675
long data;
16731676

1674-
Log *log = GetLog(POSIXLog::Memory);
1675-
LLDB_LOG(log, "addr = {0}, buf = {1}, size = {2}", addr, buf, size);
1676-
1677-
for (bytes_read = 0; bytes_read < size; bytes_read += remainder) {
1677+
for (; bytes_read < size; bytes_read += remainder) {
16781678
Status error = NativeProcessLinux::PtraceWrapper(
1679-
PTRACE_PEEKDATA, GetCurrentThreadID(), (void *)addr, nullptr, 0, &data);
1679+
PTRACE_PEEKDATA, GetCurrentThreadID(),
1680+
reinterpret_cast<void *>(addr + bytes_read), nullptr, 0, &data);
16801681
if (error.Fail())
16811682
return error;
16821683

16831684
remainder = size - bytes_read;
16841685
remainder = remainder > k_ptrace_word_size ? k_ptrace_word_size : remainder;
16851686

16861687
// Copy the data into our buffer
1687-
memcpy(dst, &data, remainder);
1688-
1689-
LLDB_LOG(log, "[{0:x}]:{1:x}", addr, data);
1690-
addr += k_ptrace_word_size;
1691-
dst += k_ptrace_word_size;
1688+
memcpy(dst + bytes_read, &data, remainder);
16921689
}
16931690
return Status();
16941691
}

lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.cpp

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2526,28 +2526,18 @@ GDBRemoteCommunicationServerLLGS::Handle_memory_read(
25262526
size_t bytes_read = 0;
25272527
Status error = m_current_process->ReadMemoryWithoutTrap(
25282528
read_addr, &buf[0], byte_count, bytes_read);
2529-
if (error.Fail()) {
2530-
LLDB_LOGF(log,
2531-
"GDBRemoteCommunicationServerLLGS::%s pid %" PRIu64
2532-
" mem 0x%" PRIx64 ": failed to read. Error: %s",
2533-
__FUNCTION__, m_current_process->GetID(), read_addr,
2534-
error.AsCString());
2535-
return SendErrorResponse(0x08);
2536-
}
2537-
2538-
if (bytes_read == 0) {
2539-
LLDB_LOGF(log,
2540-
"GDBRemoteCommunicationServerLLGS::%s pid %" PRIu64
2541-
" mem 0x%" PRIx64 ": read 0 of %" PRIu64 " requested bytes",
2542-
__FUNCTION__, m_current_process->GetID(), read_addr, byte_count);
2529+
LLDB_LOG(
2530+
log,
2531+
"ReadMemoryWithoutTrap({0}) read {1} of {2} requested bytes (error: {3})",
2532+
read_addr, byte_count, bytes_read, error);
2533+
if (bytes_read == 0)
25432534
return SendErrorResponse(0x08);
2544-
}
25452535

25462536
StreamGDBRemote response;
25472537
packet.SetFilePos(0);
25482538
char kind = packet.GetChar('?');
25492539
if (kind == 'x')
2550-
response.PutEscapedBytes(buf.data(), byte_count);
2540+
response.PutEscapedBytes(buf.data(), bytes_read);
25512541
else {
25522542
assert(kind == 'm');
25532543
for (size_t i = 0; i < bytes_read; ++i)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
CXX_SOURCES := main.cpp
2+
3+
include Makefile.rules
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"""
2+
Test the memory commands operating on memory regions with holes (inaccessible
3+
pages)
4+
"""
5+
6+
import lldb
7+
from lldbsuite.test.lldbtest import *
8+
import lldbsuite.test.lldbutil as lldbutil
9+
from lldbsuite.test.decorators import *
10+
11+
12+
class MemoryHolesTestCase(TestBase):
13+
NO_DEBUG_INFO_TESTCASE = True
14+
15+
def setUp(self):
16+
super().setUp()
17+
# Find the line number to break inside main().
18+
self.line = line_number("main.cpp", "// break here")
19+
20+
def _prepare_inferior(self):
21+
self.build()
22+
exe = self.getBuildArtifact("a.out")
23+
self.runCmd("file " + exe, CURRENT_EXECUTABLE_SET)
24+
25+
# Break in main() after the variables are assigned values.
26+
lldbutil.run_break_set_by_file_and_line(
27+
self, "main.cpp", self.line, num_expected_locations=1, loc_exact=True
28+
)
29+
30+
self.runCmd("run", RUN_SUCCEEDED)
31+
32+
# The stop reason of the thread should be breakpoint.
33+
self.expect(
34+
"thread list",
35+
STOPPED_DUE_TO_BREAKPOINT,
36+
substrs=["stopped", "stop reason = breakpoint"],
37+
)
38+
39+
# The breakpoint should have a hit count of 1.
40+
lldbutil.check_breakpoint(self, bpno=1, expected_hit_count=1)
41+
42+
# Avoid the expression evaluator, as it can allocate allocate memory
43+
# inside the holes we've deliberately left empty.
44+
self.memory = self.frame().FindVariable("mem_with_holes").GetValueAsUnsigned()
45+
self.pagesize = self.frame().FindVariable("pagesize").GetValueAsUnsigned()
46+
positions = self.frame().FindVariable("positions")
47+
self.positions = [
48+
positions.GetChildAtIndex(i).GetValueAsUnsigned()
49+
for i in range(positions.GetNumChildren())
50+
]
51+
self.assertEqual(len(self.positions), 5)
52+
53+
@expectedFailureWindows
54+
def test_memory_read(self):
55+
self._prepare_inferior()
56+
57+
error = lldb.SBError()
58+
content = self.process().ReadMemory(self.memory, 2 * self.pagesize, error)
59+
self.assertEqual(len(content), self.pagesize)
60+
self.assertEqual(content[0:7], b"needle\0")
61+
self.assertTrue(error.Fail())
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
#include <algorithm>
2+
#include <cstdint>
3+
#include <cstdio>
4+
#include <cstdlib>
5+
#include <cstring>
6+
#include <iostream>
7+
8+
constexpr size_t num_pages = 7;
9+
constexpr size_t accessible_pages[] = {0, 2, 4, 6};
10+
11+
bool is_accessible(size_t page) {
12+
return std::find(std::begin(accessible_pages), std::end(accessible_pages),
13+
page) != std::end(accessible_pages);
14+
}
15+
16+
// allocate_memory_with_holes returns a pointer to `num_pages` pages of memory,
17+
// where some of the pages are inaccessible (even to debugging APIs). We use
18+
// this to test lldb's ability to skip over inaccessible blocks.
19+
#ifdef _WIN32
20+
#include "Windows.h"
21+
22+
int getpagesize() {
23+
SYSTEM_INFO system_info;
24+
GetSystemInfo(&system_info);
25+
return system_info.dwPageSize;
26+
}
27+
28+
char *allocate_memory_with_holes() {
29+
int pagesize = getpagesize();
30+
void *mem =
31+
VirtualAlloc(nullptr, num_pages * pagesize, MEM_RESERVE, PAGE_NOACCESS);
32+
if (!mem) {
33+
std::cerr << std::system_category().message(GetLastError()) << std::endl;
34+
exit(1);
35+
}
36+
char *bytes = static_cast<char *>(mem);
37+
for (size_t page = 0; page < num_pages; ++page) {
38+
if (!is_accessible(page))
39+
continue;
40+
if (!VirtualAlloc(bytes + page * pagesize, pagesize, MEM_COMMIT,
41+
PAGE_READWRITE)) {
42+
std::cerr << std::system_category().message(GetLastError()) << std::endl;
43+
exit(1);
44+
}
45+
}
46+
return bytes;
47+
}
48+
#else
49+
#include "sys/mman.h"
50+
#include "unistd.h"
51+
52+
char *allocate_memory_with_holes() {
53+
int pagesize = getpagesize();
54+
void *mem = mmap(nullptr, num_pages * pagesize, PROT_READ | PROT_WRITE,
55+
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
56+
if (mem == MAP_FAILED) {
57+
perror("mmap");
58+
exit(1);
59+
}
60+
char *bytes = static_cast<char *>(mem);
61+
for (size_t page = 0; page < num_pages; ++page) {
62+
if (is_accessible(page))
63+
continue;
64+
if (munmap(bytes + page * pagesize, pagesize) != 0) {
65+
perror("munmap");
66+
exit(1);
67+
}
68+
}
69+
return bytes;
70+
}
71+
#endif
72+
73+
int main(int argc, char const *argv[]) {
74+
char *mem_with_holes = allocate_memory_with_holes();
75+
int pagesize = getpagesize();
76+
char *positions[] = {
77+
mem_with_holes, // Beginning of memory
78+
mem_with_holes + 2 * pagesize, // After a hole
79+
mem_with_holes + 2 * pagesize +
80+
pagesize / 2, // Middle of a block, after an existing match.
81+
mem_with_holes + 5 * pagesize - 7, // End of a block
82+
mem_with_holes + 7 * pagesize - 7, // End of memory
83+
};
84+
for (char *p : positions)
85+
strcpy(p, "needle");
86+
87+
return 0; // break here
88+
}

0 commit comments

Comments
 (0)