资讯专栏INFORMATION COLUMN

使用pyrasite进行python进程调试,改变运行中进程的代码

Y3G / 3415人阅读

摘要:反之,如果是使用并通过进行调用,那么就应当进行修改如果一个函数内部有阻塞式的,那么改变这个函数是没有用的,显然要应用改变的对象需要对象下一次被调用,这个不难理解但是容易漏想到

后端开发中有时会遇到这种情况:进程运行中偶现,重启进程问题就消失;或者是,进程一定要运行一段时间才会出现问题;又或是,极难复现的问题出现了,然而已有的log不足以定位

对于这些情况,尽管大部分时候,我们可以通过在可能的地方加log,然后重启进程等待问题复现,但这样相对被动。我们都知道如果要调试C/C++程序,gdb attach上进程就可以,而python虽然有相似的工具pdb,但它无法附加到一个进程上,必须要用pdb启动进程,在实际环境中显然不管用,那么python是否有类似的办法来改变运行中进程的代码呢?这样我们就可以通过实时加log来定位问题,这样几乎可以解决python层面的任何问题

可以参考两篇文章:

https://mozillazg.com/2018/07...
https://mozillazg.com/2017/07...

简单来说,可以直接用gdb使用类似调试c程序的方式,但要求python进程是使用python-debug这种版本的python,同样不够实用。这里介绍博客中提到的“纯gdb”的方式,通过github上一个开源python包pyrasite,本质上是通过gdb的-eval-command和它的PyRun_SimpleString来向进程注入代码。

这个库有一些附加功能,可以通过它的文档去了解。这里只说实现进程注入的核心,是其中一个很短的文件injector.py,这里去掉了原文件中用于windows平台的一段代码,我们这里只考虑linux,核心代码如下:

import os
import subprocess
import platform

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)
        

这个函数做的事很简单,不难看懂,所以,我们需要做的就是调用这个函数,传入pid和文件名,文件是一个你要对这个进程执行的python代码。现在我们运行一个很简单的python进程test.py

import time
def b():
  print("b")

while 1:
  b()
  time.sleep(1)
  

然后创建一个文件patch.py

print("injecting")
def newb():
  print("new b")
b = newb

injector.py的末尾加上一段,以便接收命令行调用:

import sys
pid = sys.argv[1]
filename = sys.argv[2]

inject(int(pid), filename)

通过ps aux|grep test.py查看上面进程的pid,然后执行python injector.py pid patch.py,为方便反复测试可以这样:

pid=`ps aux | grep test.py | grep -v grep | awk "{print $2}"`;python injector.py $pid patch.py;echo $pid injected

输出如下:

至此就实现了进程注入。

注意点:

修改类或类方法和函数同理,改变类的方法时,直接使用类名classA.method = new_method会将变化应用到所有实例,注意对类方法来说在patch.py中定义时也要加上self参数

patch.py中,我们可以直接对b赋值,因为我们gdb进入一个进程后,所在的上下文环境就是该进程的入口模块,可以通过打印globals()来看到有哪些全局变量,这些就是可以直接访问的对象。如果是在一个普通的业务进程中,必然有大量import,这种情况下你需要import相应模块再对该模块的函数或类进行修改,如import x.y.z as z; z.b = newb

特别需要注意的是,如果一个模块A使用了from B import func,那么如果你想改变A中运行的func,需要import A; A.func = newfunc,像这样改变B是没有用的:import B; B.func = newfunc,因为from .. import ..会将对象复制一份到本地命名空间。反之,如果A是使用import B并通过B.func进行调用,那么就应当import B进行修改

如果一个函数内部有阻塞式的while True,那么改变这个函数是没有用的,显然要应用改变的对象需要对象下一次被调用,这个不难理解但是容易漏想到

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/43988.html

相关文章

  • 将任意Bytecode注入运行Python进程

    摘要:在调试程序的时候,一般我们只能通过以下几种方式进行调试程序中已经有的日志在代码中插入但是以上的方法也有不方便的地方,比如对于已经在运行中的程序,就不可能停止程序后加入调试代码和增加新的日志从的项目得到灵感,尝试对正在运行的进程插入代码,在程 在调试 Python 程序的时候,一般我们只能通过以下几种方式进行调试: 程序中已经有的日志 在代码中插入 import pdb; pdb.s...

    endiat 评论0 收藏0
  • Python 调试方法

    摘要:背景这几天一直在查一个线上程序住的问题这个程序总是在运行分钟后住通过以下的一些调试手段发现是打日志的时候因为满被了日志是默认打到的无论日志级别而我这个程序是被另一个程序调起的父进程没有接收子进程的导致了被打满在调试的过程中用到了以下几种调试 FROM http://kamushin.github.io/debug/python.html 背景 这几天一直在查一个线上程序 hang 住的...

    klivitamJ 评论0 收藏0
  • 【openQPA】教你DIY进程流量捕获工具

    摘要:开源项目起因最近做病毒分析的时候遇到远控马,需要记录连接的远程地址用火绒剑或者可以看到一部分,但是我想要更全面的信息,于是捣鼓了和。使用比较简单,但是只能看到的流量,虽然能捕获所有流量,但没法过滤特定进程的包,而且过滤规则对我来说太复杂了。 开源项目QPA 起因最近做病毒分析的时候遇到远控马,需要记录连接的远程地址!用火绒剑或者ProcessMonitr可以看到一部分,但是我想要更全面...

    SimonMa 评论0 收藏0

发表评论

0条评论

Y3G

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<