Python Runtime Debugging


使用 GDB 调试 Python 程序

GDB 7+ 支持使用 Python 语言扩展 GDB 功能 - Extending GDB using Python , 比如,可以通过编写代码可视化展现某些库独有的数据结构(比如 C++ STL 类型)。

Python 提供的 python-gdb.py 模块就属于这类扩展,它可以用在调试对象(the inferior process)是 Python 进程或者链接了 libpython 库进程的场合。它为 GDB 提供了如下功能:

  • 默认配置下,GDB 将进程中的数据元素(几乎所有元素)都作为 PyObject * 类型处理。这时,GDB 打印 出的指针地址对用户来说并没有什么启发性。本模块使用 libpython 的实现细节,识别诸如 stringlistdict 等语言类型,并为用户提供直观的输出信息。

    In particular, given a ``gdb.Value`` corresponding to a ``PyObject *`` in the inferior
    process, we can generate a "proxy value" within the gdb process. For example, given a
    ``PyObject*`` in the inferior process that is in fact a ``PyListObject*`` holding three
    ``PyObject*`` that turn out to be ``PyStringObject*`` instances, we can generate a proxy
    value within the gdb process that is a list of strings: ``["foo", "bar", "baz"]``.
    
  • 本模块除了为 PyObject* 提供了易于理解的输出,还为 GDB 增加了几个命令: py-list, py-up, py-down, py-bt, py-print, py-locals 。详细使用方式参数 这里


这个模块的使用步骤如下:

  • 升级 GDB 版本为 7+;

  • 安装 CPython 解释器调试符号(The installed debugging symbols will be used by the CPython script for gdb in order to analyze the PyEval_EvalFrameEx frames (A frame essentially is a function call and the associated state in a form of local variables and CPU registers, etc) and map those application level functions in your code.);

    # Ubuntu
    apt install python-dbg
    
    # CentOS / RHEL
    yum install python-debuginfo
    debuginfo-install python
    
    # Pyenv
    # 自带调试符号
    
  • 使 GDB 加载 python-gdb.py (或者名为 libpython.py) ;

    • 调试系统安装的 Python 时,使用 GDB 加载 Python 进程时,该文件自动加载;
    • 调试使用 Pyenv 安装的 Python 时,设置 GDB 的 safe-path, scripts-directory 等等都无法正 常加载此扩展脚本。目前只能通过 source 命令手动加载;
  • 使用 GDB 中断运行中的进程;

单步调试工具

这类工具会中断 Python 进程执行流程。

CPU/Memory Profiler

  • stackimpact - 使用定时器定期触发信号处理函 数,然后使用 sys._current_frames() 函数收集 Python 代码上下文,并计算函数 CPU 占用。

  • pyflame - 使用 Linux 系统调用 ptrace(2) 收集 Python 程序 调用信息,然后将 profiling 数据输出为 Flame Graph 图示。该程序不能使用 pip 安装,需要手动编译。

    # compile and install *pyflame* first.
    # 该程序依赖 Python 头文件和库文件,在编译时需提供正确的文件路径
    git clone https://github.com/uber/pyflame
    cd pyflame; ./autogen.sh;
    PKG_CONFIG_PATH=/usr/local/pyenv/versions/2.7.14/lib/pkgconfig ./configure
    make; make install
    
    # 指定进程
    pyflame -p 3166 -r 0.01 -s 10 -o output.txt -p <PID>
    
    # 生成 flamegraph: https://github.com/brendangregg/FlameGraph
    /path/to/FlameGraph/flamegraph.pl output.txt>flaskt.svg
    

进程状态收集

  • lptrace - 基于 GDB 7+。它可以输出类似 strace 格式的 Python 代码级的调用信息。

  • pyrasite - 基于 GDB 7+。它向运行中的 Python 注入代码并获取执行结果。该软件包附带提供了几种方便获取某些信息的脚本。

    # Requirements: gdb 7.3+
    pip install pyrasite
    
    # See https://pyrasite.readthedocs.io/en/latest/Payloads.html for Payload examples
    
  • 使用 faulthandler 模块,在它注册的信号处理函数中打印栈信息。Python 3 将此模块作为标准模块提供。

  • 使用 Python 源代码提供 GDB Python Macros 打印应用调用栈等信息

    Put http://svn.python.org/projects/python/trunk/Misc/gdbinit into ~/.gdbinit
    然后进入 GDB 后,就可以使用 ``pystack`` 等命令了
    
  • 使用和 StackImpact 类似的方式打印各线程的调用栈:

    # https://stackoverflow.com/questions/132058/showing-the-stack-trace-from-a-running-python-application
    import threading, sys, traceback
    
    def dumpstacks(signal, frame):
        id2name = dict([(th.ident, th.name) for th in threading.enumerate()])
        code = []
        for threadId, stack in sys._current_frames().items():
            code.append("\n# Thread: %s(%d)" % (id2name.get(threadId,""), threadId))
            for filename, lineno, name, line in traceback.extract_stack(stack):
                code.append('File: "%s", line %d, in %s' % (filename, lineno, name))
                if line:
                    code.append("  %s" % (line.strip()))
        print "\n".join(code)
    
    import signal
    signal.signal(signal.SIGQUIT, dumpstacks)
    
Comments

不要轻轻地离开我,请留下点什么...

comments powered by Disqus

Published

Category

ProgLang

Tags

Contact