@@ -30,7 +30,7 @@ Debugging Python processes in production and live environments presents unique
3030challenges. Developers often need to analyze application behavior without
3131stopping or restarting services, which is especially crucial for
3232high-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
3535Very few Python tools can attach to running processes, primarily because doing
3636so 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,
5252they must also understand and safely interact with CPython's runtime state,
5353including the interpreter loop, garbage collector, thread state, and reference
5454counting 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
5656exceptionally 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,
5959using system debuggers like gdb and lldb to forcefully inject code. This
6060approach is fundamentally unsafe because the injected code can execute at any
6161point during the interpreter's execution cycle - even during critical operations
@@ -80,7 +80,7 @@ Rationale
8080
8181Rather than forcing tools to work around interpreter limitations with unsafe
8282code 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
8484with the interpreter's existing evaluation loop, we can ensure debugging
8585operations only occur at well-defined safe points. This eliminates the
8686possibility 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>`__,
9797proving both its feasibility and effectiveness. Their implementation
9898demonstrates that we can provide safe debugging capabilities with zero runtime
9999overhead 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
101101future enhancements. For instance, this framework could enable integration with
102102popular observability tools, providing real-time insights into interpreter
103103performance or memory usage. One compelling use case for this interface is
@@ -120,7 +120,7 @@ state to coordinate debugging operations.
120120
121121The mechanism works by having debuggers write to specific memory locations in
122122the 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
124124requested operations only when it's safe to do so - that is, when no internal
125125locks 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
174176Attachment Protocol
175177-------------------
@@ -178,39 +180,43 @@ When a debugger wants to attach to a Python process, it follows these steps:
1781801. 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
193199Interpreter Integration
194200-----------------------
195201
196202The 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
198204leverage this existing mechanism by checking for debugger pending calls only
199205when 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
201207is highly predictable - the ``debugger_pending_call `` check is never taken during
202208normal execution, allowing modern CPUs to effectively speculate past it.
203209
204210
205211When a debugger has set both the ``eval_breaker `` flag and ``debugger_pending_call ``,
206212the 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
230236To 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
232238with a new function. This function allows debuggers or external tools to execute
233239arbitrary 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
321330For 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
323332Developer Guide could describe the internal workings of the mechanism, including
324333the ``debugger_support `` offsets and how to interact with them using system
325334APIs.
@@ -337,4 +346,4 @@ Copyright
337346=========
338347
339348This 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