Skip to content

python pdb使用手册

Posted on:2021年8月27日 at 16:33 (10 min read)

0x00 背景

在本地开发时,我们能很方便的借助 ide 的 debug 功能对代码进行断点调试,同时也能方便的查看每个变量的值。但是在服务器上调试时,就很难有这么方便的 ide 可以使用了。常用的做法是在不停的打 print,或者启动交互终端。很明显,这种方式的效率不可避免的很低。

这时,python 的调试器 PDB 就派上用场了,它支持在源码行间设置(有条件的)断点和单步执行,检视堆栈帧,列出源码列表,以及在任何堆栈帧的上下文中运行任意 Python 代码。同时还支持事后调试,可以在程序控制下调用。有了这样的神器,我们就能在服务器上断点调试代码了,下面让我们来看一下怎么使用 PDB。

0x01 使用方式

有两种方式可以进入到调试模式

  1. 无代码侵入,在执行开始时,就启动 pdb
python -m pdb script.py

例如有个脚本文件为 script.py

def power(a):
  return a*a

if __name__ == '__main__':
   b = power(10)
   print(b)

使用上面命令进入脚本

$ python -m pdb script.py
> /Users/vlean/worker/py/script.py(2)<module>() #进入到脚本,显示,当前运行的文件和行数
-> def power(a): # 运行到的文件
(Pdb) n  #n 执行下一行
> /Users/vlean/worker/py/script.py(5)<module>()
-> if __name__ == '__main__':
(Pdb) n
> /Users/vlean/worker/py/script.py(6)<module>()
-> b = power(10)
(Pdb) n
> /Users/vlean/worker/py/script.py(7)<module>()
-> print(b)
(Pdb) pp b  # 打印b的值
100
(Pdb)

2.有代码侵入,在特定的代码位置引入 pdb 包 还是上面的脚本,引入 pdb 包,设置断点

import pdb

def power(a):
  pdb.set_trace() #打断点
  return a*a

if __name__ == '__main__':
   b = power(10)
   print(b)

正常执行命令,效果如下

$ python script.py
> /Users/vlean/worker/py/script.py(5)power() #运行到断点处,才进入debug
-> return a*a
(Pdb) pp a
10
(Pdb) n
--Return--
> /Users/vlean/worker/py/script.py(5)power()->100
-> return a*a
(Pdb) n
> /Users/vlean/worker/py/script.py(9)<module>()
-> print(b)
(Pdb) pp b
100
(Pdb)

如果你想要使用 ipython 一样,也有一个叫 ipdb 的扩展,他的使用界面和 ipython 类似,也提供了很多优化。但是需要额外安装依赖,不像 pdb 是 python 自带的组件。具体使用可以看下面的截图: image-20210906163601464

0x02 常用命令

在 pdb 交互界面,按 h 能看到全部的命令,如下:

(Pdb) h

Documented commands (type help <topic>):
========================================
EOF    c          d        h         list      q        rv       undisplay
a      cl         debug    help      ll        quit     s        unt
alias  clear      disable  ignore    longlist  r        source   until
args   commands   display  interact  n         restart  step     up
b      condition  down     j         next      return   tbreak   w
break  cont       enable   jump      p         retval   u        whatis
bt     continue   exit     l         pp        run      unalias  where

Miscellaneous help topics:
==========================
exec  pdb

常用的主要是设置断点,查看源代码,顺序控制,进入交互模式等

类型命令说明
查看源代码l(ist)列出文件源代码image-20210906163639538
ll列出当前函数或帧的所有源代码
断点操作b(reak)设置指定文件、行数、函数的断点image-20210906163707536
tbreak设置临时断点,参数同 b
执行顺序r(eturn)继续运行,直到当前函数返回
c(ontinue)继续运行,仅在遇到断点时停止
s(tep)运行当前行,在第一个可以停止的位置(在被调用的函数内部或在当前函数的下一行)停下
n(ext)继续运行,直到运行到当前函数的下一行,或当前函数返回为止
进入交互模式interact启动一个交互式解释器(使用 code 模块),它的全局命名空间将包含当前作用域中的所有(全局和局部)名称,使用 ctrl+D 退出交互模式image-20210906163737193
其他命令p
pp打印变量值,也可以直接用 print(变量)
whatis打印变量类型
restart重启脚本
alias给一些命令设置别名

0x03 进阶使用

1.别名

创建一个标识为 name 的别名来执行 command。 执行的命令不可加上引号。 可替换形参可通过 %1, %2 等来标示,而 %* 会被所有形参所替换。别名允许嵌套并可包含能在 pdb 提示符下合法输入的任何内容。别名会递归地应用到命令行的第一个单词;行内的其他单词不会受影响。

摘抄个官方的示例:

# 打印对象属性
alias pi for k in %1.__dict__.keys(): print("%1.",k,"=",%1.__dict__[k])

这里有个示例,让我们一起看下:

class Spam():
    """Run Away! Run Away!"""
    def __init__(self, ham):
        self.ham = ham

    def egg(self):
        print(self.ham)



if __name__ == '__main__':
  spam = Spam('ham')
  spam.egg()

执行 pdb 调试

$ python -m pdb spam.py
...
(Pdb) pp spam # 使用pp查看实例spam
<__main__.Spam instance at 0x1101c7c20>
# 声明别名
(Pdb) alias pi for k in %1.__dict__.keys(): print("%1.",k,"=",%1.__dict__[k])
(Pdb) pi spam # 别名查看spam
('spam.', 'ham', '=', 'ham')
(Pdb) pi Spam # 别名查看Spam类,这样我们就能看到更详细的信息了
('Spam.', '__module__', '=', '__main__')
('Spam.', 'egg', '=', <function egg at 0x11036f500>)
('Spam.', '__doc__', '=', 'Run Away! Run Away!')
('Spam.', '__init__', '=', <function __init__ at 0x11036f668>)

2..pdbrc文件

.pdbrc 可以存放一 pdb 命令,例如设置断点,设置命令别名。如果文件 .pdbrc 存在于用户主目录或当前目录中,则将其读入并执行,等同于在调试器提示符下键入该文件。若两个文件都存在则首先读取主目录中的文件,且本地文件可以覆盖其中定义的别名。

例如上面设置的别名,进入文件后要执行的断点,我们都可以放到.pdbrc 文件内。

# Print instance variables (usage "pi classInst")
alias pi for k in %1.__dict__.keys(): print("%1.",k,"=",%1.__dict__[k])
# Print instance variables in self
alias ps pi self

# 时间戳格式化,是的也支持部分python命令
import time
alias dt time.strftime("%Y-%m-%d %H:%M:%S",time.localtime(%1))

# 断点设置
b 10
b cube

# 继续执行
c

再执行最开始的示例文件时,就不需要再侵入代码设置断点,同时,也不需要进入代码后先设置断点,再执行调试了。

3.事后调试

我们往往不知道异常发生在什么地方,这时候就需要用到事后调试技术了。它能在异常发生时,获取 traceback 回溯对象的执行过程。当然,因为此时异常已经发生,我们不能在调试时重新执行代码或者断点调试,只能回溯堆栈信息。这样说可能不太清除,我们来看一个示例。

有这样一个脚本文件

class Math:
  def div(self, x, y=0):
    print(x, y)
    z = x/y
    return z

if __name__ == '__main__':
  try:
    Math().div(100)
  except Exception as e:
    import pdb; pdb.post_mortem(e.__traceback__) # 也可以替换为 pdb.pm(),会自动获取当前在执行的异常

现在,执行脚本即可进入到堆栈内。

 $ python task.py
100 0
> /Users/vlean/worker/py/oa/task.py(4)div()
-> z = x/y
(Pdb) w  # 查看当前堆栈位置
  /Users/vlean/worker/py/oa/task.py(9)<module>()
-> Math().div(100)
> /Users/vlean/worker/py/oa/task.py(4)div()
-> z = x/y
(Pdb) u  # 移动到堆栈上一帧
> /Users/vlean/worker/py/oa/task.py(9)<module>()
-> Math().div(100)
(Pdb) d  # 移动到堆栈下一帧
> /Users/vlean/worker/py/oa/task.py(4)div()
-> z = x/y
(Pdb) p self # 查看math实例
<__main__.Math object at 0x109a856d8>
(Pdb) pp x
100
(Pdb) pp y
0

4.远程调试

如果需要调试程序,但是又没有远端机器的运行权限,可以通过借助 remote-pdb 包,来实现远程调试。 在代码内引入 remote_pdb 包,然后设置端口地址

def power(a):
  return a*a

def cube(a):
  return a*a*a

if __name__ == '__main__':
   import remote_pdb as rpdb
   rpdb.set_trace('0.0.0.0', 9527) #设置监听端口
   b = power(10)
   print(b)
   c = cube(b)
   print(c)

启动程序,可以看到程序正等待连接

$ python task.py
RemotePdb session open at 0.0.0.0:9527, waiting for connection ...
RemotePdb session open at 0.0.0.0:9527, waiting for connection ...

执行连接,之后就可以在本地进行代码调试了

$ telnet 0.0.0.0 9527
Trying 0.0.0.0...
Connected to 0.0.0.0.
Escape character is '^]'.
Breakpoint 1 at /Users/vlean/worker/py/oa/task.py:10
Breakpoint 2 at /Users/vlean/worker/py/oa/task.py:5
> /Users/vlean/worker/py/oa/task.py(6)cube()
-> return a*a*a
(Pdb)

0xff 参考文档

https://docs.python.org/zh-cn/3/library/pdb.html#pdbcommand-list https://pypi.org/project/ipdb/ https://pypi.org/project/pdbpp/ https://pypi.org/project/remote-pdb/ https://xmfbit.github.io/2017/08/21/debugging-with-ipdb/