您好,登錄后才能下訂單哦!
這篇文章主要講解了“Python虛擬機中調試器的實現原理是什么”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“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 程序的執行首先需要經過 python 編譯器編譯成 python 字節碼,然后交給 python 虛擬機進行執行,如果需要程序停下來就一定需要虛擬機給上層的 python 程序提供接口,讓程序在執行的時候可以知道現在執行到什么位置了。這個神秘的機制就隱藏在 sys 這個模塊當中,事實上這個模塊幾乎承擔了所有我們與 python 解釋器交互的接口。實現調試器一個非常重要的函數就是 sys.settrace 函數,這個函數將為線程設置一個追蹤函數,當虛擬機有函數調用,執行完一行代碼的時候、甚至執行完一條字節碼之后就會執行這個函數。
設置系統的跟蹤函數,允許在 Python 中實現一個 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 是將要返回的值,如果事件是由引發的異常引起的,則arg為None。跟蹤函數的返回值將被忽略。
exception,發生了異常。調用本地跟蹤函數;arg是一個元組(exception,value,traceback);返回值指定了新的本地跟蹤函數。
opcode,解釋器即將執行新的字節碼指令。調用本地跟蹤函數;arg 為 None;返回值指定了新的本地跟蹤函數。默認情況下,不會發出每個操作碼的事件:必須通過在幀上設置 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) ?
可以看到我們的程序真正的被調試起來了。
現在我們來分析一下我們自己實現的簡易版本的調試器,在前文當中我們已經提到了 sys.settrace 函數,調用這個函數時需要傳遞一個函數作為參數,被傳入的函數需要接受三個參數:
frame,當前正在執行的棧幀。
event,事件的類別,這一點在前面的文件當中已經提到了。
arg,參數這一點在前面也已經提到了。
同時需要注意的是這個函數也需要有一個返回值,python 虛擬機在下一次事件發生的時候會調用返回的這個函數,如果返回 None 那么就不會在發生事件的時候調用 tracing 函數了,這是代碼當中為什么在 debug 返回 debug 的原因。
我們只對 line 這個事件進行處理,然后進行死循環,只有輸入 n 指令的時候才會執行下一行,然后打印正在執行的行,這個時候就會退出函數 debug ,程序就會繼續執行了。python 內置的 eval 函數可以獲取變量的值。
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虛擬機中調試器的實現原理是什么”的內容了,經過本文的學習后,相信大家對Python虛擬機中調試器的實現原理是什么這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。