Skip to content

Commit 08d3fb5

Browse files
authored
Merge pull request #4 from godlygeek/debugger_review
Suggested changes to PEP 778
2 parents 94ac54d + be294a5 commit 08d3fb5

File tree

1 file changed

+46
-37
lines changed

1 file changed

+46
-37
lines changed

peps/pep-0778.rst

Lines changed: 46 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ Debugging Python processes in production and live environments presents unique
3030
challenges. Developers often need to analyze application behavior without
3131
stopping or restarting services, which is especially crucial for
3232
high-availability systems. Common scenarios include diagnosing deadlocks,
33-
inspecting memory usage, or investigating unexpected behavior in real-time.
33+
inspecting memory usage, or investigating unexpected behavior in real-time.
3434

3535
Very few Python tools can attach to running processes, primarily because doing
3636
so requires deep expertise in both operating system debugging interfaces and
@@ -52,10 +52,10 @@ layer of complexity. Not only do they need to implement the above mechanism,
5252
they must also understand and safely interact with CPython's runtime state,
5353
including the interpreter loop, garbage collector, thread state, and reference
5454
counting system. This combination of low-level system manipulation and
55-
high-level interpreter knowledge makes implementing Python debugging tools
55+
deep domain specific interpreter knowledge makes implementing Python debugging tools
5656
exceptionally difficult.
5757

58-
The few tools that do attempt this must resort to suboptimal and unsafe methods,
58+
The few tools that do attempt this resort to suboptimal and unsafe methods,
5959
using system debuggers like gdb and lldb to forcefully inject code. This
6060
approach is fundamentally unsafe because the injected code can execute at any
6161
point during the interpreter's execution cycle - even during critical operations
@@ -80,7 +80,7 @@ Rationale
8080

8181
Rather than forcing tools to work around interpreter limitations with unsafe
8282
code injection, we can extend CPython with a proper debugging interface that
83-
guarantees safe execution. By adding minimal thread state fields and integrating
83+
guarantees safe execution. By adding a few thread state fields and integrating
8484
with the interpreter's existing evaluation loop, we can ensure debugging
8585
operations only occur at well-defined safe points. This eliminates the
8686
possibility of crashes and corruption while maintaining zero overhead during
@@ -97,7 +97,7 @@ already `been implemented in PyPy <https://github.com/pypy/pypy/pull/5135>`__,
9797
proving both its feasibility and effectiveness. Their implementation
9898
demonstrates that we can provide safe debugging capabilities with zero runtime
9999
overhead during normal execution. The proposed mechanism not only reduces risks
100-
associated with current debugging practices but also lays the foundation for
100+
associated with current debugging approaches but also lays the foundation for
101101
future enhancements. For instance, this framework could enable integration with
102102
popular observability tools, providing real-time insights into interpreter
103103
performance or memory usage. One compelling use case for this interface is
@@ -120,7 +120,7 @@ state to coordinate debugging operations.
120120

121121
The mechanism works by having debuggers write to specific memory locations in
122122
the target process that the interpreter then checks during its normal execution
123-
cycle. When the interpreter detects a debugger wants to attach, it executes the
123+
cycle. When the interpreter detects that a debugger wants to attach, it executes the
124124
requested operations only when it's safe to do so - that is, when no internal
125125
locks are held and all data structures are in a consistent state.
126126

@@ -160,16 +160,18 @@ debugger support:
160160
.. code-block:: C
161161
162162
struct _debugger_support {
163-
uint64_t eval_breaker; /* Location of the eval breaker flag */
164-
uint64_t remote_debugger_support; /* Offset to our support structure */
165-
uint64_t debugger_pending_call; /* Where to write the pending flag */
166-
uint64_t debugger_script; /* Where to write the script */
163+
uint64_t eval_breaker; // Location of the eval breaker flag
164+
uint64_t remote_debugger_support; // Offset to our support structure
165+
uint64_t debugger_pending_call; // Where to write the pending flag
166+
uint64_t debugger_script; // Where to write the script
167167
} debugger_support;
168168
169169
These offsets allow debuggers to locate critical debugging control structures in
170-
the target process's memory space. The offsets are relative to the relevant
171-
structure address, making them valid regardless of where structures are actually
172-
loaded in memory.
170+
the target process's memory space. The ``eval_breaker`` and ``remote_debugger_support``
171+
offsets are relative to each ``PyThreadState``, while the ``debugger_pending_call``
172+
and ``debugger_script`` offsets are relative to each ``_PyRemoteDebuggerSupport``
173+
structure, allowing the new structure and its fields to be found regardless of
174+
where they are in memory.
173175

174176
Attachment Protocol
175177
-------------------
@@ -178,39 +180,43 @@ When a debugger wants to attach to a Python process, it follows these steps:
178180
1. Locate ``PyRuntime`` structure in the process:
179181
- Find Python binary (executable or libpython) in process memory (OS dependent process)
180182
- Extract ``.PyRuntime`` section offset from binary's format (ELF/Mach-O/PE)
181-
- Calculate the actual ``PyRuntime`` address in the running process by relocating the offset to the binary's load address
183+
- Calculate the actual ``PyRuntime`` address in the running process by relocating the offset to the binary's load address
182184

183-
2. Access debug offset information by read ``_Py_DebugOffsets`` table from located ``PyRuntime`` structure.
184-
185-
3. Use the offsets to locate the debugger interface structure withing the desired thread state.
185+
2. Access debug offset information by reading the ``_Py_DebugOffsets`` at the start of the ``PyRuntime`` structure.
186186

187-
4. Write control information:
188-
- Write python code to be executed.
187+
3. Use the offsets to locate the desired thread state
188+
189+
4. Use the offsets to locate the debugger interface fields within that thread state
190+
191+
5. Write control information:
192+
- Write python code to be executed into the ``debugger_script`` field in ``_PyRemoteDebuggerSupport``
189193
- Set ``debugger_pending_call`` flag in ``_PyRemoteDebuggerSupport``
190194
- Set ``_PY_EVAL_PLEASE_STOP_BIT`` in the ``eval_breaker`` field
191-
- Wait for the interpreter to reach next safe point and execute debugger code
195+
196+
Once the interpreter reaches the next safe point, it will execute the script
197+
provided by the debugger.
192198

193199
Interpreter Integration
194200
-----------------------
195201

196202
The interpreter's regular evaluation loop already includes a check of the
197-
eval_breaker flag for handling signals, periodic tasks, and other interrupts. We
203+
``eval_breaker`` flag for handling signals, periodic tasks, and other interrupts. We
198204
leverage this existing mechanism by checking for debugger pending calls only
199205
when the ``eval_breaker`` is set, ensuring zero overhead during normal execution.
200-
This check has no overhead. Indeed, profiling with Linux perf shows this branch
206+
This check has no overhead. Indeed, profiling with Linux ``perf`` shows this branch
201207
is highly predictable - the ``debugger_pending_call`` check is never taken during
202208
normal execution, allowing modern CPUs to effectively speculate past it.
203209

204210

205211
When a debugger has set both the ``eval_breaker`` flag and ``debugger_pending_call``,
206212
the interpreter will execute the provided debugging code at the next safe point
207-
and executes the provided code. This all happens in a completely safe context as
208-
any of the operations that happen in the eval breaker as the interpreter is in a
209-
consistent state:
213+
and executes the provided code. This all happens in a completely safe context, since
214+
the interpreter is guaranteed to be in a consistent state whenever the eval breaker
215+
is checked.
210216

211217
.. code-block:: c
212218
213-
/* In ceval.c */
219+
// In ceval.c
214220
if (tstate->eval_breaker) {
215221
if (tstate->remote_debugger_support.debugger_pending_call) {
216222
tstate->remote_debugger_support.debugger_pending_call = 0;
@@ -228,27 +234,29 @@ Python API
228234
----------
229235

230236
To support safe execution of Python code in a remote process without having to
231-
re-implement all these steps in every tool, this proposal extends the sys module
237+
re-implement all these steps in every tool, this proposal extends the ``sys`` module
232238
with a new function. This function allows debuggers or external tools to execute
233239
arbitrary Python code within the context of a specified Python process:
234240

235241
.. code-block:: python
236242
237243
def remote_exec(pid: int, code: str) -> None:
238-
Executes a block of Python code in a remote Python process, identified by its process ID.
244+
"""
245+
Executes a block of Python code in a given remote Python process.
239246
240-
Args:
241-
pid (int): The process ID of the target Python interpreter.
242-
code (str): A string containing the Python code to be executed.
247+
Args:
248+
pid (int): The process ID of the target Python process.
249+
code (str): A string containing the Python code to be executed.
250+
"""
243251
244252
An example usage of the API would look like:
245253

246254
.. code-block:: python
247255
248-
import sys
256+
import sys
249257
# Execute a print statement in a remote Python process with PID 12345
250258
try:
251-
sys.remote_execute(12345, "print('Hello from remote execution!')")
259+
sys.remote_exec(12345, "print('Hello from remote execution!')")
252260
except Exception as e:
253261
print(f"Failed to execute code: {e}")
254262
@@ -274,8 +282,9 @@ debuggers and tools. Some examples are:
274282
are used to read and write memory from another process. These operations are
275283
controlled by ptrace access mode checks - the same ones that govern debugger
276284
attachment. A process can only read from or write to another process's memory
277-
if it has the appropriate permissions (typically requiring the same user ID as
278-
the target process or ``CAP_SYS_PTRACE`` capability).
285+
if it has the appropriate permissions (typically requiring either root or the
286+
``CAP_SYS_PTRACE`` capability, though less security minded distributions may
287+
allow any process running as the same uid to attach).
279288

280289
* On macOS, the interface would leverage ``mach_vm_read_overwrite()`` and
281290
``mach_vm_write()`` through the Mach task system. These operations require
@@ -319,7 +328,7 @@ How to Teach This
319328
=================
320329

321330
For tool authors, this interface becomes the standard way to implement debugger
322-
attachment, replacing unsafe system debugger approaches.A section in the Python
331+
attachment, replacing unsafe system debugger approaches. A section in the Python
323332
Developer Guide could describe the internal workings of the mechanism, including
324333
the ``debugger_support`` offsets and how to interact with them using system
325334
APIs.
@@ -337,4 +346,4 @@ Copyright
337346
=========
338347

339348
This document is placed in the public domain or under the CC0-1.0-Universal
340-
license, whichever is more permissive.
349+
license, whichever is more permissive.

0 commit comments

Comments
 (0)