[Lldb-commits] [lldb] [lldb][debugserver] Expedite the stopped frame's stack memory in jThreadsInfo (PR #212706)
Yao Qi via lldb-commits
lldb-commits at lists.llvm.org
Fri Jul 31 03:55:51 PDT 2026
https://github.com/qiyao updated https://github.com/llvm/llvm-project/pull/212706
>From b5e1a0fcfa3935cbad087603035e6db6180b5dc1 Mon Sep 17 00:00:00 2001
From: Yao Qi <yao_qi at apple.com>
Date: Thu, 25 Jun 2026 16:57:15 +0100
Subject: [PATCH 1/4] [lldb][debugserver] Expedite the stopped frame's stack
memory in jThreadsInfo
`ReadStackMemory` only expedites the frame-pointer backchain, which is
enough to unwind the stack but not to read a frame's locals: a local's
bytes live between `$sp` and `$fp`, addresses that are never sent up. As
a result, examining the stopped frame's locals at a public stop generated
one memory-read packets.
Add `ReadFrameZeroStackMemory`, which expedites the innermost frame's
stack memory. Rather than a single `[$sp, $fp)` block (which would have
to give up entirely on large frames), it covers where variables actually
live using two anchored windows: `[$fp - k_expedite_stack_window, $fp)`
for locals/spills and `[$sp, $sp + k_expedite_stack_window)` for params.
k_expedite_stack_window = 512 is sized to cover the common case
(variables sit within a few hundred bytes of their anchor) while bounding
the per-frame cost to 2*k_expedite_stack_window bytes regardless of frame
size.
For a small frame (`$fp - $sp <= 2*k_expedite_stack_window`) the two
windows would overlap, so a single contiguous `[$sp, $fp)` chunk is emitted
instead, giving full coverage with no gap. The `$fp` side always stops at
`$fp` exclusive so it does not duplicate the saved `{fp, lr}` pair already
covered by the backchain expedite. Each window is emitted as its own
chunk because lldb's L1 cache only serves a read fully contained in one
expedited chunk.
A debugger reads a frame's locals only for the selected frame
of the thread that stopped, so this frame-0 window is emitted for the
current thread only rather than every thread, avoiding stop-reply bloat
proportional to thread count.
`GetJSONThreadsInfo` now builds the `"memory"` array from both sources
and emits it whenever either produced an entry. Add
`JSONGenerator::Array::empty` to support that check.
---
.../TestExpeditedStackMemory.py | 72 ++++++++------
lldb/tools/debugserver/source/JSONGenerator.h | 2 +
lldb/tools/debugserver/source/RNBRemote.cpp | 95 ++++++++++++++++++-
3 files changed, 138 insertions(+), 31 deletions(-)
diff --git a/lldb/test/API/macosx/expedited-stack-memory/TestExpeditedStackMemory.py b/lldb/test/API/macosx/expedited-stack-memory/TestExpeditedStackMemory.py
index e8c8076699af7..e76750c13aa25 100644
--- a/lldb/test/API/macosx/expedited-stack-memory/TestExpeditedStackMemory.py
+++ b/lldb/test/API/macosx/expedited-stack-memory/TestExpeditedStackMemory.py
@@ -3,19 +3,21 @@
public stop.
On Darwin, debugserver expedites the frame-pointer backchain (up to 256 frames,
-for every thread) in the jThreadsInfo response at a public stop, and lldb seeds
-those bytes into its memory cache. Consequences exercised here:
+for every thread) and the stopped frame's stack memory in the jThreadsInfo
+response at a public stop, and lldb seeds those bytes into its memory cache.
+Consequences exercised here:
* A backtrace (GetNumFrames() / GetFrameAtIndex() for every frame) is
satisfied entirely from the expedited/cached backchain and sends no packets.
With the cache disabled it must read the backchain frame by frame, which
confirms the test is really exercising the unwinder's memory reads.
- * Examining frame local variables the way an IDE does is NOT covered by the
- expedite: the values live at addresses that were never sent up, so reading
- them produces memory-read packets. This is checked two ways, mirroring
- an IDE: examining only the selected frame's locals (frame 0, what a
- variables view does on a stop) and examining every frame's locals (the
+ * Examining the stopped frame's locals is covered by the stack expedite, so
+ it sends no stack memory-read packets; heap buffers behind pointers are not
+ on the stack and are still read from the stub. Examining deeper frames'
+ locals does produce stack memory-read packets. This is checked two ways,
+ mirroring an IDE: examining only the selected frame's locals (frame 0, what
+ a variables view does on a stop) and examining every frame's locals (the
"view all frames" case).
"""
@@ -50,24 +52,31 @@ def test_memory_reads_when_examining_frame0_locals(self):
"""Model an IDE stop: walk the whole stack (a backtrace / debug
navigator) but examine the locals of only the selected frame 0.
Frame 0 (func_e in main.c) carries scalar, aggregate, and
- pointer-to-heap locals, so examining it alone reads both stack and
- heap memory."""
- self.check_memory_reads_when_examining_locals(examine_all_frames=False)
+ pointer-to-heap locals. Its stack is expedited, so examining it
+ reads heap memory but no stack memory."""
+ self.check_memory_reads_when_examining_locals(
+ examine_all_frames=False, expect_stack_reads=False
+ )
@skipUnlessDarwin
def test_memory_reads_when_examining_all_frames_locals(self):
"""Model "view all frames": walk the whole stack and examine every
- frame's locals. This reads the same variety of memory across several
- frames."""
- self.check_memory_reads_when_examining_locals(examine_all_frames=True)
+ frame's locals. Only the stopped frame's stack is expedited, so the
+ deeper frames' locals still read stack memory."""
+ self.check_memory_reads_when_examining_locals(
+ examine_all_frames=True, expect_stack_reads=True
+ )
- def check_memory_reads_when_examining_locals(self, examine_all_frames):
- """Examining frame locals reads value memory that is not expedited.
- Classify those reads into stack vs heap and check the counts.
+ def check_memory_reads_when_examining_locals(
+ self, examine_all_frames, expect_stack_reads
+ ):
+ """Examining frame locals reads value memory; classify those reads into
+ stack vs heap and check the counts.
- The frame-pointer backchain is expedited, but the locals' *values* are
- not, so both stack-resident locals and heap buffers behind pointers are
- read from the stub today.
+ The frame-pointer backchain and the stopped frame's stack are expedited,
+ so frame 0's stack-resident locals are served from the cache while heap
+ buffers behind pointers (and deeper frames' locals) are read from the
+ stub.
We have two regions and ask the process which one each read falls in:
* the stack region: whichever region the stack pointer points into.
@@ -131,15 +140,22 @@ def per_frame(idx, frame):
)
)
- # Examining locals reads both stack and heap memory.
- self.assertGreater(
- len(stack_reads),
- 0,
- "expected stack memory reads while examining stack-resident "
- "locals.\n" + breakdown,
- )
- # Heap reads come from disclosing the pointer-to-heap local; a stack
- # expedite would NOT remove these.
+ if expect_stack_reads:
+ # Deeper frames' stacks are not expedited.
+ self.assertGreater(
+ len(stack_reads),
+ 0,
+ "expected stack memory reads while examining deeper frames' "
+ "stack-resident locals.\n" + breakdown,
+ )
+ else:
+ # The stopped frame's stack is expedited.
+ self.assertEqual(
+ len(stack_reads),
+ 0,
+ "expected NO stack memory reads for frame 0 (its stack is "
+ "expedited in jThreadsInfo).\n" + breakdown,
+ )
self.assertGreater(
len(heap_reads),
0,
diff --git a/lldb/tools/debugserver/source/JSONGenerator.h b/lldb/tools/debugserver/source/JSONGenerator.h
index b545a6e29a767..114de9dd992af 100644
--- a/lldb/tools/debugserver/source/JSONGenerator.h
+++ b/lldb/tools/debugserver/source/JSONGenerator.h
@@ -127,6 +127,8 @@ class JSONGenerator {
void AddItem(ObjectSP item) { m_items.push_back(item); }
+ bool empty() const { return m_items.empty(); }
+
void AddIntegerItem(uint64_t value) {
AddItem(ObjectSP(new Integer(value)));
}
diff --git a/lldb/tools/debugserver/source/RNBRemote.cpp b/lldb/tools/debugserver/source/RNBRemote.cpp
index 91d611f9ecbc5..a3ca5a73ddfc2 100644
--- a/lldb/tools/debugserver/source/RNBRemote.cpp
+++ b/lldb/tools/debugserver/source/RNBRemote.cpp
@@ -2733,6 +2733,76 @@ static void ReadStackMemory(nub_process_t pid, nub_thread_t tid,
}
}
+// The size of each per-side stack window we expedite. 512 is sized to cover
+// the common case (locals and params sit within a few hundred bytes) while
+// bounding the per-frame cost to 1K bytes regardless of frame size.
+static const nub_size_t k_expedite_stack_window = 512;
+
+// A single contiguous chunk of expedited memory.
+struct ExpeditedMemory {
+ nub_addr_t addr;
+ std::vector<uint8_t> bytes;
+};
+
+// Read the innermost frame's stack memory so that examining its local variables
+// at a public stop is served from the expedited cache instead of generating one
+// memory-read packet.
+//
+// We produce either:
+//
+// - one chunk [$sp, $fp) for a small frame ($fp - $sp <=
+// 2*k_expedite_stack_window), which covers the whole frame with no gap, or
+//
+// - two chunks [$sp, $sp + k_expedite_stack_window) and [$fp -
+// k_expedite_stack_window, $fp) for a large frame, covering the params near
+// $sp and the locals near $fp while leaving the (rarely-interesting) middle
+// spill area out so the cost stays bounded.
+static void ReadFrameZeroStackMemory(nub_process_t pid, nub_thread_t tid,
+ std::vector<ExpeditedMemory> &chunks) {
+ chunks.clear();
+ std::unique_ptr<DNBRegisterValue> sp_value =
+ std::make_unique<DNBRegisterValue>();
+ std::unique_ptr<DNBRegisterValue> fp_value =
+ std::make_unique<DNBRegisterValue>();
+ if (!DNBThreadGetRegisterValueByID(pid, tid, REGISTER_SET_GENERIC,
+ GENERIC_REGNUM_SP, sp_value.get()) ||
+ !DNBThreadGetRegisterValueByID(pid, tid, REGISTER_SET_GENERIC,
+ GENERIC_REGNUM_FP, fp_value.get()))
+ return;
+
+ const nub_size_t ptr_size = sp_value->info.size;
+ uint64_t sp =
+ (ptr_size == 4) ? sp_value->value.uint32 : sp_value->value.uint64;
+ uint64_t fp =
+ (ptr_size == 4) ? fp_value->value.uint32 : fp_value->value.uint64;
+
+ // The stack grows down, so a normal frame has sp < fp. Bail on a leaf/empty
+ // frame (sp == fp) or anything that doesn't look like a frame.
+ if (sp == 0 || fp <= sp)
+ return;
+
+ auto read_range = [&](uint64_t start, uint64_t length) {
+ std::vector<uint8_t> buf(length);
+ if (DNBProcessMemoryRead(pid, start, length, buf.data()) != length)
+ return;
+ chunks.push_back({start, std::move(buf)});
+ };
+
+ const uint64_t frame_size = fp - sp;
+ const nub_size_t window = k_expedite_stack_window;
+
+ if (frame_size <= 2 * window) {
+ // Small frame: cover the whole frame as a single contiguous chunk [sp, fp).
+ read_range(sp, frame_size);
+ return;
+ }
+
+ // Large frame: cover the params near $sp and the locals near $fp with two
+ // bounded windows, leaving the middle spill area out to keep the cost capped.
+ read_range(sp, window); // [sp, sp + WINDOW)
+ read_range(fp - window, window); // [fp - WINDOW, fp)
+}
+
rnb_err_t RNBRemote::SendStopReplyPacketForThread(nub_thread_t tid) {
const nub_process_t pid = m_ctx.ProcessID();
if (pid == INVALID_NUB_PROCESS)
@@ -5866,9 +5936,10 @@ RNBRemote::GetJSONThreadsInfo(bool threads_with_valid_stop_info_only) {
// frame pointer chain.
StackMemoryMap stack_mmap;
ReadStackMemory(pid, tid, stack_mmap);
- if (!stack_mmap.empty()) {
- JSONGenerator::ArraySP memory_array_sp(new JSONGenerator::Array());
+ JSONGenerator::ArraySP memory_array_sp(new JSONGenerator::Array());
+
+ if (!stack_mmap.empty()) {
for (const auto &stack_memory : stack_mmap) {
JSONGenerator::DictionarySP stack_memory_sp(
new JSONGenerator::Dictionary());
@@ -5877,9 +5948,27 @@ RNBRemote::GetJSONThreadsInfo(bool threads_with_valid_stop_info_only) {
"bytes", stack_memory.second.bytes, stack_memory.second.length);
memory_array_sp->AddItem(stack_memory_sp);
}
- thread_dict_sp->AddItem("memory", memory_array_sp);
}
+ // Also expedite the innermost frame's stack memory of the thread that
+ // stopped.
+ if (tid == DNBProcessGetCurrentThread(pid)) {
+ std::vector<ExpeditedMemory> frame_zero_chunks;
+ ReadFrameZeroStackMemory(pid, tid, frame_zero_chunks);
+
+ for (const auto &chunk : frame_zero_chunks) {
+ JSONGenerator::DictionarySP frame_zero_sp(
+ new JSONGenerator::Dictionary());
+ frame_zero_sp->AddIntegerItem("address", chunk.addr);
+ frame_zero_sp->AddBytesAsHexASCIIString("bytes", chunk.bytes.data(),
+ chunk.bytes.size());
+ memory_array_sp->AddItem(frame_zero_sp);
+ }
+ }
+
+ if (!memory_array_sp->empty())
+ thread_dict_sp->AddItem("memory", memory_array_sp);
+
std::vector<uint64_t> added_binaries;
JSONGenerator::ObjectSP detailed_binary_infos;
>From 38bde0caa503fc4fe970ae58368e9ba47cd75e16 Mon Sep 17 00:00:00 2001
From: Yao Qi <yao_qi at apple.com>
Date: Thu, 30 Jul 2026 17:48:38 +0100
Subject: [PATCH 2/4] fixup! [lldb][debugserver] Expedite the stopped frame's
stack memory in jThreadsInfo
---
lldb/tools/debugserver/source/RNBRemote.cpp | 35 ++++++++++-----------
1 file changed, 16 insertions(+), 19 deletions(-)
diff --git a/lldb/tools/debugserver/source/RNBRemote.cpp b/lldb/tools/debugserver/source/RNBRemote.cpp
index a3ca5a73ddfc2..a92a3a3eab14a 100644
--- a/lldb/tools/debugserver/source/RNBRemote.cpp
+++ b/lldb/tools/debugserver/source/RNBRemote.cpp
@@ -2757,29 +2757,25 @@ struct ExpeditedMemory {
// k_expedite_stack_window, $fp) for a large frame, covering the params near
// $sp and the locals near $fp while leaving the (rarely-interesting) middle
// spill area out so the cost stays bounded.
-static void ReadFrameZeroStackMemory(nub_process_t pid, nub_thread_t tid,
- std::vector<ExpeditedMemory> &chunks) {
- chunks.clear();
- std::unique_ptr<DNBRegisterValue> sp_value =
- std::make_unique<DNBRegisterValue>();
- std::unique_ptr<DNBRegisterValue> fp_value =
- std::make_unique<DNBRegisterValue>();
+static std::vector<ExpeditedMemory> ReadFrameZeroStackMemory(nub_process_t pid,
+ nub_thread_t tid) {
+ std::vector<ExpeditedMemory> chunks;
+ DNBRegisterValue sp_value;
+ DNBRegisterValue fp_value;
if (!DNBThreadGetRegisterValueByID(pid, tid, REGISTER_SET_GENERIC,
- GENERIC_REGNUM_SP, sp_value.get()) ||
+ GENERIC_REGNUM_SP, &sp_value) ||
!DNBThreadGetRegisterValueByID(pid, tid, REGISTER_SET_GENERIC,
- GENERIC_REGNUM_FP, fp_value.get()))
- return;
+ GENERIC_REGNUM_FP, &fp_value))
+ return chunks;
- const nub_size_t ptr_size = sp_value->info.size;
- uint64_t sp =
- (ptr_size == 4) ? sp_value->value.uint32 : sp_value->value.uint64;
- uint64_t fp =
- (ptr_size == 4) ? fp_value->value.uint32 : fp_value->value.uint64;
+ const nub_size_t ptr_size = sp_value.info.size;
+ uint64_t sp = (ptr_size == 4) ? sp_value.value.uint32 : sp_value.value.uint64;
+ uint64_t fp = (ptr_size == 4) ? fp_value.value.uint32 : fp_value.value.uint64;
// The stack grows down, so a normal frame has sp < fp. Bail on a leaf/empty
// frame (sp == fp) or anything that doesn't look like a frame.
if (sp == 0 || fp <= sp)
- return;
+ return chunks;
auto read_range = [&](uint64_t start, uint64_t length) {
std::vector<uint8_t> buf(length);
@@ -2794,13 +2790,14 @@ static void ReadFrameZeroStackMemory(nub_process_t pid, nub_thread_t tid,
if (frame_size <= 2 * window) {
// Small frame: cover the whole frame as a single contiguous chunk [sp, fp).
read_range(sp, frame_size);
- return;
+ return chunks;
}
// Large frame: cover the params near $sp and the locals near $fp with two
// bounded windows, leaving the middle spill area out to keep the cost capped.
read_range(sp, window); // [sp, sp + WINDOW)
read_range(fp - window, window); // [fp - WINDOW, fp)
+ return chunks;
}
rnb_err_t RNBRemote::SendStopReplyPacketForThread(nub_thread_t tid) {
@@ -5953,8 +5950,8 @@ RNBRemote::GetJSONThreadsInfo(bool threads_with_valid_stop_info_only) {
// Also expedite the innermost frame's stack memory of the thread that
// stopped.
if (tid == DNBProcessGetCurrentThread(pid)) {
- std::vector<ExpeditedMemory> frame_zero_chunks;
- ReadFrameZeroStackMemory(pid, tid, frame_zero_chunks);
+ std::vector<ExpeditedMemory> frame_zero_chunks =
+ ReadFrameZeroStackMemory(pid, tid);
for (const auto &chunk : frame_zero_chunks) {
JSONGenerator::DictionarySP frame_zero_sp(
>From ec16b2bc20d4d8dcb4a471bfab39532f993675db Mon Sep 17 00:00:00 2001
From: Yao Qi <yao_qi at apple.com>
Date: Fri, 31 Jul 2026 11:39:06 +0100
Subject: [PATCH 3/4] fixup! [lldb][debugserver] Expedite the stopped frame's
stack memory in jThreadsInfo
---
.../TestExpeditedStackMemory.py | 6 +-
.../API/macosx/expedited-stack-memory/main.c | 21 ++++-
lldb/tools/debugserver/source/RNBRemote.cpp | 81 ++++++++++++-------
3 files changed, 70 insertions(+), 38 deletions(-)
diff --git a/lldb/test/API/macosx/expedited-stack-memory/TestExpeditedStackMemory.py b/lldb/test/API/macosx/expedited-stack-memory/TestExpeditedStackMemory.py
index e76750c13aa25..7e8d25e742f79 100644
--- a/lldb/test/API/macosx/expedited-stack-memory/TestExpeditedStackMemory.py
+++ b/lldb/test/API/macosx/expedited-stack-memory/TestExpeditedStackMemory.py
@@ -51,9 +51,9 @@ def test_memory_reads_during_backtrace_without_cache(self):
def test_memory_reads_when_examining_frame0_locals(self):
"""Model an IDE stop: walk the whole stack (a backtrace / debug
navigator) but examine the locals of only the selected frame 0.
- Frame 0 (func_e in main.c) carries scalar, aggregate, and
- pointer-to-heap locals. Its stack is expedited, so examining it
- reads heap memory but no stack memory."""
+ Frame 0 (func_e in main.c) carries scalar, aggregate,
+ pointer-to-heap, and stack-passed-parameter locals. Its stack is
+ expedited, so examining it reads heap memory but no stack memory."""
self.check_memory_reads_when_examining_locals(
examine_all_frames=False, expect_stack_reads=False
)
diff --git a/lldb/test/API/macosx/expedited-stack-memory/main.c b/lldb/test/API/macosx/expedited-stack-memory/main.c
index 57d219d944995..8dca4d8f16ab7 100644
--- a/lldb/test/API/macosx/expedited-stack-memory/main.c
+++ b/lldb/test/API/macosx/expedited-stack-memory/main.c
@@ -10,6 +10,7 @@
// - aggregate locals (a struct and a fixed stack array)
// - a variable-length array (dynamically sized stack storage, like alloca)
// - pointer locals, including a pointer to heap memory
+// - stack-passed parameters
//
// The outer frames (func_d / func_c) also carry locals of these kinds, so that
// walking the whole stack and examining every frame reads the same variety of
@@ -31,11 +32,18 @@ struct Stats {
double mean;
};
+// A large by-value struct. Passed as an argument it does not fit in registers,
+// so it is passed on the stack, above the callee's frame in the caller.
+struct Big {
+ long v[8];
+};
+
// The innermost frame, where we stop. It carries several kind of local: a
// scalar, aggregates (struct + array), pointers (including one into heap
// memory) and a variable-length array. Examining this single frame on a stop
// reads both stack and heap memory.
-static int func_e(int depth) {
+static int func_e(int depth, int a1, int a2, int a3, int a4, int a5, int a6,
+ int a7, int a8, int a9, struct Big big) {
int i = depth + 1;
long l = (long)depth * 1000;
double d = depth + 0.5;
@@ -51,9 +59,10 @@ static int func_e(int depth) {
const char *str = "hello from func_e";
int *self = &i;
g_sink = i + l + (long)d + stats.sum + arr[3] + vla[n - 1] +
- heap[HEAP_COUNT - 1] + str[0] + *self; // break here
+ heap[HEAP_COUNT - 1] + str[0] + *self + a8 + a9 +
+ big.v[7]; // break here
int r = i + (int)l + (int)d + (int)stats.sum + (int)arr[3] + (int)vla[n - 1] +
- (int)heap[HEAP_COUNT - 1] + str[0] + *self;
+ (int)heap[HEAP_COUNT - 1] + str[0] + *self + a8 + a9 + (int)big.v[7];
free(heap);
return r;
}
@@ -62,7 +71,11 @@ static int func_e(int depth) {
static int func_d(int x) {
struct Stats stats = {.sum = x, .min = x - 1, .max = x + 1, .mean = x + 0.5};
long arr[4] = {x, x + 1, x + 2, x + 3};
- int r = func_e(x);
+ struct Big big;
+ for (int k = 0; k < 8; ++k)
+ big.v[k] = 100 + k;
+ int r = func_e(x, x + 1, x + 2, x + 3, x + 4, x + 5, x + 6, x + 7, x + 8,
+ x + 9, big);
return r + (int)stats.sum + (int)arr[3];
}
diff --git a/lldb/tools/debugserver/source/RNBRemote.cpp b/lldb/tools/debugserver/source/RNBRemote.cpp
index a92a3a3eab14a..6411da486e4da 100644
--- a/lldb/tools/debugserver/source/RNBRemote.cpp
+++ b/lldb/tools/debugserver/source/RNBRemote.cpp
@@ -2733,10 +2733,17 @@ static void ReadStackMemory(nub_process_t pid, nub_thread_t tid,
}
}
-// The size of each per-side stack window we expedite. 512 is sized to cover
-// the common case (locals and params sit within a few hundred bytes) while
-// bounding the per-frame cost to 1K bytes regardless of frame size.
-static const nub_size_t k_expedite_stack_window = 512;
+// The total stack-memory budget we expedite for frame 0, in bytes. Sized to
+// cover the common case (locals, spilled register arguments, and stack-passed
+// parameters) while bounding the per-frame cost.
+static const nub_size_t k_expedite_stack_window = 1024;
+
+// Bytes reserved for the above-fp "stack-passed parameters" window,
+// [fp + 2*ptr_size, fp + 2*ptr_size + k_expedite_stack_arg_size).
+static const nub_size_t k_expedite_stack_arg_size = 160;
+
+static_assert(k_expedite_stack_arg_size <= k_expedite_stack_window,
+ "above-fp arg window must fit within the total stack budget");
// A single contiguous chunk of expedited memory.
struct ExpeditedMemory {
@@ -2744,19 +2751,29 @@ struct ExpeditedMemory {
std::vector<uint8_t> bytes;
};
-// Read the innermost frame's stack memory so that examining its local variables
-// at a public stop is served from the expedited cache instead of generating one
-// memory-read packet.
-//
-// We produce either:
-//
-// - one chunk [$sp, $fp) for a small frame ($fp - $sp <=
-// 2*k_expedite_stack_window), which covers the whole frame with no gap, or
-//
-// - two chunks [$sp, $sp + k_expedite_stack_window) and [$fp -
-// k_expedite_stack_window, $fp) for a large frame, covering the params near
-// $sp and the locals near $fp while leaving the (rarely-interesting) middle
-// spill area out so the cost stays bounded.
+// Heuristic to decide whether frame 0's $fp looks like a valid frame pointer.
+static bool FrameZeroFPLooksValid(nub_process_t pid, nub_thread_t tid,
+ uint64_t sp, uint64_t fp,
+ nub_size_t ptr_size) {
+ static const uint64_t k_expedite_max_frame_size = 8 * 1024 * 1024; // 8 MB
+
+ if (sp == 0 || fp == 0 || fp <= sp)
+ return false;
+ if (fp - sp > k_expedite_max_frame_size)
+ return false;
+
+ const nub_size_t rec = 2 * ptr_size;
+ uint8_t bytes[2 * sizeof(uint64_t)];
+ if (DNBProcessMemoryRead(pid, fp, rec, bytes) != rec)
+ return false;
+
+ uint64_t prev_fp =
+ (ptr_size == 4) ? ((uint32_t *)bytes)[0] : ((uint64_t *)bytes)[0];
+ // The saved previous fp must chain upward (stack grows down).
+ return prev_fp > fp;
+}
+
+// Read the innermost frame's stack memory.
static std::vector<ExpeditedMemory> ReadFrameZeroStackMemory(nub_process_t pid,
nub_thread_t tid) {
std::vector<ExpeditedMemory> chunks;
@@ -2772,31 +2789,33 @@ static std::vector<ExpeditedMemory> ReadFrameZeroStackMemory(nub_process_t pid,
uint64_t sp = (ptr_size == 4) ? sp_value.value.uint32 : sp_value.value.uint64;
uint64_t fp = (ptr_size == 4) ? fp_value.value.uint32 : fp_value.value.uint64;
- // The stack grows down, so a normal frame has sp < fp. Bail on a leaf/empty
- // frame (sp == fp) or anything that doesn't look like a frame.
- if (sp == 0 || fp <= sp)
- return chunks;
-
auto read_range = [&](uint64_t start, uint64_t length) {
+ if (length == 0)
+ return;
std::vector<uint8_t> buf(length);
if (DNBProcessMemoryRead(pid, start, length, buf.data()) != length)
return;
chunks.push_back({start, std::move(buf)});
};
- const uint64_t frame_size = fp - sp;
- const nub_size_t window = k_expedite_stack_window;
+ const uint64_t rec = 2 * ptr_size; // frame record size (16 on arm64)
+
+ if (FrameZeroFPLooksValid(pid, tid, sp, fp, ptr_size)) {
+ // above-fp: stack-passed params, skipping the already-expedited frame
+ // record.
+ read_range(fp + rec, k_expedite_stack_arg_size);
- if (frame_size <= 2 * window) {
- // Small frame: cover the whole frame as a single contiguous chunk [sp, fp).
- read_range(sp, frame_size);
+ // below-fp: locals + spilled register args, clamped at $sp so a small frame
+ // reads only [sp, fp).
+ uint64_t below = std::min<uint64_t>(fp - sp, k_expedite_stack_window -
+ k_expedite_stack_arg_size);
+ read_range(fp - below, below);
return chunks;
}
- // Large frame: cover the params near $sp and the locals near $fp with two
- // bounded windows, leaving the middle spill area out to keep the cost capped.
- read_range(sp, window); // [sp, sp + WINDOW)
- read_range(fp - window, window); // [fp - WINDOW, fp)
+ // Frameless / cannot validate $fp: expedite a single window anchored at $sp.
+ if (sp != 0)
+ read_range(sp, k_expedite_stack_window);
return chunks;
}
>From e13752d5053a4c058c72ec2597bfd8679ac0ffc2 Mon Sep 17 00:00:00 2001
From: Yao Qi <yao_qi at apple.com>
Date: Fri, 31 Jul 2026 11:49:03 +0100
Subject: [PATCH 4/4] Fix
---
lldb/tools/debugserver/source/RNBRemote.cpp | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/lldb/tools/debugserver/source/RNBRemote.cpp b/lldb/tools/debugserver/source/RNBRemote.cpp
index 6411da486e4da..bc4ba6dc10f37 100644
--- a/lldb/tools/debugserver/source/RNBRemote.cpp
+++ b/lldb/tools/debugserver/source/RNBRemote.cpp
@@ -2798,12 +2798,10 @@ static std::vector<ExpeditedMemory> ReadFrameZeroStackMemory(nub_process_t pid,
chunks.push_back({start, std::move(buf)});
};
- const uint64_t rec = 2 * ptr_size; // frame record size (16 on arm64)
-
if (FrameZeroFPLooksValid(pid, tid, sp, fp, ptr_size)) {
// above-fp: stack-passed params, skipping the already-expedited frame
// record.
- read_range(fp + rec, k_expedite_stack_arg_size);
+ read_range(fp + 2 * ptr_size, k_expedite_stack_arg_size);
// below-fp: locals + spilled register args, clamped at $sp so a small frame
// reads only [sp, fp).
More information about the lldb-commits
mailing list