>  기사  >  백엔드 개발  >  Python 가상 머신에서 디버거의 구현 원리는 무엇입니까?

Python 가상 머신에서 디버거의 구현 원리는 무엇입니까?

王林
王林앞으로
2023-05-20 23:19:461256검색

디버거는 프로그래밍 언어에서 매우 중요한 부분입니다. 디버거는 개발자가 프로그램을 실행하는 동안 코드 오류(또는 버그)를 단계별로 보고 분석할 수 있도록 하는 도구입니다. 개발자가 코드 오류를 진단 및 수정하고, 프로그램 동작을 이해하고, 성능을 최적화하는 데 도움이 될 수 있습니다. 디버거는 모든 프로그래밍 언어에서 매우 강력한 도구이며 개발 효율성과 코드 품질을 향상시킬 수 있습니다.

프로그램을 중지시키세요

프로그램을 디버깅해야 할 경우 가장 중요한 점은 프로그램을 중지하면 프로그램 실행을 중지해야만 프로그램 실행 상태를 관찰할 수 있다는 것입니다. 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
> /xxxx/Desktop /workdir/dive-into-cpython/code/pdb_test/pdbusage.py(3)()
-> def m99():
(Pdb) s
> /Desktop/workdir /dive-into-cpython/code/pdb_test/pdbusage.py(10)()
-> if __name__ == '__main__':
(Pdb) s
> xxxx/Desktop/ workdir/dive-into-cpython/code/pdb_test/pdbusage.py(11)()
-> m99()
(Pdb) s
--Call--
> Users/xxxx/ Desktop/workdir/dive-into-cpython/code/pdb_test/pdbusage.py(3)m99()
-> def m99():
(Pdb) s
> /workdir/dive -into-cpython/code/pdb_test/pdbusage.py(4)m99()
-> 범위(1, 10):
(Pdb) s
> /workdir/dive -into-cpython/code/pdb_test/pdbusage.py(5)m99()
-> for j in range(1, i + 1):
(Pdb) s
> /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의 킹 메커니즘 - 추적

이제 질문은 프로그램이 실행될 때 위 프로그램이 어떻게 중지됩니까?

이전 연구에 따르면, Python 프로그램의 실행은 먼저 Python 컴파일러에 의해 Python 바이트코드로 컴파일된 다음, 프로그램 실행을 중지해야 하는 경우 Python 가상 머신에 넘겨져야 한다는 것을 이해할 수 있습니다. 상위 계층에 가상머신을 주어야 한다. 파이썬 프로그램은 프로그램이 실행될 때 현재 어디에서 실행되고 있는지 알 수 있도록 인터페이스를 제공한다. 이 신비한 메커니즘은 sys 모듈에 숨겨져 있습니다. 실제로 이 모듈은 Python 인터프리터와 상호 작용하는 데 사용하는 거의 모든 인터페이스를 담당합니다. 디버거를 구현하는 데 매우 중요한 함수는 sys.settrace 함수입니다. 이 함수는 가상 머신이 함수 호출을 갖고 코드 줄을 실행할 때 또는 바이트코드를 실행한 후에도 이 함수를 설정합니다. 기능이 실행됩니다.

Python 소스 코드 디버거를 구현하려면 시스템에서 추적 기능을 설정해야 합니다. 이 함수는 스레드별로 다릅니다. 다중 스레드 디버깅을 지원하려면 settrace() 또는 threading.settrace() 를 사용하여 디버깅 중인 각 스레드에 대해 추적 함수를 등록해야 합니다.

추적 기능에는 프레임, 이벤트, 인수의 세 가지 매개변수가 있어야 합니다. 프레임은 현재 스택 프레임입니다. 이벤트는 문자열입니다: 'call', 'line', 'return', 'Exception', 'opcode', 'c_call' 또는 'c_Exception'. arg는 이벤트 유형에 따라 다릅니다.

추적 기능은 새 로컬 범위가 입력될 때마다 호출됩니다(이벤트는 '호출'로 설정됨). 새 범위에 대한 로컬 추적 기능에 대한 참조를 반환해야 합니다. 또는 해당 범위에서 원하지 않는 경우 추적을 위해, 아무것도 반환되지 않습니다.

trace 함수에 오류가 발생하면 settrace(None)을 호출하는 것과 마찬가지로 설정이 해제됩니다.

이벤트의 의미는 다음과 같습니다.

  • 호출, 함수가 호출됩니다(또는 다른 코드 블록이 입력됩니다). 로컬 추적 함수를 호출할 때 arg를 None으로 지정하고 반환 값에 로컬 함수를 지정합니다.

  • 라인에서 새로운 코드 라인이 실행되며 매개변수 arg 의 값은 None 입니다.

  • return, 함수(또는 다른 코드 블록)가 곧 반환됩니다. 예외로 인해 이벤트가 발생하면 로컬 추적 함수가 호출되고 인수 값 None을 반환합니다. 추적 함수의 반환 값은 무시됩니다.

  • 예외, 예외가 발생했습니다. 로컬 추적 함수를 호출합니다. arg는 튜플(예외, 값, 역추적)입니다. 반환 값은 새 로컬 추적 함수를 지정합니다.

  • 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 == &#39;line&#39;:
            print("In line")
            return self.dispatch_line(frame)
        if event == &#39;call&#39;:
            print("In call")
            return self.dispatch_call(frame, arg)
        if event == &#39;return&#39;:
            print("In return")
            return self.dispatch_return(frame, arg)
        if event == &#39;exception&#39;:
            print("In execption")
            return self.dispatch_exception(frame, arg)
        if event == &#39;c_call&#39;:
            print("In c_call")
            return self.trace_dispatch
        if event == &#39;c_exception&#39;:
            print("In c_exception")
            return self.trace_dispatch
        if event == &#39;c_return&#39;:
            print("In c_return")
            return self.trace_dispatch
        print(&#39;bdb.Bdb.dispatch: unknown debugging event:&#39;, 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 = &#39;(Pdb)&#39;
                        except EOFError:
                            line = &#39;EOF&#39;
                    else:
                        self.stdout.write(self.prompt)
                        self.stdout.flush()
                        line = self.stdin.readline()
                        if not len(line):
                            line = &#39;EOF&#39;
                        else:
                            line = line.rstrip(&#39;\r\n&#39;)

                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 == &#39;EOF&#39; :
            self.lastcmd = &#39;&#39;
        if cmd == &#39;&#39;:
            return self.default(line)
        else:
            try:
                # 根据下面的代码可以分析了解到如果我们执行命令 p 执行的函数为 do_p
                func = getattr(self, &#39;do_&#39; + 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 가상 머신에서 디버거의 구현 원리는 무엇입니까?의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 yisu.com에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제