溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶(hù)服務(wù)條款》

Python虛擬機(jī)中調(diào)試器的實(shí)現(xiàn)原理是什么

發(fā)布時(shí)間:2023-04-26 11:13:07 來(lái)源:億速云 閱讀:139 作者:iii 欄目:開(kāi)發(fā)技術(shù)

這篇文章主要講解了“Python虛擬機(jī)中調(diào)試器的實(shí)現(xiàn)原理是什么”,文中的講解內(nèi)容簡(jiǎn)單清晰,易于學(xué)習(xí)與理解,下面請(qǐng)大家跟著小編的思路慢慢深入,一起來(lái)研究和學(xué)習(xí)“Python虛擬機(jī)中調(diào)試器的實(shí)現(xiàn)原理是什么”吧!

調(diào)試器是一個(gè)編程語(yǔ)言非常重要的部分,調(diào)試器是一種用于診斷和修復(fù)代碼錯(cuò)誤(或稱(chēng)為 bug)的工具,它允許開(kāi)發(fā)者在程序執(zhí)行時(shí)逐步查看和分析代碼的狀態(tài)和行為,它可以幫助開(kāi)發(fā)者診斷和修復(fù)代碼錯(cuò)誤,理解程序的行為,優(yōu)化性能。無(wú)論在哪種編程語(yǔ)言中,調(diào)試器都是一個(gè)強(qiáng)大的工具,對(duì)于提高開(kāi)發(fā)效率和代碼質(zhì)量都起著積極的作用。

讓程序停下來(lái)

如果我們需要對(duì)一個(gè)程序進(jìn)行調(diào)試最重要的一個(gè)點(diǎn)就是如果讓程序停下來(lái),只有讓程序的執(zhí)行停下來(lái)我們才能夠觀察程序執(zhí)行的狀態(tài),比如我們需要調(diào)試 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()

現(xiàn)在執(zhí)行命令 python -m pdb pdbusage.py 就可以對(duì)上面的程序進(jìn)行調(diào)試:

(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) 

當(dāng)然你也可以在 IDE 當(dāng)中進(jìn)行調(diào)試:

Python虛擬機(jī)中調(diào)試器的實(shí)現(xiàn)原理是什么

根據(jù)我們的調(diào)試經(jīng)歷容易知道,要想調(diào)試一個(gè)程序首先最重要的一點(diǎn)就是程序需要在我們?cè)O(shè)置斷點(diǎn)的位置要能夠停下來(lái)

cpython 王炸機(jī)制 &mdash;&mdash; tracing

現(xiàn)在的問(wèn)題是,上面的程序是怎么在程序執(zhí)行時(shí)停下來(lái)的呢?

根據(jù)前面的學(xué)習(xí)我們可以了解到,一個(gè) python 程序的執(zhí)行首先需要經(jīng)過(guò) python 編譯器編譯成 python 字節(jié)碼,然后交給 python 虛擬機(jī)進(jìn)行執(zhí)行,如果需要程序停下來(lái)就一定需要虛擬機(jī)給上層的 python 程序提供接口,讓程序在執(zhí)行的時(shí)候可以知道現(xiàn)在執(zhí)行到什么位置了。這個(gè)神秘的機(jī)制就隱藏在 sys 這個(gè)模塊當(dāng)中,事實(shí)上這個(gè)模塊幾乎承擔(dān)了所有我們與 python 解釋器交互的接口。實(shí)現(xiàn)調(diào)試器一個(gè)非常重要的函數(shù)就是 sys.settrace 函數(shù),這個(gè)函數(shù)將為線程設(shè)置一個(gè)追蹤函數(shù),當(dāng)虛擬機(jī)有函數(shù)調(diào)用,執(zhí)行完一行代碼的時(shí)候、甚至執(zhí)行完一條字節(jié)碼之后就會(huì)執(zhí)行這個(gè)函數(shù)。

設(shè)置系統(tǒng)的跟蹤函數(shù),允許在 Python 中實(shí)現(xiàn)一個(gè) Python 源代碼調(diào)試器。該函數(shù)是線程特定的;為了支持多線程調(diào)試,必須對(duì)每個(gè)正在調(diào)試的線程注冊(cè)一個(gè)跟蹤函數(shù),使用 settrace() 或者使用 threading.settrace() 。

跟蹤函數(shù)應(yīng)該有三個(gè)參數(shù):frame、event 和 arg。frame 是當(dāng)前的棧幀。event 是一個(gè)字符串:'call'、'line'、'return'、'exception'、 'opcode' 、'c_call' 或者 'c_exception'。arg 取決于事件類(lèi)型。

跟蹤函數(shù)在每次進(jìn)入新的局部作用域時(shí)被調(diào)用(事件設(shè)置為'call');它應(yīng)該返回一個(gè)引用,用于新作用域的本地跟蹤函數(shù),或者如果不想在該作用域中進(jìn)行跟蹤,則返回None。

如果在跟蹤函數(shù)中發(fā)生任何錯(cuò)誤,它將被取消設(shè)置,就像調(diào)用settrace(None)一樣。

事件的含義如下:

  • call,調(diào)用了一個(gè)函數(shù)(或者進(jìn)入了其他代碼塊)。調(diào)用全局跟蹤函數(shù);arg 為 None;返回值指定了本地跟蹤函數(shù)。

  • line,將要執(zhí)行一行新的代碼,參數(shù) arg 的值為 None 。

  • return,函數(shù)(或其他代碼塊)即將返回。調(diào)用本地跟蹤函數(shù);arg 是將要返回的值,如果事件是由引發(fā)的異常引起的,則arg為None。跟蹤函數(shù)的返回值將被忽略。

  • exception,發(fā)生了異常。調(diào)用本地跟蹤函數(shù);arg是一個(gè)元組(exception,value,traceback);返回值指定了新的本地跟蹤函數(shù)。

  • opcode,解釋器即將執(zhí)行新的字節(jié)碼指令。調(diào)用本地跟蹤函數(shù);arg 為 None;返回值指定了新的本地跟蹤函數(shù)。默認(rèn)情況下,不會(huì)發(fā)出每個(gè)操作碼的事件:必須通過(guò)在幀上設(shè)置 f_trace_opcodes 為 True 來(lái)顯式請(qǐng)求。

  • c_call,一個(gè) c 函數(shù)將要被調(diào)用。

  • c_exception,調(diào)用 c 函數(shù)的時(shí)候產(chǎn)生了異常。

自己動(dòng)手實(shí)現(xiàn)一個(gè)簡(jiǎn)單的調(diào)試器

在本小節(jié)當(dāng)中我們將實(shí)現(xiàn)一個(gè)非常簡(jiǎn)單的調(diào)試器幫助大家理解調(diào)試器的實(shí)現(xiàn)原理。調(diào)試器的實(shí)現(xiàn)代碼如下所示,只有短短幾十行卻可以幫助我們深入去理解調(diào)試器的原理,我們先看一下實(shí)現(xiàn)的效果在后文當(dāng)中再去分析具體的實(shí)現(xiàn):

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)

在上面的程序當(dāng)中使用如下:

  • 輸入 n 執(zhí)行一行代碼。

  • p name 打印變量 name 。

  • q 退出調(diào)試。

現(xiàn)在我們執(zhí)行上面的程序,進(jìn)行程序調(diào)試:

(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虛擬機(jī)中調(diào)試器的實(shí)現(xiàn)原理是什么

可以看到我們的程序真正的被調(diào)試起來(lái)了。

現(xiàn)在我們來(lái)分析一下我們自己實(shí)現(xiàn)的簡(jiǎn)易版本的調(diào)試器,在前文當(dāng)中我們已經(jīng)提到了 sys.settrace 函數(shù),調(diào)用這個(gè)函數(shù)時(shí)需要傳遞一個(gè)函數(shù)作為參數(shù),被傳入的函數(shù)需要接受三個(gè)參數(shù):

  • frame,當(dāng)前正在執(zhí)行的棧幀。

  • event,事件的類(lèi)別,這一點(diǎn)在前面的文件當(dāng)中已經(jīng)提到了。

  • arg,參數(shù)這一點(diǎn)在前面也已經(jīng)提到了。

  • 同時(shí)需要注意的是這個(gè)函數(shù)也需要有一個(gè)返回值,python 虛擬機(jī)在下一次事件發(fā)生的時(shí)候會(huì)調(diào)用返回的這個(gè)函數(shù),如果返回 None 那么就不會(huì)在發(fā)生事件的時(shí)候調(diào)用 tracing 函數(shù)了,這是代碼當(dāng)中為什么在 debug 返回 debug 的原因。

我們只對(duì) line 這個(gè)事件進(jìn)行處理,然后進(jìn)行死循環(huán),只有輸入 n 指令的時(shí)候才會(huì)執(zhí)行下一行,然后打印正在執(zhí)行的行,這個(gè)時(shí)候就會(huì)退出函數(shù) debug ,程序就會(huì)繼續(xù)執(zhí)行了。python 內(nèi)置的 eval 函數(shù)可以獲取變量的值。

python 官方調(diào)試器源碼分析

python 官方的調(diào)試器為 pdb 這個(gè)是 python 標(biāo)準(zhǔn)庫(kù)自帶的,我們可以通過(guò) python -m pdb xx.py 去調(diào)試文件 xx.py 。這里我們只分析核心代碼:

代碼位置:bdp.py 下面的 Bdb 類(lèi)

    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)

上面的函數(shù)主要是使用 sys.settrace 函數(shù)進(jìn)行 tracing 操作,當(dāng)有事件發(fā)生的時(shí)候就能夠捕捉了。在上面的代碼當(dāng)中 tracing 函數(shù)為 self.trace_dispatch 我們?cè)賮?lái)看這個(gè)函數(shù)的代碼:

    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

從上面的代碼當(dāng)中可以看到每一種事件都有一個(gè)對(duì)應(yīng)的處理函數(shù),在本文當(dāng)中我們主要分析 函數(shù) dispatch_line,這個(gè)處理 line 事件的函數(shù)。

    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

這個(gè)函數(shù)首先會(huì)判斷是否需要在當(dāng)前行停下來(lái),如果需要停下來(lái)就需要進(jìn)入 user_line 這個(gè)函數(shù),后面的調(diào)用鏈函數(shù)比較長(zhǎng),我們直接看最后執(zhí)行的函數(shù),根據(jù)我們使用 pdb 的經(jīng)驗(yàn)來(lái)看,最終肯定是一個(gè) while 循環(huán)讓我們可以不斷的輸入指令進(jìn)行處理:

    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:
                            # 核心邏輯就在這里 不斷的要求輸入然后進(jìn)行處理
                            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) # 這個(gè)函數(shù)就是處理我們輸入的字符串的比如 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:
                # 根據(jù)下面的代碼可以分析了解到如果我們執(zhí)行命令 p 執(zhí)行的函數(shù)為 do_p
                func = getattr(self, 'do_' + cmd)
            except AttributeError:
                return self.default(line)
            return func(arg)

現(xiàn)在我們?cè)賮?lái)看一下 do_p 打印一個(gè)表達(dá)式是如何實(shí)現(xiàn)的:

    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:
            # 看到這里就破案了這不是和我們自己實(shí)現(xiàn)的 pdb 獲取變量的方式一樣嘛 都是
            # 使用當(dāng)前執(zhí)行棧幀的全局和局部變量交給 eval 函數(shù)處理 并且將它的返回值輸出
            return eval(arg, self.curframe.f_globals, self.curframe_locals)
        except:
            self._error_exc()
            raise

感謝各位的閱讀,以上就是“Python虛擬機(jī)中調(diào)試器的實(shí)現(xiàn)原理是什么”的內(nèi)容了,經(jīng)過(guò)本文的學(xué)習(xí)后,相信大家對(duì)Python虛擬機(jī)中調(diào)試器的實(shí)現(xiàn)原理是什么這一問(wèn)題有了更深刻的體會(huì),具體使用情況還需要大家實(shí)踐驗(yàn)證。這里是億速云,小編將為大家推送更多相關(guān)知識(shí)點(diǎn)的文章,歡迎關(guān)注!

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI