Skip to content

Commit 8d7935d

Browse files
[lldb] Make StackID comparison search parent heap CFAs
For two StackIDs whose CFAs are on the heap, i.e., they are both async frames, we can decide which one is younger by finding one async context in the continuation chain of the other. A lot of stepping algorithms rely on frame comparisons. In particular, this patch fixes stepping out of async functions, and makes strides towards fixing other stepping algorithms. The explanation below is not needed to understand this patch, but it sheds some light into why the most common kind of step out fails. Consider the case where a breakpoint was set inside some async function, the program hit that breakpoint, and then a "step out" operation is performed. This is a very common use case. The thread plan looks like this: 0. Single step past breakpoint. 1. Step out LLDB performs a single step, and thread plan 0 says "continue running, I'm done". Before continuing program execution, LLDB asks all thread plans, in this case there's only the step out plan, if it should stop. To answer the "should stop" question, the step out plan compares the current frame (which is the same as when step out was pushed, since LLDB has only performed a single step so far) with its target frame (which is just one frame above). In this scenario, the current frame is async, and so the parent frame must also be async. The StepOut plan is going to compare two async StackIDs. Using the incorrect comparison, StepOut concludes the current frame is _older_ than the target frame. In other words, the program reached an older frame without ever stopping on the target frame. Something weird must have happened, and the plan concludes it is done.
1 parent 349d906 commit 8d7935d

File tree

3 files changed

+66
-3
lines changed

3 files changed

+66
-3
lines changed

lldb/source/Target/StackID.cpp

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#include "lldb/Target/MemoryRegionInfo.h"
1414
#include "lldb/Target/Process.h"
1515
#include "lldb/Target/Thread.h"
16+
#include "lldb/Utility/LLDBLog.h"
1617
#include "lldb/Utility/Stream.h"
1718

1819
using namespace lldb_private;
@@ -85,7 +86,6 @@ IsYoungerHeapCFAs(const StackID &lhs, const StackID &rhs, Process &process) {
8586
if (lhs_cfa_on_stack && rhs_cfa_on_stack)
8687
return HeapCFAComparisonResult::NoOpinion;
8788

88-
// FIXME: rdar://76119439
8989
// If one of the frames has a CFA on the stack and the other doesn't, we are
9090
// at the boundary between an asynchronous and a synchronous function.
9191
// Synchronous functions cannot call asynchronous functions, therefore the
@@ -94,6 +94,35 @@ IsYoungerHeapCFAs(const StackID &lhs, const StackID &rhs, Process &process) {
9494
return HeapCFAComparisonResult::Younger;
9595
if (!lhs_cfa_on_stack && rhs_cfa_on_stack)
9696
return HeapCFAComparisonResult::Older;
97+
98+
const lldb::addr_t lhs_cfa = lhs.GetCallFrameAddress();
99+
const lldb::addr_t rhs_cfa = rhs.GetCallFrameAddress();
100+
// If the cfas are the same, fallback to the usual scope comparison.
101+
if (lhs_cfa == rhs_cfa)
102+
return HeapCFAComparisonResult::NoOpinion;
103+
104+
// Both CFAs are on the heap and they are distinct.
105+
// LHS is younger if and only if its continuation async context is (directly
106+
// or indirectly) RHS. Chase continuation pointers to check this case, until
107+
// we hit the end of the chain (parent_ctx == 0) or a safety limit in case of
108+
// an invalid continuation chain.
109+
auto max_num_frames = 512;
110+
for (lldb::addr_t parent_ctx = lhs_cfa; parent_ctx && max_num_frames;
111+
max_num_frames--) {
112+
Status error;
113+
lldb::addr_t old_parent_ctx = parent_ctx;
114+
// The continuation's context is the first field of an async context.
115+
parent_ctx = process.ReadPointerFromMemory(old_parent_ctx, error);
116+
if (error.Fail()) {
117+
Log *log = GetLog(LLDBLog::Unwind);
118+
LLDB_LOGF(log, "Failed to read parent async context of: 0x%8.8" PRIx64,
119+
old_parent_ctx);
120+
break;
121+
}
122+
if (parent_ctx == rhs_cfa)
123+
return HeapCFAComparisonResult::Younger;
124+
}
125+
97126
return HeapCFAComparisonResult::NoOpinion;
98127
}
99128
// END SWIFT

lldb/test/API/lang/swift/async/stepping/step_out/TestSteppingOutAsync.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ def step_out_checks(self, thread, expected_func_names):
3030

3131
@swiftTest
3232
@skipIf(oslist=["windows", "linux"])
33-
@skipIf("rdar://133849022")
3433
def test(self):
3534
"""Test `frame variable` in async functions"""
3635
self.build()

lldb/unittests/StackID/StackIDTest.cpp

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,19 @@ class StackIDTest : public ::testing::Test {
3939
};
4040

4141
struct MockProcess : Process {
42+
llvm::DenseMap<addr_t, addr_t> memory_map;
43+
4244
MockProcess(TargetSP target_sp, ListenerSP listener_sp)
4345
: Process(target_sp, listener_sp) {}
46+
MockProcess(TargetSP target_sp, ListenerSP listener_sp,
47+
llvm::DenseMap<addr_t, addr_t> &&memory_map)
48+
: Process(target_sp, listener_sp), memory_map(memory_map) {}
4449
size_t DoReadMemory(addr_t vm_addr, void *buf, size_t size,
4550
Status &error) override {
46-
return 0;
51+
assert(memory_map.contains(vm_addr));
52+
assert(size == sizeof(addr_t));
53+
*reinterpret_cast<addr_t *>(buf) = memory_map[vm_addr];
54+
return sizeof(addr_t);
4755
}
4856
size_t ReadMemory(addr_t addr, void *buf, size_t size,
4957
Status &status) override {
@@ -88,3 +96,30 @@ TEST_F(StackIDTest, StackHeapCFAComparison) {
8896
EXPECT_TRUE(StackID::IsYounger(cfa_on_stack, cfa_on_heap, process));
8997
EXPECT_FALSE(StackID::IsYounger(cfa_on_heap, cfa_on_stack, process));
9098
}
99+
100+
TEST_F(StackIDTest, HeapHeapCFAComparison) {
101+
// Create a mock async continuation chain:
102+
// 100 -> 108 -> 116 -> 0
103+
// This should be read as:
104+
// "Async context whose address is 100 has a continuation context whose
105+
// address is 108", etc.
106+
llvm::DenseMap<addr_t, addr_t> memory_map;
107+
memory_map[100] = 108;
108+
memory_map[108] = 116;
109+
memory_map[116] = 0;
110+
auto process = MockProcess(m_target_sp, Listener::MakeListener("dummy"),
111+
std::move(memory_map));
112+
113+
MockStackID oldest_cfa(/*cfa*/ 116, OnStack::No);
114+
MockStackID middle_cfa(/*cfa*/ 108, OnStack::No);
115+
MockStackID youngest_cfa(/*cfa*/ 100, OnStack::No);
116+
117+
EXPECT_TRUE(StackID::IsYounger(youngest_cfa, oldest_cfa, process));
118+
EXPECT_FALSE(StackID::IsYounger(oldest_cfa, youngest_cfa, process));
119+
120+
EXPECT_TRUE(StackID::IsYounger(youngest_cfa, middle_cfa, process));
121+
EXPECT_FALSE(StackID::IsYounger(middle_cfa, youngest_cfa, process));
122+
123+
EXPECT_TRUE(StackID::IsYounger(middle_cfa, oldest_cfa, process));
124+
EXPECT_FALSE(StackID::IsYounger(oldest_cfa, middle_cfa, process));
125+
}

0 commit comments

Comments
 (0)