Skip to content

Commit a78e01c

Browse files
DinoVmeta-codesync[bot]
authored andcommitted
Use stack walking to recover IP
Summary: Updates our ARM64 builds to use the new stack walking IP. This allows us to implement both `getIP` and `setIP` so in addition to not having to generate the deopt updates in the code we also get support for deopting all running functions on the stack. Reviewed By: alexmalyshev Differential Revision: D115080934 fbshipit-source-id: 5cdb04beb2dae83ede3bebcd18342049db3275f8
1 parent 566c1fd commit a78e01c

8 files changed

Lines changed: 265 additions & 130 deletions

File tree

cinderx/Jit/code_runtime.cpp

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -168,20 +168,6 @@ int CodeRuntime::traverse(visitproc visit, void* arg) {
168168
return 0;
169169
}
170170

171-
std::optional<UnitCallStack> CodeRuntime::getUnitCallStackFromDeoptIdx(
172-
std::size_t deopt_idx) const {
173-
if (deopt_idx >= deopt_metadatas_.size()) {
174-
return std::nullopt;
175-
}
176-
const DeoptMetadata& meta = deopt_metadatas_[deopt_idx];
177-
UnitCallStack stack;
178-
stack.reserve(meta.frame_meta.size());
179-
for (const auto& frame : meta.frame_meta) {
180-
stack.emplace_back(frame.code, frame.cause_instr_idx);
181-
}
182-
return stack;
183-
}
184-
185171
std::optional<uintptr_t> CodeRuntime::getCallsiteDeoptExit(
186172
uintptr_t return_addr) const {
187173
auto it = callsite_deopt_exits_.find(return_addr);

cinderx/Jit/code_runtime.h

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,10 +114,6 @@ class alignas(16) CodeRuntime {
114114
// True if the references have been cleared
115115
bool isCleared() const;
116116

117-
// Get the UnitCallStack from a deopt metadata index.
118-
std::optional<UnitCallStack> getUnitCallStackFromDeoptIdx(
119-
std::size_t deopt_idx) const;
120-
121117
std::optional<uintptr_t> getCallsiteDeoptExit(uintptr_t return_addr) const;
122118

123119
void addCallsiteDeoptExit(uintptr_t return_addr, uintptr_t deopt_exit_addr);

cinderx/Jit/frame.cpp

Lines changed: 114 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,16 @@
1717
#include "cinderx/Jit/deopt.h"
1818
#include "cinderx/Jit/frame_header.h"
1919
#include "cinderx/Jit/gen_data_footer.h"
20-
#include "cinderx/Jit/threaded_compile.h"
21-
#if defined(CINDER_X86_64)
20+
#include "cinderx/Jit/stack_walk.h"
2221
#include "cinderx/Jit/symbolizer.h"
23-
#endif
22+
#include "cinderx/Jit/threaded_compile.h"
2423
#include "cinderx/UpstreamBorrow/borrowed.h"
2524
#include "cinderx/module_state.h"
2625

26+
#include <csignal>
27+
#include <cstring>
2728
#include <optional>
29+
#include <string>
2830

2931
namespace cinderx::jit {
3032

@@ -91,7 +93,6 @@ bool isInlined(_PyInterpreterFrame* frame) {
9193
return isInlinedFrame(frame);
9294
}
9395

94-
#if defined(CINDER_X86_64)
9596
// Return the base of the stack frame given its frame.
9697
uintptr_t getFrameBaseFromOnStackFrame(_PyInterpreterFrame* frame) {
9798
// The frame is embedded in the frame header at the beginning of the
@@ -100,59 +101,108 @@ uintptr_t getFrameBaseFromOnStackFrame(_PyInterpreterFrame* frame) {
100101
sizeof(PyObject*) * _PyFrame_GetCode(frame)->co_framesize;
101102
}
102103

103-
uintptr_t getIP(_PyInterpreterFrame* frame, int frame_size) {
104104
#ifdef ENABLE_LIGHTWEIGHT_FRAMES
105-
JIT_CHECK(isJitFrame(frame), "frame not executed by the JIT");
105+
106+
#if defined(CINDER_X86_64)
107+
108+
const void** getIPStackAddr(_PyInterpreterFrame* frame, int frame_size) {
106109
uintptr_t frame_base;
107110
if (isGeneratorFrame(frame)) {
111+
// A running generator spills to the heap but still executes on the real
112+
// stack, so its calls push return addresses relative to the frame pointer
113+
// the resume function was entered with.
108114
PyGenObject* gen = _PyGen_GetGeneratorFromFrame(frame);
109-
auto footer = jitGenDataFooter(gen);
110-
if (footer->yieldPoint == nullptr) {
111-
// The generator is running.
112-
// On x86, we read the return address from a fixed offset on the real
113-
// stack relative to the resume function's RBP.
114-
frame_base = footer->originalFramePointer;
115-
} else {
116-
// The generator is suspended.
117-
return footer->yieldPoint->resumeTarget();
118-
}
115+
frame_base = jitGenDataFooter(gen)->originalFramePointer;
119116
} else {
120117
frame_base = getFrameBaseFromOnStackFrame(frame);
121118
}
122-
// Read the saved IP from the stack.
123-
// On x86, `call` pushes the return address on the stack at a fixed
124-
// location relative to the caller's frame pointer.
125-
uintptr_t ip;
126-
auto saved_ip =
127-
reinterpret_cast<uintptr_t*>(frame_base - frame_size - kPointerSize);
128-
memcpy(&ip, saved_ip, kPointerSize);
129-
return ip;
130-
#else
131-
throw std::runtime_error{"getIP: Lightweight frames are not supported"};
132-
#endif
119+
// `call` pushes the return address immediately below the unit's stack frame.
120+
return reinterpret_cast<const void**>(frame_base - frame_size - kPointerSize);
121+
}
122+
123+
#elif defined(CINDER_AARCH64)
124+
125+
// `bl` leaves the return address in LR and the callee spills it into its own
126+
// frame record, so there is no fixed location relative to the unit's frame to
127+
// read it back from. Walk the native stack the unit lives on instead, looking
128+
// for the frame whose caller is the unit.
129+
const void** getIPStackAddr(_PyInterpreterFrame* frame, int) {
130+
// The value the frame-pointer register holds while the unit runs. Resumed
131+
// generators point it at their heap-allocated data so that spills survive
132+
// suspension; every other unit leaves it at the base of the stack frame the
133+
// interpreter frame is embedded in.
134+
const StackFrame* unit_frame;
135+
if (isGeneratorFrame(frame)) {
136+
PyGenObject* gen = _PyGen_GetGeneratorFromFrame(frame);
137+
unit_frame = reinterpret_cast<const StackFrame*>(jitGenDataFooter(gen));
138+
} else {
139+
unit_frame = reinterpret_cast<const StackFrame*>(
140+
getFrameBaseFromOnStackFrame(frame));
141+
}
142+
143+
const void** result = nullptr;
144+
auto visit = [&](const void* frame_pointer, const void* pc) {
145+
auto record =
146+
const_cast<StackFrame*>(static_cast<const StackFrame*>(frame_pointer));
147+
if (record->frame_pointer == unit_frame) {
148+
// This frame belongs to the unit's callee, which saved the address the
149+
// unit resumes at.
150+
result = &record->return_address;
151+
return false;
152+
}
153+
// Keep going as long as we aren't at the running function who would
154+
// have no return address.
155+
return record != unit_frame;
156+
};
157+
158+
PyThreadState* owner = jitFrameGetHeader(frame)->tstate;
159+
JIT_CHECK(owner != nullptr, "JIT frame is not associated with a thread");
160+
161+
FreeThreadedJITEntrypointGuard guard;
162+
StackWalk walker;
163+
walker.walk(owner, visit);
164+
return result;
133165
}
166+
167+
#else
168+
CINDER_UNSUPPORTED
134169
#endif
135170

136-
void setIP(
137-
[[maybe_unused]] _PyInterpreterFrame* frame,
138-
[[maybe_unused]] int frame_size,
139-
[[maybe_unused]] uintptr_t new_ip) {
140-
#if defined(__x86_64__) && defined(ENABLE_LIGHTWEIGHT_FRAMES)
171+
uintptr_t getIP(_PyInterpreterFrame* frame, int frame_size) {
141172
JIT_CHECK(isJitFrame(frame), "frame not executed by the JIT");
142-
uintptr_t frame_base;
143173
if (isGeneratorFrame(frame)) {
144-
PyGenObject* gen = _PyGen_GetGeneratorFromFrame(frame);
145-
auto footer = jitGenDataFooter(gen);
146-
frame_base = footer->originalFramePointer;
147-
} else {
148-
frame_base = getFrameBaseFromOnStackFrame(frame);
174+
auto footer = jitGenDataFooter(_PyGen_GetGeneratorFromFrame(frame));
175+
if (footer->yieldPoint != nullptr) {
176+
// The generator is suspended, so it has no native frame of its own; it
177+
// will resume at its yield point.
178+
return footer->yieldPoint->resumeTarget();
179+
}
149180
}
150-
auto saved_ip_addr =
151-
reinterpret_cast<uintptr_t*>(frame_base - frame_size - kPointerSize);
152-
*saved_ip_addr = new_ip;
153-
#endif
181+
const void** saved = getIPStackAddr(frame, frame_size);
182+
if (saved != nullptr) {
183+
uintptr_t ip;
184+
memcpy(&ip, saved, kPointerSize);
185+
return ip;
186+
}
187+
// Callers report a missing IP; they have the code object needed to make the
188+
// message useful.
189+
return 0;
154190
}
155191

192+
// Overwrite the address the unit resumes at when its current call returns.
193+
// Returns false if the unit's IP isn't saved anywhere that can be patched.
194+
bool setIP(_PyInterpreterFrame* frame, int frame_size, uintptr_t new_ip) {
195+
JIT_CHECK(isJitFrame(frame), "frame not executed by the JIT");
196+
const void** saved = getIPStackAddr(frame, frame_size);
197+
if (saved == nullptr) {
198+
return false;
199+
}
200+
*saved = reinterpret_cast<const void*>(new_ip);
201+
return true;
202+
}
203+
204+
#endif // ENABLE_LIGHTWEIGHT_FRAMES
205+
156206
// Collect all the frames in the unit, with the frame for the
157207
// non-inlined function as the first element in the return vector.
158208
std::vector<_PyInterpreterFrame*> getUnitFrames(_PyInterpreterFrame* frame) {
@@ -211,13 +261,10 @@ UnitState getUnitState(_PyInterpreterFrame* frame) {
211261
}
212262
};
213263

214-
#if defined(CINDER_AARCH64)
215-
// Look up bytecode offsets using the deopt index stored in the frame header.
216-
// The JIT updates this index before each instruction that can deopt, so it
217-
// always reflects the current position in the bytecode.
218-
std::size_t deopt_idx = jitFrameGetHeader(non_inlined_sf)->deopt_idx;
264+
// Look up bytecode offsets from the unit's current instruction pointer.
265+
uintptr_t ip = getIP(non_inlined_sf, code_rt->frameSize());
219266
std::optional<UnitCallStack> locs =
220-
code_rt->getUnitCallStackFromDeoptIdx(deopt_idx);
267+
code_rt->debugInfo()->getUnitCallStack(ip);
221268
if (locs.has_value()) {
222269
// We may have a different number of unit_frames than locs, this happens
223270
// when we're updating the outer frame while we're in an inlined function,
@@ -230,35 +277,6 @@ UnitState getUnitState(_PyInterpreterFrame* frame) {
230277
codeName(locs->at(i).code));
231278
unit_state.emplace_back(unit_frames[i], locs->at(i));
232279
}
233-
} else {
234-
// We might not have debug info for a number of reasons.
235-
// The consequences of getting this wrong (incorrect line numbers) don't
236-
// warrant aborting in production, but it is worth investigating.
237-
JIT_LOG(
238-
"No debug info for deopt_idx {} in {}",
239-
deopt_idx,
240-
PyUnicode_AsUTF8(code_rt->code()->co_qualname));
241-
logUnitFrames();
242-
JIT_DABORT("No debug info for deopt_idx {}", deopt_idx);
243-
for (_PyInterpreterFrame* unit_frame : unit_frames) {
244-
unit_state.emplace_back(
245-
unit_frame, CodeObjLoc{_PyFrame_GetCode(unit_frame), BCOffset{-1}});
246-
}
247-
}
248-
#elif defined(CINDER_X86_64)
249-
// On x86-64, look up bytecode offsets using the IP-based symbolizer.
250-
uintptr_t ip = getIP(non_inlined_sf, code_rt->frameSize());
251-
std::optional<UnitCallStack> locs =
252-
code_rt->debugInfo()->getUnitCallStack(ip);
253-
if (locs.has_value()) {
254-
for (std::size_t i = 0; i < unit_frames.size(); i++) {
255-
JIT_DCHECK(
256-
_PyFrame_GetCode(unit_frames[i]) == locs->at(i).code,
257-
"code mismatch {} vs {}",
258-
codeName(_PyFrame_GetCode(unit_frames[i])),
259-
codeName(locs->at(i).code));
260-
unit_state.emplace_back(unit_frames[i], locs->at(i));
261-
}
262280
} else {
263281
JIT_LOG(
264282
"No debug info for addr {:x} {}",
@@ -271,9 +289,6 @@ UnitState getUnitState(_PyInterpreterFrame* frame) {
271289
unit_frame, CodeObjLoc{_PyFrame_GetCode(unit_frame), BCOffset{-1}});
272290
}
273291
}
274-
#else
275-
CINDER_UNSUPPORTED
276-
#endif
277292

278293
return unit_state;
279294
#else
@@ -342,12 +357,7 @@ std::optional<ActiveDeoptMetadata> getActiveDeoptMetadata(
342357
auto footer = jitGenDataFooter(gen);
343358
frame_base = reinterpret_cast<uintptr_t>(footer);
344359
} else {
345-
#if defined(CINDER_X86_64)
346360
frame_base = getFrameBaseFromOnStackFrame(owning_frame);
347-
#else
348-
frame_base = reinterpret_cast<uintptr_t>(owning_frame) +
349-
sizeof(PyObject*) * _PyFrame_GetCode(owning_frame)->co_framesize;
350-
#endif
351361
}
352362
return ActiveDeoptMetadata{
353363
.meta = &code_rt->getDeoptMetadata(deopt_idx),
@@ -654,9 +664,6 @@ void jitFrameInitLightweight(
654664
setFrameCode(frame, reifier);
655665
setFrameFunction(frame, (PyObject*)Py_NewRef(func));
656666
jitFrameGetHeader(frame)->frame_status = 0;
657-
#if defined(CINDER_AARCH64)
658-
jitFrameGetHeader(frame)->deopt_idx = 0;
659-
#endif
660667
#else
661668
frame->stacktop = 0;
662669
setFrameInstruction(frame, _PyCode_CODE(code) - 1);
@@ -667,6 +674,14 @@ void jitFrameInitLightweight(
667674
"frame helper must be immortal");
668675
setFrameFunction(frame, cinderx::getModuleState()->frame_reifier);
669676
jitFrameSetFunction(frame, (PyFunctionObject*)Py_NewRef(func));
677+
#endif
678+
#if defined(CINDER_AARCH64)
679+
// Refreshed on every resume by the generator resume entry, since a generator
680+
// can be resumed on a thread other than the one that started it.
681+
jitFrameGetHeader(frame)->tstate = tstate;
682+
#endif
683+
#if defined(Py_GIL_DISABLED)
684+
jitFrameGetHeader(frame)->deopt_idx = 0;
670685
#endif
671686
frame->previous = previous;
672687
}
@@ -830,7 +845,7 @@ void retainActiveDeferredData(
830845
}
831846

832847
void deoptAllJitFramesOnStack() {
833-
#if defined(__x86_64__) && defined(ENABLE_LIGHTWEIGHT_FRAMES)
848+
#ifdef ENABLE_LIGHTWEIGHT_FRAMES
834849
PyInterpreterState* interp = PyInterpreterState_Get();
835850

836851
// In free-threaded builds, other threads are actively running and may be
@@ -879,11 +894,16 @@ void deoptAllJitFramesOnStack() {
879894
}
880895

881896
jitFramePopulateFrame(frame);
882-
setIP(frame, code_rt->frameSize(), deopt_exit.value());
883-
884-
// Mark frame so updatePrevInstr skips it (deopt exit IP
885-
// has no debug info entry).
886-
jitFrameGetHeader(frame)->frame_status |= JIT_FRAME_DEOPT_PATCHED;
897+
if (setIP(frame, code_rt->frameSize(), deopt_exit.value())) {
898+
// Mark frame so updatePrevInstr skips it (deopt exit IP
899+
// has no debug info entry).
900+
jitFrameGetHeader(frame)->frame_status |=
901+
JIT_FRAME_DEOPT_PATCHED;
902+
} else {
903+
JIT_DLOG(
904+
"Couldn't patch the IP of the JIT frame at {:#x}",
905+
current_ip);
906+
}
887907
} else {
888908
JIT_DLOG(
889909
"No callsite deopt exit for JIT frame at IP {:#x}",

cinderx/Jit/frame_header.h

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,17 @@ int frameHeaderSize(BorrowedRef<PyCodeObject> code);
1717
// FrameHeader lives at the beginning of the stack frame for JIT-compiled
1818
// functions. This is followed by the _PyInterpreterFrame.
1919
struct FrameHeader {
20-
#if defined(CINDER_AARCH64) || defined(Py_GIL_DISABLED)
21-
// Index into the CodeRuntime's deopt metadata array. Used to recover the
22-
// current bytecode offset for frame introspection (e.g. sys._current_frames).
23-
// Updated before each instruction that can deopt. In free-threaded builds,
24-
// this also gives GC an exact active callsite for deferred-RC root scanning.
25-
// On aarch64 deopt_idx is placed first so that func/frame_status is adjacent
26-
// to the _PyInterpreterFrame fields that follow, enabling consecutive stores
27-
// via StorePair during frame initialization.
20+
#if defined(CINDER_AARCH64)
21+
// The thread this frame is running on. AArch64 leaves the return address of
22+
// an active call in the callee's frame record rather than at a fixed offset
23+
// from the caller's frame pointer, so recovering the frame's current IP
24+
// means walking the native stack it lives on. See frame.cpp.
25+
PyThreadState* tstate;
26+
#endif
27+
#if defined(Py_GIL_DISABLED)
28+
// Index into the CodeRuntime's deopt metadata array, giving GC an exact
29+
// active callsite for deferred-RC root scanning. Updated before each
30+
// instruction that can deopt.
2831
std::size_t deopt_idx;
2932
#endif
3033
union {

0 commit comments

Comments
 (0)