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 <[email protected]> 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 <[email protected]> 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 <[email protected]> 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 <[email protected]> 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). _______________________________________________ lldb-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits
