深入理解python虚拟机:调试器实现原理与源码分析
在本篇文章当中主要给大家介绍 python 语言当中调试器的实现原理,通过了解一个语言的调试器的实现原理我们可以更加深入的理解整个语言的运行机制,可以帮助我们更好的理解程序的执行。
让程序停下来
def m99(:
for i in range(1, 10:
for j in range(1, i + 1:
print(f"{i}x{j}={i*j}", end='\t'
print(
if __name__ == '__main__':
m99(
现在执行命令 python -m pdb pdbusage.py
就可以对上面的程序进行调试:
(py3.8 ➜ pdb_test git:(master ✗ python -m pdb pdbusage.py
> /Users/xxxx/Desktop/workdir/dive-into-cpython/code/pdb_test/pdbusage.py(3<module>(
-> def m99(:
(Pdb s
> /Users/xxxx/Desktop/workdir/dive-into-cpython/code/pdb_test/pdbusage.py(10<module>(
-> if __name__ == '__main__':
(Pdb s
> /Users/xxxx/Desktop/workdir/dive-into-cpython/code/pdb_test/pdbusage.py(11<module>(
-> m99(
(Pdb s
--Call--
> /Users/xxxx/Desktop/workdir/dive-into-cpython/code/pdb_test/pdbusage.py(3m99(
-> def m99(:
(Pdb s
> /Users/xxxx/Desktop/workdir/dive-into-cpython/code/pdb_test/pdbusage.py(4m99(
-> for i in range(1, 10:
(Pdb s
> /Users/xxxx/Desktop/workdir/dive-into-cpython/code/pdb_test/pdbusage.py(5m99(
-> for j in range(1, i + 1:
(Pdb s
> /Users/xxxx/Desktop/workdir/dive-into-cpython/code/pdb_test/pdbusage.py(6m99(
-> print(f"{i}x{j}={i*j}", end='\t'
(Pdb p i
1
(Pdb
当然你也可以在 IDE 当中进行调试:
cpython 王炸机制 —— tracing
现在的问题是,上面的程序是怎么在程序执行时停下来的呢?
设置系统的跟踪函数,允许在 Python 中实现一个 Python 源代码调试器。该函数是线程特定的;为了支持多线程调试,必须对每个正在调试的线程注册一个跟踪函数,使用 settrace( 或者使用 threading.settrace( 。
跟踪函数在每次进入新的局部作用域时被调用(事件设置为'call');它应该返回一个引用,用于新作用域的本地跟踪函数,或者如果不想在该作用域中进行跟踪,则返回None。
事件的含义如下:
-
line,将要执行一行新的代码,参数 arg 的值为 None 。
-
exception,发生了异常。调用本地跟踪函数;arg是一个元组(exception,value,traceback);返回值指定了新的本地跟踪函数。
-
c_call,一个 c 函数将要被调用。
自己动手实现一个简单的调试器
在本小节当中我们将实现一个非常简单的调试器帮助大家理解调试器的实现原理。调试器的实现代码如下所示,只有短短几十行却可以帮助我们深入去理解调试器的原理,我们先看一下实现的效果在后文当中再去分析具体的实现:
import sys
file = sys.argv[1]
with open(file, "r+" as fp:
code = fp.read(
lines = code.split("\n"
def do_line(frame, event, arg:
print("debugging line:", lines[frame.f_lineno - 1]
return debug
def debug(frame, event, arg:
if event == "line":
while True:
_ = input("(Pdb"
if _ == 'n':
return do_line(frame, event, arg
elif _.startswith('p':
_, v = _.split(
v = eval(v, frame.f_globals, frame.f_locals
print(v
elif _ == 'q':
sys.exit(0
return debug
if __name__ == '__main__':
sys.settrace(debug
exec(code, None, None
sys.settrace(None
在上面的程序当中使用如下:
- 输入 n 执行一行代码。
- p name 打印变量 name 。
- q 退出调试。
(py3.10 ➜ pdb_test git:(master ✗ python mydebugger.py pdbusage.py
(Pdbn
debugging line: def m99(:
(Pdbn
debugging line: if __name__ == '__main__':
(Pdbn
debugging line: m99(
(Pdbn
debugging line: for i in range(1, 10:
(Pdbn
debugging line: for j in range(1, i + 1:
(Pdbn
debugging line: print(f"{i}x{j}={i*j}", end='\t'
1x1=1 (Pdbn
debugging line: for j in range(1, i + 1:
(Pdbp i
1
(Pdbp j
1
(Pdbq
(py3.10 ➜ pdb_test git:(master ✗
现在我们来分析一下我们自己实现的简易版本的调试器,在前文当中我们已经提到了 sys.settrace 函数,调用这个函数时需要传递一个函数作为参数,被传入的函数需要接受三个参数:
-
event,事件的类别,这一点在前面的文件当中已经提到了。
-
同时需要注意的是这个函数也需要有一个返回值,python 虚拟机在下一次事件发生的时候会调用返回的这个函数,如果返回 None 那么就不会在发生事件的时候调用 tracing 函数了,这是代码当中为什么在 debug 返回 debug 的原因。
python 官方调试器源码分析
python 官方的调试器为 pdb 这个是 python 标准库自带的,我们可以通过 python -m pdb xx.py
去调试文件 xx.py 。这里我们只分析核心代码:
def run(self, cmd, globals=None, locals=None:
"""Debug a statement executed via the exec( function.
globals defaults to __main__.dict; locals defaults to globals.
"""
if globals is None:
import __main__
globals = __main__.__dict__
if locals is None:
locals = globals
self.reset(
if isinstance(cmd, str:
cmd = compile(cmd, "<string>", "exec"
sys.settrace(self.trace_dispatch
try:
exec(cmd, globals, locals
except BdbQuit:
pass
finally:
self.quitting = True
sys.settrace(None
上面的函数主要是使用 sys.settrace 函数进行 tracing 操作,当有事件发生的时候就能够捕捉了。在上面的代码当中 tracing 函数为 self.trace_dispatch 我们再来看这个函数的代码:
def trace_dispatch(self, frame, event, arg:
"""Dispatch a trace function for debugged frames based on the event.
This function is installed as the trace function for debugged
frames. Its return value is the new trace function, which is
usually itself. The default implementation decides how to
dispatch a frame, depending on the type of event (passed in as a
string that is about to be executed.
The event can be one of the following:
line: A new line of code is going to be executed.
call: A function is about to be called or another code block
is entered.
return: A function or other code block is about to return.
exception: An exception has occurred.
c_call: A C function is about to be called.
c_return: A C function has returned.
c_exception: A C function has raised an exception.
For the Python events, specialized functions (see the dispatch_*(
methods are called. For the C events, no action is taken.
The arg parameter depends on the previous event.
"""
if self.quitting:
return # None
if event == 'line':
print("In line"
return self.dispatch_line(frame
if event == 'call':
print("In call"
return self.dispatch_call(frame, arg
if event == 'return':
print("In return"
return self.dispatch_return(frame, arg
if event == 'exception':
print("In execption"
return self.dispatch_exception(frame, arg
if event == 'c_call':
print("In c_call"
return self.trace_dispatch
if event == 'c_exception':
print("In c_exception"
return self.trace_dispatch
if event == 'c_return':
print("In c_return"
return self.trace_dispatch
print('bdb.Bdb.dispatch: unknown debugging event:', repr(event
return self.trace_dispatch
从上面的代码当中可以看到每一种事件都有一个对应的处理函数,在本文当中我们主要分析 函数 dispatch_line,这个处理 line 事件的函数。
def dispatch_line(self, frame:
"""Invoke user function and return trace function for line event.
If the debugger stops on the current line, invoke
self.user_line(. Raise BdbQuit if self.quitting is set.
Return self.trace_dispatch to continue tracing in this scope.
"""
if self.stop_here(frame or self.break_here(frame:
self.user_line(frame
if self.quitting: raise BdbQuit
return self.trace_dispatch
这个函数首先会判断是否需要在当前行停下来,如果需要停下来就需要进入 user_line 这个函数,后面的调用链函数比较长,我们直接看最后执行的函数,根据我们使用 pdb 的经验来看,最终肯定是一个 while 循环让我们可以不断的输入指令进行处理:
def cmdloop(self, intro=None:
"""Repeatedly issue a prompt, accept input, parse an initial prefix
off the received input, and dispatch to action methods, passing them
the remainder of the line as argument.
"""
print("In cmdloop"
self.preloop(
if self.use_rawinput and self.completekey:
try:
import readline
self.old_completer = readline.get_completer(
readline.set_completer(self.complete
readline.parse_and_bind(self.completekey+": complete"
except ImportError:
pass
try:
if intro is not None:
self.intro = intro
print(f"{self.intro = }"
if self.intro:
self.stdout.write(str(self.intro+"\n"
stop = None
while not stop:
print(f"{self.cmdqueue = }"
if self.cmdqueue:
line = self.cmdqueue.pop(0
else:
print(f"{self.prompt = } {self.use_rawinput}"
if self.use_rawinput:
try:
# 核心逻辑就在这里 不断的要求输入然后进行处理
line = input(self.prompt # self.prompt = '(Pdb'
except EOFError:
line = 'EOF'
else:
self.stdout.write(self.prompt
self.stdout.flush(
line = self.stdin.readline(
if not len(line:
line = 'EOF'
else:
line = line.rstrip('\r\n'
line = self.precmd(line
stop = self.onecmd(line # 这个函数就是处理我们输入的字符串的比如 p n 等等
stop = self.postcmd(stop, line
self.postloop(
finally:
if self.use_rawinput and self.completekey:
try:
import readline
readline.set_completer(self.old_completer
except ImportError:
pass
def onecmd(self, line:
"""Interpret the argument as though it had been typed in response
to the prompt.
This may be overridden, but should not normally need to be;
see the precmd( and postcmd( methods for useful execution hooks.
The return value is a flag indicating whether interpretation of
commands by the interpreter should stop.
"""
cmd, arg, line = self.parseline(line
if not line:
return self.emptyline(
if cmd is None:
return self.default(line
self.lastcmd = line
if line == 'EOF' :
self.lastcmd = ''
if cmd == '':
return self.default(line
else:
try:
# 根据下面的代码可以分析了解到如果我们执行命令 p 执行的函数为 do_p
func = getattr(self, 'do_' + cmd
except AttributeError:
return self.default(line
return func(arg
现在我们再来看一下 do_p 打印一个表达式是如何实现的:
def do_p(self, arg:
"""p expression
Print the value of the expression.
"""
self._msg_val_func(arg, repr
def _msg_val_func(self, arg, func:
try:
val = self._getval(arg
except:
return # _getval( has displayed the error
try:
self.message(func(val
except:
self._error_exc(
def _getval(self, arg:
try:
# 看到这里就破案了这不是和我们自己实现的 pdb 获取变量的方式一样嘛 都是
# 使用当前执行栈帧的全局和局部变量交给 eval 函数处理 并且将它的返回值输出
return eval(arg, self.curframe.f_globals, self.curframe_locals
except:
self._error_exc(
raise
总结
在本篇文章当中我们主要分析 python 当中实现调试器的原理,并且通过一个几十行的代码实现了一个非常简单的调试器,这可以深入帮助我们理解调试器实现的细节,这让我们对于程序设计语言的认识又加深了一点。最后简单的介绍了一下 python 自己的调试器 pdb,但是有一点遗憾的目前 pdb 还不能够支持直接调试 python 字节码,但是在 python 虚拟机当中已经有调试字节码的事件了,相信在未来应该可以直接调试字节码了。
本篇文章是深入理解 python 虚拟机系列文章之一,文章地址:https://github.com/Chang-LeHung/dive-into-cpython
关注公众号:一无是处的研究僧,了解更多计算机(Java、Python、计算机系统基础、算法与数据结构)知识。