GDB调试CPython时在Python源代码中设置断点的最佳方式

The optimal way to set a breakpoint in the Python source code while debugging CPython by GDB

我使用 GDB 来了解 CPython 如何执行 test.py 源文件,我想停止 CPython 当它开始执行我感兴趣的操作码时。

OS: Ubuntu 18.04.2 LTS
调试器: GNU gdb (Ubuntu 8.1-0ubuntu3) 8.1.0.20180409-git


第一个问题 - 许多CPython的.py自己的文件在轮到我的test.py之前执行,所以我不能只在_PyEval_EvalFrameDefault - 有很多,所以我应该把我的文件与其他文件区分开来。

第二个问题 - 我不能像"when the filename is equal to the test.py"那样设置条件,因为文件名不是简单的C字符串,它是CPython 的 Unicode 对象,所以标准的 GDB 字符串函数不能用于比较。

此刻我在 test.py source:

的所需行执行下一个技巧来中断执行

比如我有源文件:

x = ['a', 'b', 'c']

# I want to set the breakpoint at this line.

for e in x:
    print(e)

我将二进制左移运算符添加到代码中:

x = ['a', 'b', 'c']

# Added for breakpoint   
a = 12
b = 2 << a

for e in x:
    print(e)

然后,通过 GDB 命令跟踪 Python/ceval.c 文件中的 BINARY_LSHIFT 操作码执行:

break ceval.c:1327

我选择了 BINARY_LSHIFT 操作码,因为它在代码中很少使用。因此,我可以快速到达 .py 文件的所需部分 - 它在我的 test.py.

之前执行的所有其他 .py 模块中发生一次

我看起来更直接的方法是做同样的事情,所以 问题:

  1. 我可以捕捉到 test.py 开始执行的时刻吗?我应该提一下,test.py 文件名出现在不同的阶段:解析、编译、执行。因此,能够在任何阶段中断 CPython 执行也是很好的。
  2. 我可以指定 test.py 的那一行吗? .c 个文件很容易,但 .py 个文件不方便。

我的想法是使用 C 扩展,以便在 python 脚本中设置 C 断点(类似于 pdb.set_trace() or breakpoint(),因为 Python3.7),我称之为 cbreakpoint.

考虑以下 python-脚本:

#example.py
from cbreakpoint import cbreakpoint

cbreakpoint(breakpoint_id=1)
print("hello")
cbreakpoint(breakpoint_id=2)

在gdb中可以这样使用:

>>> gdb --args python example.py
[gdb] b cbreakpoint
[gdb] run

现在,调试器将停止在 cbreakpoint(breakpoint_id=1)cbreakpoint(breakpoint_id=2)

这里是概念证明,用 Cython 编写,以避免其他需要的样板代码:

#cbreakpoint.pyx
cdef extern from *:
    """
    long long last_breakpoint_id = -1;
    void cbreakpoint(long long breakpoint_id){
         last_breakpoint_id = breakpoint_id;
    }
    """
    void c_cbreakpoint "cbreakpoint"(long long breakpoint_id)


def cbreakpoint(breakpoint_id = 0):
    c_cbreakpoint(breakpoint_id)

可以通过以下方式就地构建:

cythonize -i cbreakpoint.pyx

如果未安装 Cython,我已在 github 上上传了一个不依赖于 Cython 的版本(此 post 的代码太多)。

也可以有条件地中断,给定 breakpoint_id,即:

>>> gdb --args python example.py
[gdb] break src/cbreakpoint.c:595 if breakpoint_id == 2
[gdb] run

将仅在打印 hello 后中断 - 在 cbreakpointid=2 处(而 cbreakpointid=1 将被跳过)。根据 Cython 版本的不同,该行可能会有所不同,但是一旦 gdb 停止在 cbreakpoint.

就可以找到

它也可以在没有任何额外模块的情况下做类似的事情:

  1. 添加 breakpointimport pdb; pdb.set_trace() 而不是 cbreakpoint
  2. gdb --args python example.py + 运行
  3. pdb中断程序时,在gdb中按Ctrl+C中断。
  4. 激活 gdb 中的断点。
  5. gdb 中继续,然后在 pdb 中继续(即 c+enter 两次 )。

一个小问题是,之后在pdb中可能会遇到断点,所以第一种方法更稳健一些。