通过 pyrasite 查看linux下正在运行的python进程栈,分析线上问题,使用方法及踩坑指南 (python3.8下实测通过)

1,168 阅读1分钟

前言

pyrasite工具通过gdb工具和cpython提供的PyRun_SimpleString方法,能够做到attach到正在运行的python进程中,查看正在运行的python堆栈,以及注入修改代码的功能。

遇到的问题:

  1. 执行pyrasite $pid dump_stacks.py命令后,被注入的进程控制台无任何输出
  2. 执行pyrasite-shell $pid命令后,卡住无反应,ctrl+c之后,python栈卡在socket的accept方法

解决方法如下:

参考文档

官方文档:pyrasite.readthedocs.io/en/latest/i…

针对不同的操作系统,需要做一些额外的设置:参考pyrasite.readthedocs.io/en/latest/I…

安装方法:

pip install pyrasite pyrasite-gui

使用方法

使用 pyrasite $pid xxx.py方法,在被attach的进程中执行一段python代码

例如: image.png

dump_stacks是pyrasite提供的通用payload,执行pyrasite --list-payload命令,可以列出支持的payload。

除了通用payload之外,也可以实现自己的payload,方便线上环境debug。

image.png

使用 pyrasite-shell $pid方法,会在被attach的进程中新启动一个线程来和控制台交互,实时执行想要的代码,及观测想要观察的数据,更改内容等

例如: image.png

使用pyrasite-memory-viewer $pid查看内存信息

使用前,需要先pip install urwid

遇到的问题及解决方法

  1. ubuntu下执行时报错

image.png

根据提示, 执行echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope命令即可

  1. 执行pyrasite $pid dump_stacks.py后, 没有任何输出, 终端并没有打印栈信息

image.png

执行pyrasite-shell $pid后,卡住,ctrl+c后发现栈卡在socket和accept方法上:

image.png

查看源码:

import os
import subprocess

def inject(pid, filename, verbose=False, gdb_prefix=''):
    """Executes a file in a running Python process."""
    filename = os.path.abspath(filename)
    gdb_cmds = [
        'PyGILState_Ensure()',
        'PyRun_SimpleString("'
            'import sys; sys.path.insert(0, \"%s\"); '
            'sys.path.insert(0, \"%s\"); '
            'exec(open(\"%s\").read())")' %
                (os.path.dirname(filename),
                os.path.abspath(os.path.join(os.path.dirname(__file__), '..')),
                filename),
        'PyGILState_Release($1)',
        ]
    p = subprocess.Popen('%sgdb -p %d -batch %s' % (gdb_prefix, pid,
        ' '.join(["-eval-command='call %s'" % cmd for cmd in gdb_cmds])),
        shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    out, err = p.communicate()
    if verbose:
        print(out)
        print(err)

其使用的方法是通过gdb工具执行PyRun_SimpleString方法来注入python代码,同时发现使用--verbose可以输出详细信息:

image.png

根据输出的信息,可以看到报错内容 PyGILState_Ensure' has unknown return type; cast the call to its declared return type

尝试使用gdb -p $pid命令attach到进程中,有下面的报错:

image.png

搜索后得知,这个是由于linux机器上安装的python版本编译时没有加-g选项,gdb工具不能通过debug信息找到函数的定义来得知方法的返回类型,因此报错。

解决方法:

  1. 使用编译时带了-g选项的python版本,方法:源码下载后执行./configure --with-pydebug,添加with-pydebug参数
  2. yum或者apt下载对应版本的python debug包?(这个并没有找到办法,尝试了下载python38-debug之后仍然不行)
  3. 查看python源码找到命令中使用到的三个函数的定义,找到函数的返回类型,手动指定其返回类型。

通过第三种方法解决的方式:

PyGILState_Ensure、PyGILState_Release

// PyGILState_Ensure函数返回值为PyGILState_STATE
PyGILState_STATE
PyGILState_Ensure(void)
{
...
}

// PyGILState_Release函数返回值为void
void
PyGILState_Release(PyGILState_STATE oldstate)
{
...
}

继续查找PyGILState_STATE定义:

// 枚举类型直接设定返回值为int即可
typedef
    enum {PyGILState_LOCKED, PyGILState_UNLOCKED}
        PyGILState_STATE;

PyRun_SimpleString


PyAPI_FUNC(int)
PyRun_SimpleString(const char *s)
{
    return PyRun_SimpleStringFlags(s, NULL);
}

// PyRun_SimpleString函数返回值为int
define PyAPI_FUNC(RTYPE) RTYPE

因此, 补充上返回值, inject.py代码可以修改为:

    gdb_cmds = [
        '(int) PyGILState_Ensure()',
        '(int) PyRun_SimpleString("'
            'import sys; sys.path.insert(0, \"%s\"); '
            'sys.path.insert(0, \"%s\"); '
            'exec(open(\"%s\").read())")' %
                (os.path.dirname(filename),
                os.path.abspath(os.path.join(os.path.dirname(__file__), '..')),
                filename),
        '(void) PyGILState_Release($1)',
        ]

通过python -m site命令,找pyrasite安装路径

image.png

修改/home/zk/venv/lib/python3.8/site-packages/pyrasite/injector.py的即可,注意不要直接复制代码到py中,会有问题,用vim单独在每一行前面加(int) 和(void):

image.png

可以看到执行成功!被监控程序打印了栈。

再次启动进程,使用pyrasite-shell $pid,也能正常连接到进程中,执行python代码

image.png