深入理解Python虛擬機中調(diào)試器實現(xiàn)原理與源碼分析
調(diào)試器是一個編程語言非常重要的部分,調(diào)試器是一種用于診斷和修復代碼錯誤(或稱為 bug)的工具,它允許開發(fā)者在程序執(zhí)行時逐步查看和分析代碼的狀態(tài)和行為,它可以幫助開發(fā)者診斷和修復代碼錯誤,理解程序的行為,優(yōu)化性能。無論在哪種編程語言中,調(diào)試器都是一個強大的工具,對于提高開發(fā)效率和代碼質(zhì)量都起著積極的作用。
在本篇文章當中主要給大家介紹 python 語言當中調(diào)試器的實現(xiàn)原理,通過了解一個語言的調(diào)試器的實現(xiàn)原理我們可以更加深入的理解整個語言的運行機制,可以幫助我們更好的理解程序的執(zhí)行。
讓程序停下來
如果我們需要對一個程序進行調(diào)試最重要的一個點就是如果讓程序停下來,只有讓程序的執(zhí)行停下來我們才能夠觀察程序執(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
就可以對上面的程序進行調(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)
當然你也可以在 IDE 當中進行調(diào)試:
根據(jù)我們的調(diào)試經(jīng)歷容易知道,要想調(diào)試一個程序首先最重要的一點就是程序需要在我們設置斷點的位置要能夠停下來
cpython 王炸機制 —— tracing
現(xiàn)在的問題是,上面的程序是怎么在程序執(zhí)行時停下來的呢?
根據(jù)前面的學習我們可以了解到,一個 python 程序的執(zhí)行首先需要經(jīng)過 python 編譯器編譯成 python 字節(jié)碼,然后交給 python 虛擬機進行執(zhí)行,如果需要程序停下來就一定需要虛擬機給上層的 python 程序提供接口,讓程序在執(zhí)行的時候可以知道現(xiàn)在執(zhí)行到什么位置了。這個神秘的機制就隱藏在 sys 這個模塊當中,事實上這個模塊幾乎承擔了所有我們與 python 解釋器交互的接口。實現(xiàn)調(diào)試器一個非常重要的函數(shù)就是 sys.settrace 函數(shù),這個函數(shù)將為線程設置一個追蹤函數(shù),當虛擬機有函數(shù)調(diào)用,執(zhí)行完一行代碼的時候、甚至執(zhí)行完一條字節(jié)碼之后就會執(zhí)行這個函數(shù)。
設置系統(tǒng)的跟蹤函數(shù),允許在 Python 中實現(xiàn)一個 Python 源代碼調(diào)試器。該函數(shù)是線程特定的;為了支持多線程調(diào)試,必須對每個正在調(diào)試的線程注冊一個跟蹤函數(shù),使用 settrace() 或者使用 threading.settrace() 。
跟蹤函數(shù)應該有三個參數(shù):frame、event 和 arg。frame 是當前的棧幀。event 是一個字符串:'call'、'line'、'return'、'exception'、 'opcode' 、'c_call' 或者 'c_exception'。arg 取決于事件類型。
跟蹤函數(shù)在每次進入新的局部作用域時被調(diào)用(事件設置為'call');它應該返回一個引用,用于新作用域的本地跟蹤函數(shù),或者如果不想在該作用域中進行跟蹤,則返回None。
如果在跟蹤函數(shù)中發(fā)生任何錯誤,它將被取消設置,就像調(diào)用settrace(None)一樣。
事件的含義如下:
- call,調(diào)用了一個函數(shù)(或者進入了其他代碼塊)。調(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是一個元組(exception,value,traceback);返回值指定了新的本地跟蹤函數(shù)。
- opcode,解釋器即將執(zhí)行新的字節(jié)碼指令。調(diào)用本地跟蹤函數(shù);arg 為 None;返回值指定了新的本地跟蹤函數(shù)。默認情況下,不會發(fā)出每個操作碼的事件:必須通過在幀上設置 f_trace_opcodes 為 True 來顯式請求。
- c_call,一個 c 函數(shù)將要被調(diào)用。
- c_exception,調(diào)用 c 函數(shù)的時候產(chǎn)生了異常。
自己動手實現(xiàn)一個簡單的調(diào)試器
在本小節(jié)當中我們將實現(xiàn)一個非常簡單的調(diào)試器幫助大家理解調(diào)試器的實現(xiàn)原理。調(diào)試器的實現(xiàn)代碼如下所示,只有短短幾十行卻可以幫助我們深入去理解調(diào)試器的原理,我們先看一下實現(xiàn)的效果在后文當中再去分析具體的實現(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)
在上面的程序當中使用如下:
- 輸入 n 執(zhí)行一行代碼。
- p name 打印變量 name 。
- q 退出調(diào)試。
現(xiàn)在我們執(zhí)行上面的程序,進行程序調(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) ?
可以看到我們的程序真正的被調(diào)試起來了。
現(xiàn)在我們來分析一下我們自己實現(xiàn)的簡易版本的調(diào)試器,在前文當中我們已經(jīng)提到了 sys.settrace 函數(shù),調(diào)用這個函數(shù)時需要傳遞一個函數(shù)作為參數(shù),被傳入的函數(shù)需要接受三個參數(shù):
- frame,當前正在執(zhí)行的棧幀。
- event,事件的類別,這一點在前面的文件當中已經(jīng)提到了。
- arg,參數(shù)這一點在前面也已經(jīng)提到了。
- 同時需要注意的是這個函數(shù)也需要有一個返回值,python 虛擬機在下一次事件發(fā)生的時候會調(diào)用返回的這個函數(shù),如果返回 None 那么就不會在發(fā)生事件的時候調(diào)用 tracing 函數(shù)了,這是代碼當中為什么在 debug 返回 debug 的原因。
我們只對 line 這個事件進行處理,然后進行死循環(huán),只有輸入 n 指令的時候才會執(zhí)行下一行,然后打印正在執(zhí)行的行,這個時候就會退出函數(shù) debug ,程序就會繼續(xù)執(zhí)行了。python 內(nèi)置的 eval 函數(shù)可以獲取變量的值。
python 官方調(diào)試器源碼分析
python 官方的調(diào)試器為 pdb 這個是 python 標準庫自帶的,我們可以通過 python -m pdb xx.py
去調(diào)試文件 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)
上面的函數(shù)主要是使用 sys.settrace 函數(shù)進行 tracing 操作,當有事件發(fā)生的時候就能夠捕捉了。在上面的代碼當中 tracing 函數(shù)為 self.trace_dispatch 我們再來看這個函數(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
從上面的代碼當中可以看到每一種事件都有一個對應的處理函數(shù),在本文當中我們主要分析 函數(shù) dispatch_line,這個處理 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
這個函數(shù)首先會判斷是否需要在當前行停下來,如果需要停下來就需要進入 user_line 這個函數(shù),后面的調(diào)用鏈函數(shù)比較長,我們直接看最后執(zhí)行的函數(shù),根據(jù)我們使用 pdb 的經(jīng)驗來看,最終肯定是一個 while 循環(huá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: # 核心邏輯就在這里 不斷的要求輸入然后進行處理 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) # 這個函數(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)在我們再來看一下 do_p 打印一個表達式是如何實現(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: # 看到這里就破案了這不是和我們自己實現(xiàn)的 pdb 獲取變量的方式一樣嘛 都是 # 使用當前執(zhí)行棧幀的全局和局部變量交給 eval 函數(shù)處理 并且將它的返回值輸出 return eval(arg, self.curframe.f_globals, self.curframe_locals) except: self._error_exc() raise
總結(jié)
在本篇文章當中我們主要分析 python 當中實現(xiàn)調(diào)試器的原理,并且通過一個幾十行的代碼實現(xiàn)了一個非常簡單的調(diào)試器,這可以深入幫助我們理解調(diào)試器實現(xiàn)的細節(jié),這讓我們對于程序設計語言的認識又加深了一點。最后簡單的介紹了一下 python 自己的調(diào)試器 pdb,但是有一點遺憾的目前 pdb 還不能夠支持直接調(diào)試 python 字節(jié)碼,但是在 python 虛擬機當中已經(jīng)有調(diào)試字節(jié)碼的事件了,相信在未來應該可以直接調(diào)試字節(jié)碼了。
還記得我們在討論 frameobject 的時候有一個字段 f_trace 嘛,這個字段就是指向我們傳遞給 sys.settrace 的函數(shù),當發(fā)生事件的時候虛擬機就會調(diào)用這個函數(shù)。
以上就是深入理解Python虛擬機中調(diào)試器實現(xiàn)原理與源碼分析的詳細內(nèi)容,更多關(guān)于Python虛擬機調(diào)試器的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Python基于Tkinter模塊實現(xiàn)的彈球小游戲
這篇文章主要介紹了Python基于Tkinter模塊實現(xiàn)的彈球小游戲,涉及Python圖形繪制、數(shù)值計算、判斷等相關(guān)操作技巧,需要的朋友可以參考下2018-12-1215行Python代碼實現(xiàn)網(wǎng)易云熱門歌單實例教程
這篇文章主要給大家介紹了關(guān)于利用15行Python代碼實現(xiàn)網(wǎng)易云熱門歌單的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者使用python具有一定的參考學習價值,需要的朋友們下面來一起學習學習吧2019-03-03python入門學習關(guān)于for else的特殊特性講解
本文將介紹 Python 中的" for-else"特性,并通過簡單的示例說明如何正確使用它,有需要的朋友可以借鑒參考下,希望能夠有所幫助2021-11-11python跨文件夾調(diào)用別的文件夾下py文件或參數(shù)方式詳解
這篇文章主要給大家介紹了關(guān)于python跨文件夾調(diào)用別的文件夾下py文件或參數(shù)方式的相關(guān)資料,在python中有時候我們需要調(diào)用另一.py文件中的方法或者類,需要的朋友可以參考下2023-08-08