Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Doc/howto/perf_profiling.rst
Original file line number Diff line number Diff line change
Expand Up @@ -218,8 +218,8 @@ How to obtain the best results
------------------------------

For best results, keep frame pointers enabled. On supported GCC-compatible
toolchains, CPython builds itself with ``-fno-omit-frame-pointer`` and, when
available, ``-mno-omit-leaf-frame-pointer`` by default. These flags allow
toolchains, CPython builds itself with ``-fno-omit-frame-pointer`` and similar
flags (see :option:`--without-frame-pointers` for details). These flags allow
profilers to unwind using only the frame pointer and not on DWARF debug
information. This is because as the code that is interposed to allow ``perf``
support is dynamically generated it doesn't have any DWARF debugging information
Expand Down
18 changes: 13 additions & 5 deletions Doc/using/configure.rst
Original file line number Diff line number Diff line change
Expand Up @@ -784,11 +784,19 @@ also be used to improve performance.

Disable frame pointers, which are enabled by default (see :pep:`831`).

By default, the build appends ``-fno-omit-frame-pointer`` (and
``-mno-omit-leaf-frame-pointer`` when the compiler supports it) to
``BASECFLAGS`` so profilers, debuggers, and system tracing tools
(``perf``, ``eBPF``, ``dtrace``, ``gdb``) can walk the C call stack
without DWARF metadata. The flags propagate to third-party C
By default, the build appends flags to generate frame or backchain
pointers to ``BASECFLAGS``:

- ``-fno-omit-frame-pointer`` and/or ``-mno-omit-leaf-frame-pointer``
are added when the compiler supports them,
- ``-marm`` is added on 32-bit ARM when supported,
- on s390x platforms, when supported, ``-mbackchain`` is added *instead*
of the above frame pointer flags
Comment thread
encukou marked this conversation as resolved.
Outdated

Frame pointers enable profilers, debuggers, and system tracing tools
(``perf``, ``eBPF``, ``dtrace``, ``gdb``) to walk the C call stack
without DWARF metadata.
The flags propagate to third-party C
extensions through :mod:`sysconfig`. On compilers that do not
understand them, the build silently skips them.

Expand Down
13 changes: 13 additions & 0 deletions Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2486,6 +2486,19 @@ Build changes
and :option:`-X dev <-X>` is passed to the Python or Python is built in :ref:`debug mode <debug-build>`.
(Contributed by Donghee Na in :gh:`141770`.)

.. _whatsnew315-frame-pointers:

* CPython is now built with frame pointers enabled by default
(:pep:`831`). Pass :option:`--without-frame-pointers` to opt out.

Authors of C extensions and native libraries built with custom build
systems should ensure the unwind chain is intact.
This is usually done by adding ``-fno-omit-frame-pointer`` and
similar flags to ``CFLAGS``. See :option:`--without-frame-pointers`
documentation for the specific flags Python uses.

(Contributed by Pablo Galindo Salgado and Savannah Ostrowski in :gh:`149201`.)

.. _whatsnew315-windows-tail-calling-interpreter:

* 64-bit builds using Visual Studio 2026 (MSVC 18) may now use the new
Expand Down
10 changes: 10 additions & 0 deletions Lib/test/test_frame_pointer_unwind.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@


def _frame_pointers_expected(machine):
_Py_WITH_FRAME_POINTERS = getattr(
_testinternalcapi,
"_Py_WITH_FRAME_POINTERS",
-1,
)
if _Py_WITH_FRAME_POINTERS > 0:
return True
if _Py_WITH_FRAME_POINTERS == 0:
return False

cflags = " ".join(
value for value in (
sysconfig.get_config_var("PY_CORE_CFLAGS"),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
Enable frame pointers by default for GCC-compatible CPython builds, including
``-mno-omit-leaf-frame-pointer`` when the compiler supports it, so profilers
and debuggers can unwind native interpreter frames more reliably. Users can pass
``--without-frame-pointers`` to opt out.
``-mno-omit-leaf-frame-pointer``, ``-marm`` on 32-bit ARM, and/or ``-mbackchain``
on s390x platforms when the compiler supports them, so profilers and debuggers
can unwind native interpreter frames more reliably. Users can pass
:option:`--without-frame-pointers` to ``./configure`` to opt out.
155 changes: 139 additions & 16 deletions Modules/_testinternalcapi.c
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,40 @@
static const uintptr_t min_frame_pointer_addr = 0x1000;
#define MAX_UNWIND_FRAMES 200

#ifdef __s390x__
// Linux's s390 "Stack Frame Layout" table documents that z/Architecture
// backchain frames start with the backchain at offset 0 and store "saved r14
// of caller function" at offset 112. The same document's register table
// identifies r14 as the return-address register, so this backchain unwinder
// reads the return address from fp + 112.
// https://www.kernel.org/doc/html/v5.3/s390/debugging390.html#stack-frame-layout
//
// This is only for Linux s390x backchain frames. The s390x ELF ABI does not
// generally mandate where RA and FP are saved, or whether they are saved at all.
// https://sourceware.org/binutils/docs/sframe-spec.html#s390x
# define S390X_FRAME_RETURN_ADDRESS_OFFSET 112
#endif

// The generic manual unwinder treats the frame pointer as a two-word record:
// fp[0] is the previous frame pointer and fp[1] is the return address. That is
// not true for every architecture, even with frame pointers enabled, so these
// offsets describe the actual slots used by each supported frame layout.
#if defined(__arm__) && !defined(__thumb__) && !defined(__clang__)
// GCC ARM mode keeps the caller's fp one word below fp and the saved LR at
// fp[0], so the return address is not in the generic fp[1] slot.
# define FRAME_POINTER_NEXT_OFFSET (-1)
# define FRAME_POINTER_RETURN_OFFSET 0
#elif defined(__s390x__)
// s390x backchain frames keep the previous frame pointer at fp[0], but save the
// return-address register in the ABI register save area rather than fp[1].
# define FRAME_POINTER_NEXT_OFFSET 0
# define FRAME_POINTER_RETURN_OFFSET \
(S390X_FRAME_RETURN_ADDRESS_OFFSET / (Py_ssize_t)sizeof(uintptr_t))
#else
# define FRAME_POINTER_NEXT_OFFSET 0
# define FRAME_POINTER_RETURN_OFFSET 1
#endif


static PyObject *
_get_current_module(void)
Expand Down Expand Up @@ -329,15 +363,96 @@ get_jit_backend(PyObject *self, PyObject *Py_UNUSED(args))
#endif
}

static int
stack_address_is_valid(uintptr_t addr, uintptr_t stack_min, uintptr_t stack_max)
{
if (addr < min_frame_pointer_addr) {
return 0;
}
if (stack_min != 0 && (addr < stack_min || addr >= stack_max)) {
return 0;
}
return 1;
}

static int
frame_pointer_slot_is_valid(uintptr_t *frame_pointer, Py_ssize_t offset,
uintptr_t stack_min, uintptr_t stack_max)
{
uintptr_t fp_addr = (uintptr_t)frame_pointer;
uintptr_t slot_addr;
uintptr_t delta = (uintptr_t)Py_ABS(offset) * sizeof(uintptr_t);
if (offset < 0) {
if (fp_addr < delta) {
return 0;
}
slot_addr = fp_addr - delta;
}
else {
if (fp_addr > UINTPTR_MAX - delta) {
return 0;
}
slot_addr = fp_addr + delta;
}
if (!stack_address_is_valid(slot_addr, stack_min, stack_max)) {
return 0;
}
if (stack_max != 0) {
if (slot_addr > UINTPTR_MAX - sizeof(uintptr_t)) {
return 0;
}
if (slot_addr + sizeof(uintptr_t) > stack_max) {
return 0;
}
}
return 1;
}

static int
next_frame_pointer_is_valid(uintptr_t *frame_pointer, uintptr_t *next_fp,
uintptr_t stack_min, uintptr_t stack_max)
{
uintptr_t fp_addr = (uintptr_t)frame_pointer;
uintptr_t next_addr = (uintptr_t)next_fp;
if (!stack_address_is_valid(next_addr, stack_min, stack_max)) {
return 0;
}
if ((next_addr % sizeof(uintptr_t)) != 0) {
return 0;
}
#if _Py_STACK_GROWS_DOWN
return next_addr > fp_addr;
#else
return next_addr < fp_addr;
#endif
}

static PyObject *
manual_unwind_from_fp(uintptr_t *frame_pointer)
{
int stack_grows_down = _Py_STACK_GROWS_DOWN;
uintptr_t stack_min = 0;
uintptr_t stack_max = 0;

#ifdef __s390x__
Py_BUILD_ASSERT(S390X_FRAME_RETURN_ADDRESS_OFFSET % sizeof(uintptr_t) == 0);
#endif

if (frame_pointer == NULL) {
return PyList_New(0);
}

PyThreadState *tstate = _PyThreadState_GET();
if (tstate != NULL) {
_PyThreadStateImpl *tstate_impl = (_PyThreadStateImpl *)tstate;
#if _Py_STACK_GROWS_DOWN
stack_min = tstate_impl->c_stack_hard_limit;
stack_max = tstate_impl->c_stack_top;
#else
stack_min = tstate_impl->c_stack_top;
stack_max = tstate_impl->c_stack_hard_limit;
#endif
}

PyObject *result = PyList_New(0);
if (result == NULL) {
return NULL;
Expand All @@ -357,7 +472,21 @@ manual_unwind_from_fp(uintptr_t *frame_pointer)
MAX_UNWIND_FRAMES);
return NULL;
}
uintptr_t return_addr = frame_pointer[1];
if (!stack_address_is_valid(fp_addr, stack_min, stack_max)) {
break;
}
if (!frame_pointer_slot_is_valid(frame_pointer,
FRAME_POINTER_NEXT_OFFSET,
stack_min, stack_max)) {
break;
}
if (!frame_pointer_slot_is_valid(frame_pointer,
FRAME_POINTER_RETURN_OFFSET,
stack_min, stack_max)) {
break;
}
uintptr_t *next_fp = (uintptr_t *)frame_pointer[FRAME_POINTER_NEXT_OFFSET];
uintptr_t return_addr = frame_pointer[FRAME_POINTER_RETURN_OFFSET];

PyObject *addr_obj = PyLong_FromUnsignedLongLong(return_addr);
if (addr_obj == NULL) {
Expand All @@ -372,22 +501,10 @@ manual_unwind_from_fp(uintptr_t *frame_pointer)
Py_DECREF(addr_obj);
depth++;

uintptr_t *next_fp = (uintptr_t *)frame_pointer[0];
// Stop if the frame pointer is extremely low.
if ((uintptr_t)next_fp < min_frame_pointer_addr) {
if (!next_frame_pointer_is_valid(frame_pointer, next_fp,
stack_min, stack_max)) {
break;
}
uintptr_t next_addr = (uintptr_t)next_fp;
if (stack_grows_down) {
if (next_addr <= fp_addr) {
break;
}
}
else {
if (next_addr >= fp_addr) {
break;
}
}
frame_pointer = next_fp;
}

Expand Down Expand Up @@ -3170,6 +3287,12 @@ module_exec(PyObject *module)
return 1;
}

#ifdef _Py_WITH_FRAME_POINTERS
if (PyModule_AddIntMacro(module, _Py_WITH_FRAME_POINTERS) < 0) {
return 1;
}
#endif

return 0;
}

Expand Down
103 changes: 103 additions & 0 deletions configure

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading