返回顶部
首页 > 资讯 > 后端开发 > Python >Python虚拟机中调试器的实现原理是什么
  • 821
分享到

Python虚拟机中调试器的实现原理是什么

Python 2023-05-21 05:05:47 821人浏览 八月长安

Python 官方文档:入门教程 => 点击学习

摘要

调试器是一个编程语言非常重要的部分,调试器是一种用于诊断和修复代码错误(或称为 bug)的工具,它允许开发者在程序执行时逐步查看和分析代码的状态和行为,它可以帮助开发者诊断和修复代码错误,理解程序的行为,优化性能。调试器在任何编程语言中都是

调试器是一个编程语言非常重要的部分,调试器是一种用于诊断和修复代码错误(或称为 bug)的工具,它允许开发者在程序执行时逐步查看和分析代码的状态和行为,它可以帮助开发者诊断和修复代码错误,理解程序的行为,优化性能。调试器在任何编程语言中都是一个非常强大的工具,可以提高开发效率和代码质量。

让程序停下来

如果我们需要对一个程序进行调试最重要的一个点就是如果让程序停下来,只有让程序的执行停下来我们才能够观察程序执行的状态,比如我们需要调试 99 乘法表:

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(3)m99()
-> def m99():
(Pdb) s
> /Users/xxxx/Desktop/workdir/dive-into-cpython/code/pdb_test/pdbusage.py(4)m99()
-> for i in range(1, 10):
(Pdb) s
> /Users/xxxx/Desktop/workdir/dive-into-cpython/code/pdb_test/pdbusage.py(5)m99()
-> for j in range(1, i + 1):
(Pdb) s
> /Users/xxxx/Desktop/workdir/dive-into-cpython/code/pdb_test/pdbusage.py(6)m99()
-> print(f"{i}x{j}={i*j}", end='\t')
(Pdb) p i
1
(Pdb)

当然你也可以在 IDE 当中进行调试:

Python虚拟机中调试器的实现原理是什么

根据我们的调试经历容易知道,要想调试一个程序首先最重要的一点就是程序需要在我们设置断点的位置要能够停下来

cpython 王炸机制 &mdash;&mdash; tracing

现在的问题是,上面的程序是怎么在程序执行时停下来的呢?

根据前面的学习我们可以了解到,一个 python 程序的执行首先需要经过 python 编译器编译成 python 字节码,然后交给 python 虚拟机进行执行,如果需要程序停下来就一定需要虚拟机给上层的 python 程序提供接口,让程序在执行的时候可以知道现在执行到什么位置了。这个神秘的机制就隐藏在 sys 这个模块当中,事实上这个模块几乎承担了所有我们与 python 解释器交互的接口。实现调试器一个非常重要的函数就是 sys.settrace 函数,这个函数将为线程设置一个追踪函数,当虚拟机有函数调用,执行完一行代码的时候、甚至执行完一条字节码之后就会执行这个函数。

实现一个 Python 源代码调试器,需要在系统中设置跟踪函数。该函数是线程特定的;为了支持多线程调试,必须对每个正在调试的线程注册一个跟踪函数,使用 settrace() 或者使用 threading.settrace() 。

跟踪函数应该有三个参数:frame、event 和 arg。frame 是当前的栈帧。event 是一个字符串:'call'、'line'、'return'、'exception'、 'opcode' 、'c_call' 或者 'c_exception'。arg 取决于事件类型。

跟踪函数在每次进入新的局部作用域时被调用(事件设置为'call');它应该返回一个引用,用于新作用域的本地跟踪函数,或者如果不想在该作用域中进行跟踪,则返回None。

如果在跟踪函数中发生任何错误,它将被取消设置,就像调用settrace(None)一样。

事件的含义如下:

  • call,调用了一个函数(或者进入了其他代码块)。调用本地跟踪函数时,指定了 arg 为 None,并在返回值中指定了本地函数。

  • line,将要执行一行新的代码,参数 arg 的值为 None 。

  • return,函数(或其他代码块)即将返回。当事件由异常引起时,本地跟踪函数被调用并返回arg值为None。跟踪函数的返回值将被忽略。

  • exception,发生了异常。调用本地跟踪函数;arg是一个元组(exception,value,traceback);返回值指定了新的本地跟踪函数。

  • opcode,解释器即将执行新的字节码指令。执行本地追踪函数,arg为空,返回一个新的本地追踪函数。默认情况下,不会发出每个操作码的事件:必须通过在帧上设置 f_trace_opcodes 为 True 来显式请求。

  • c_call,一个 c 函数将要被调用。

  • c_exception,调用 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
(Pdb)n
debugging line: def m99():
(Pdb)n
debugging line: if __name__ == '__main__':
(Pdb)n
debugging line: m99()
(Pdb)n
debugging line: for i in range(1, 10):
(Pdb)n
debugging line: for j in range(1, i + 1):
(Pdb)n
debugging line: print(f"{i}x{j}={i*j}", end='\t')
1x1=1 (Pdb)n
debugging line: for j in range(1, i + 1):
(Pdb)p i
1
(Pdb)p j
1
(Pdb)q
(py3.10) ➜ pdb_test git:(master) ✗

Python虚拟机中调试器的实现原理是什么

可以看到我们的程序真正的被调试起来了。

现在我们来分析一下我们自己实现的简易版本的调试器,在前文当中我们已经提到了 sys.settrace 函数,调用这个函数时需要传递一个函数作为参数,被传入的函数需要接受三个参数:

  • frame,当前正在执行的栈帧。

  • event,事件的类别,这一点在前面的文件当中已经提到了。

  • arg,参数这一点在前面也已经提到了。

  • 同时需要注意的是这个函数也需要有一个返回值,python 虚拟机在下一次事件发生的时候会调用返回的这个函数,如果返回 None 那么就不会在发生事件的时候调用 tracing 函数了,这是代码当中为什么在 debug 返回 debug 的原因。

我们只对 line 这个事件进行处理,然后进行死循环,只有输入 n 指令的时候才会执行下一行,然后打印正在执行的行,这个时候就会退出函数 debug ,程序就会继续执行了。python 内置的 eval 函数可以获取变量的值。

python 官方调试器源码分析

python 官方的调试器为 pdb 这个是 python 标准库自带的,我们可以通过 python -m pdb xx.py 去调试文件 xx.py 。这里我们只分析核心代码:

代码位置:bdp.py 下面的 Bdb 类

    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虚拟机中调试器的实现原理是什么的详细内容,更多请关注编程网其它相关文章!

--结束END--

本文标题: Python虚拟机中调试器的实现原理是什么

本文链接: https://lsjlt.com/news/214989.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • Python虚拟机中调试器的实现原理是什么
    调试器是一个编程语言非常重要的部分,调试器是一种用于诊断和修复代码错误(或称为 bug)的工具,它允许开发者在程序执行时逐步查看和分析代码的状态和行为,它可以帮助开发者诊断和修复代码错误,理解程序的行为,优化性能。调试器在任何编程语言中都是...
    99+
    2023-05-21
    Python
  • Python虚拟机中字典的实现原理是什么
    字典数据结构分析 typedef struct { PyObject_HEAD Py_ssize_t ma_used; PyDictKeysObject *ma_keys; PyObject **ma_val...
    99+
    2023-05-19
    Python
  • Python虚拟机中列表的实现原理是什么
    这篇文章主要介绍“Python虚拟机中列表的实现原理是什么”,在日常操作中,相信很多人在Python虚拟机中列表的实现原理是什么问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Python虚拟机中列表的实现原理...
    99+
    2023-07-05
  • Python虚拟机中元组的实现原理是什么
    这篇文章主要介绍了Python虚拟机中元组的实现原理是什么的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇Python虚拟机中元组的实现原理是什么文章都会有所收获,下面我们一起来看看吧。元组的结构在这一小节当中主...
    99+
    2023-07-05
  • Python虚拟机中字节的实现原理是什么
    这篇文章主要介绍“Python虚拟机中字节的实现原理是什么”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“Python虚拟机中字节的实现原理是什么”文章能帮助大家解决问题。数据结构typedef&nb...
    99+
    2023-07-05
  • Python虚拟机中复数的实现原理是什么
    本篇内容主要讲解“Python虚拟机中复数的实现原理是什么”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Python虚拟机中复数的实现原理是什么”吧!复数数据结构在 cpython 当中对于复数...
    99+
    2023-07-05
  • Python虚拟机中整型的实现原理是什么
    这篇文章主要介绍“Python虚拟机中整型的实现原理是什么”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“Python虚拟机中整型的实现原理是什么”文章能帮助大家解决问题。数据结构在 cpython ...
    99+
    2023-07-05
  • Python虚拟机中浮点数的实现原理是什么
    这篇文章主要介绍“Python虚拟机中浮点数的实现原理是什么”,在日常操作中,相信很多人在Python虚拟机中浮点数的实现原理是什么问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Python虚拟机中浮点数的实...
    99+
    2023-07-05
  • 深入理解Python虚拟机中调试器实现原理与源码分析
    目录让程序停下来cpython 王炸机制 —— tracing自己动手实现一个简单的调试器python 官方调试器源码分析总结调试器是一个编程语言非常重要的部...
    99+
    2023-05-17
    Python虚拟机 调试器原理 Python虚拟机 调试器 Python虚拟机 Python 调试器
  • Python虚拟机集合set实现原理是什么
    本文小编为大家详细介绍“Python虚拟机集合set实现原理是什么”,内容详细,步骤清晰,细节处理妥当,希望这篇“Python虚拟机集合set实现原理是什么”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。深入理解 ...
    99+
    2023-07-05
  • java中虚拟机jvm原理是什么
    这篇文章将为大家详细讲解有关java中虚拟机jvm原理是什么,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。曾几何时,我们还是初识Hello World的时候,我们哪曾知道,Java这门神奇的语言,在执行我...
    99+
    2023-06-25
  • android虚拟机原理是什么
    Android虚拟机的原理是将Android操作系统安装在主机操作系统上,通过虚拟化技术实现在主机上运行Android应用程序。具体...
    99+
    2023-10-12
    android
  • 深入理解Python虚拟机中描述器的实现原理
    目录描述器的基本用法描述器的实现原理描述器的应用场景实现属性访问控制实现数据验证和转换实现延迟加载和缓存实现 ORM 映射总结在本篇文章当中主要给大家介绍一个我们在使用类的时候经常使...
    99+
    2023-05-19
    Python虚拟机描述器实现原理 Python 描述器 Python虚拟机
  • java虚拟主机运行的原理是什么
    Java虚拟主机(JVM)是一个虚拟的计算机,它运行在真实计算机上。JVM可以执行Java字节码文件,将其转换为可执行代码并在操作系...
    99+
    2023-06-12
    java虚拟主机 虚拟主机
  • java虚拟主机的工作原理是什么
    Java 虚拟主机(Java Virtual Hosting,JVH)是一种基于 Java 技术的虚拟主机服务,其工作原理如下:1、...
    99+
    2023-03-20
    java虚拟主机 虚拟主机
  • Java虚拟机中垃圾回收机制的原理是什么
    Java虚拟机中垃圾回收机制的原理是什么?很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。在Java虚拟机中,对象和数组的内存都是在堆中分配的,垃圾收集器主要回收的...
    99+
    2023-05-31
    虚拟机 垃圾回收机制 java
  • docker虚拟化的原理是什么
    Docker 虚拟化的原理主要是基于 Linux 内核的 cgroups(控制组)和 namespaces(命名空间)技术实现的。 ...
    99+
    2024-04-09
    docker
  • 基于域名的虚拟主机原理是什么
    基于域名的虚拟主机原理是通过将多个域名指向同一个物理服务器的不同目录或虚拟服务器,实现多个网站共享同一个服务器资源的技术。在传统的虚...
    99+
    2023-09-07
    虚拟主机
  • Python中怎么实现调试器调试
    今天就跟大家聊聊有关Python中怎么实现调试器调试,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获。PyDev 就能显示出一个超链接,这样您可以在导入库或函数的源代码之间导航。请注意,...
    99+
    2023-06-17
  • python中什么是调试器
    这期内容当中小编将会给大家带来有关python中什么是调试器,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。python有哪些常用库python常用的库:1.requesuts;2.scrapy;3.pil...
    99+
    2023-06-14
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作