0x00 背景
在本地开发时,我们能很方便的借助 ide 的 debug 功能对代码进行断点调试,同时也能方便的查看每个变量的值。但是在服务器上调试时,就很难有这么方便的 ide 可以使用了。常用的做法是在不停的打 print,或者启动交互终端。很明显,这种方式的效率不可避免的很低。
这时,python 的调试器 PDB 就派上用场了,它支持在源码行间设置(有条件的)断点和单步执行,检视堆栈帧,列出源码列表,以及在任何堆栈帧的上下文中运行任意 Python 代码。同时还支持事后调试,可以在程序控制下调用。有了这样的神器,我们就能在服务器上断点调试代码了,下面让我们来看一下怎么使用 PDB。
0x01 使用方式
有两种方式可以进入到调试模式
- 无代码侵入,在执行开始时,就启动 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 自带的组件。具体使用可以看下面的截图:
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) | 列出文件源代码 | |
ll | 列出当前函数或帧的所有源代码 | ||
断点操作 | b(reak) | 设置指定文件、行数、函数的断点 | |
tbreak | 设置临时断点,参数同 b | ||
执行顺序 | r(eturn) | 继续运行,直到当前函数返回 | |
c(ontinue) | 继续运行,仅在遇到断点时停止 | ||
s(tep) | 运行当前行,在第一个可以停止的位置(在被调用的函数内部或在当前函数的下一行)停下 | ||
n(ext) | 继续运行,直到运行到当前函数的下一行,或当前函数返回为止 | ||
进入交互模式 | interact | 启动一个交互式解释器(使用 code 模块),它的全局命名空间将包含当前作用域中的所有(全局和局部)名称,使用 ctrl+D 退出交互模式 | |
其他命令 | 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/