我的编程空间,编程开发者的网络收藏夹
学习永远不晚

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

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

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

调试器是一个编程语言非常重要的部分,调试器是一种用于诊断和修复代码错误(或称为 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虚拟机中调试器的实现原理是什么的详细内容,更多请关注编程网其它相关文章!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

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

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

Python虚拟机中列表的实现原理是什么

这篇文章主要介绍“Python虚拟机中列表的实现原理是什么”,在日常操作中,相信很多人在Python虚拟机中列表的实现原理是什么问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Python虚拟机中列表的实现原理
2023-07-05

Python虚拟机中复数的实现原理是什么

本篇内容主要讲解“Python虚拟机中复数的实现原理是什么”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Python虚拟机中复数的实现原理是什么”吧!复数数据结构在 cpython 当中对于复数
2023-07-05

Python虚拟机中字节的实现原理是什么

这篇文章主要介绍“Python虚拟机中字节的实现原理是什么”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“Python虚拟机中字节的实现原理是什么”文章能帮助大家解决问题。数据结构typedef st
2023-07-05

Python虚拟机中元组的实现原理是什么

这篇文章主要介绍了Python虚拟机中元组的实现原理是什么的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇Python虚拟机中元组的实现原理是什么文章都会有所收获,下面我们一起来看看吧。元组的结构在这一小节当中主
2023-07-05

Python虚拟机中整型的实现原理是什么

这篇文章主要介绍“Python虚拟机中整型的实现原理是什么”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“Python虚拟机中整型的实现原理是什么”文章能帮助大家解决问题。数据结构在 cpython
2023-07-05

Python虚拟机中浮点数的实现原理是什么

这篇文章主要介绍“Python虚拟机中浮点数的实现原理是什么”,在日常操作中,相信很多人在Python虚拟机中浮点数的实现原理是什么问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Python虚拟机中浮点数的实
2023-07-05

Python虚拟机集合set实现原理是什么

本文小编为大家详细介绍“Python虚拟机集合set实现原理是什么”,内容详细,步骤清晰,细节处理妥当,希望这篇“Python虚拟机集合set实现原理是什么”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。深入理解
2023-07-05

深入理解Python虚拟机中调试器实现原理与源码分析

本文主要给大家介绍python中调试器的实现原理,通过了解一个语言的调试器的实现原理我们可以更加深入的理解整个语言的运行机制,可以帮助我们更好的理解程序的执行,感兴趣的可以了解一下
2023-05-17

java中虚拟机jvm原理是什么

这篇文章将为大家详细讲解有关java中虚拟机jvm原理是什么,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。曾几何时,我们还是初识Hello World的时候,我们哪曾知道,Java这门神奇的语言,在执行我
2023-06-25

深入理解Python虚拟机中描述器的实现原理

这篇文章主要给大家介绍一个我们在使用类的时候经常使用但是却很少在意的黑科技——描述器的实现原理,文中的示例代码讲解详细,需要的可以参考一下
2023-05-19

java虚拟机的工作原理是什么

Java虚拟机工作原理Java虚拟机(JVM)通过以下步骤执行Java程序:加载字节码验证字节码准备字节码编译字节码(JIT编译)执行字节码关键组件包括:Java堆、元空间、栈、垃圾回收器和类加载器。JVM将内存划分为运行时数据区域,包括程序计数器、虚拟机栈、本地方法栈、堆和方法区。类加载涉及加载、验证、准备、解析和初始化步骤。垃圾回收自动管理内存,释放未使用的对象。JVM优化包括JIT编译、垃圾回收和类加载优化。
java虚拟机的工作原理是什么
2024-04-12

java虚拟主机运行的原理是什么

Java虚拟主机(JVM)是一个虚拟的计算机,它运行在真实计算机上。JVM可以执行Java字节码文件,将其转换为可执行代码并在操作系统上运行。Java虚拟主机运行的原理如下:1. 通过Java编译器将Java源代码编译成Java字节码文件。
2023-06-12

java虚拟主机的工作原理是什么

Java 虚拟主机(Java Virtual Hosting,JVH)是一种基于 Java 技术的虚拟主机服务,其工作原理如下:1、Java 虚拟主机的工作原理类似于传统的虚拟主机,但是其底层基于 Java 技术实现。2、Java 虚拟主机
2023-03-20

Java虚拟机中垃圾回收机制的原理是什么

Java虚拟机中垃圾回收机制的原理是什么?很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。在Java虚拟机中,对象和数组的内存都是在堆中分配的,垃圾收集器主要回收的
2023-05-31

基于域名的虚拟主机原理是什么

基于域名的虚拟主机原理是通过将多个域名指向同一个物理服务器的不同目录或虚拟服务器,实现多个网站共享同一个服务器资源的技术。在传统的虚拟主机中,一台服务器通常只能绑定一个IP地址,因此只能承载一个域名的网站。而基于域名的虚拟主机通过HTTP协
2023-09-07

Python虚拟机中描述器怎么实现

Python虚拟机中的描述器描述器是Python中特殊类,用于修改属性访问和设置行为,实现数据验证、属性缓存等功能。通过特殊方法__get__和__set__实现,描述器可以分为数据描述符、非数据描述符和方法描述符。使用描述器可实现属性验证、缓存和权限控制,但需要注意其复杂性、性能开销和调试困难。
Python虚拟机中描述器怎么实现
2024-04-10

Python虚拟机中描述器怎么实现

在Python虚拟机中,描述器(Descriptor)是一种特殊的对象,它可以在类的属性访问过程中定义和控制对属性的访问。要实现一个描述器,需要定义一个类,并在该类中实现描述器协议中的一些特定方法。以下是描述器协议中需要实现的方法:__g
Python虚拟机中描述器怎么实现
2024-04-09

Python中hook的实现原理是什么

在Python中,hook(钩子)是一种机制,允许开发者在特定事件(例如函数调用、异常发生等)发生时插入自定义的代码进行处理。实现原理主要基于Python的装饰器(Decorator)和元编程的概念。装饰器是Python中一种用来修饰函数或
2023-09-26

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录