您現在的位置是:首頁 > 綜合

Python Pdb 原始碼解析

  • 由 Python挖掘者 發表于 綜合
  • 2022-01-10
簡介curindex == 0:print >>self

單步中斷的中斷型別碼是多少

經常使用Python的同學一定熟悉pdb模組,它是Python官方標準庫提供的互動式程式碼偵錯程式,和任何一門語言提供的除錯能力一樣,pdb提供了原始碼行級別的設定斷點、單步執行等常規除錯能力,是Python開發的一個很重要的工具模組。

pdb使用方法見官方文件,本文重點分析官方pdb模組原始碼,介紹除錯功能的實現原理。

原理

從cPython原始碼中可以看到,pdb模組並非c實現的內建模組,而是純Python實現和封裝的模組。核心檔案是pdb。py,它繼承自bdb和cmd模組:

class Pdb(bdb。Bdb, cmd。Cmd): 。。。

基本原理:利用cmd模組定義和實現一系列的除錯命令的互動式輸入,基於sys。settrace插樁跟蹤程式碼執行的棧幀,針對不同的除錯命令控制程式碼的執行和斷點狀態,並向控制檯輸出對應的資訊。

cmd模組主要是提供一個控制檯的命令互動能力,透過raw_input/readline這些阻塞的方法實現輸入等待,然後將命令交給子類處理決定是否繼續迴圈輸入下去,就和他主要的方法名runloop一樣。

cmd是一個常用的模組,並非為pdb專門設計的,pdb使用了cmd的框架從而實現了互動式自定義除錯。

bdb提供了除錯的核心框架,依賴sys。settrace進行程式碼的單步執行跟蹤,然後分發對應的事件(call/line/return/exception)交給子類(pdb)處理。bdb的核心邏輯在對於除錯命令的中斷控制,比如輸入一個單步執行的”s“命令,決定是否需要繼續跟蹤執行還是中斷等待互動輸入,中斷到哪一幀等。

基本流程

pdb啟動,當前frame繫結跟蹤函式trace_dispatch

def trace_dispatch(self, frame, event, arg): if self。quitting: return # None if event == ‘line’: return self。dispatch_line(frame) if event == ‘call’: return self。dispatch_call(frame, arg) if event == ‘return’: return self。dispatch_return(frame, arg) if event == ‘exception’: 。。。

每一幀的不同事件的處理都會經過中斷控制邏輯,主要是stop_here(line事件還會經過break_here)函式,處理後決定程式碼是否中斷,需要中斷到哪一行。

如需要中斷,觸發子類方法user_#event,子類透過interaction實現棧幀資訊更新,並在控制檯列印對應的資訊,然後執行cmdloop讓控制檯處於等待互動輸入。

def interaction(self, frame, traceback): self。setup(frame, traceback) # 當前棧、frame、local vars self。print_stack_entry(self。stack[self。curindex]) self。cmdloop() self。forget()

使用者輸入除錯命令如“next”並回車,首先會呼叫set_#命令,對stopframe、returnframe、stoplineno進行設定,它會影響中斷控制```stop_here``的邏輯,從而決定執行到下一幀的中斷結果。

def _set_stopinfo(self, stopframe, returnframe, stoplineno=0): self。stopframe = stopframe self。returnframe = returnframe self。quitting = 0 # stoplineno >= 0 means: stop at line >= the stoplineno # stoplineno -1 means: don‘t stop at all self。stoplineno = stoplineno

對於除錯過程控制類的命令,一般do_#命令都會返回1,這樣本次runloop立馬結束,下次執行到某一幀觸發中斷會再次啟動runloop(見第三點);對於資訊獲取類的命令,do_#命令都沒有返回值,保持當前的中斷狀態。

程式碼執行到下一幀,重複第三點。

中斷控制

中斷控制也就是對於不同的除錯命令輸入後,能讓程式碼執行到正確的位置停止,等待使用者輸入,比如輸入”s”控制檯就應該在下一個執行frame的程式碼處停止,而輸出“c”就需要執行到下一個打斷點的地方。

中斷控制發生在sys。settrace的每一步跟蹤的中,是除錯執行的核心邏輯。

pdb中主要跟蹤了frame的四個事件:

line:同一個frame中的順序執行事件

call:發生函式呼叫,跳到下一級的frame中,在函式第一行產生call事件

return:函式執行完最後一行(line),發生結果返回,即將跳出當前frame回到上一級frame,在函式最後一行產生return事件

exception:函式執行中發生異常,在異常行產生exception事件,然後在該行返回(return事件),接下來一級一級向上在frame中產生exception和return事件,直到回到底層frame。

它們是程式碼跟蹤時的不同節點型別,pdb根據使用者輸入的除錯命令,在每一步frame跟蹤時都會進行中斷控制,決定接下來是否中斷,中斷到哪一行。中斷控制的主要方法是stop_here:

def stop_here(self, frame): # (CT) stopframe may now also be None, see dispatch_call。 # (CT) the former test for None is therefore removed from here。 if self。skip and \ self。is_skipped_module(frame。f_globals。get(’__name__‘)): return False # next if frame is self。stopframe: # stoplineno >= 0 means: stop at line >= the stoplineno # stoplineno -1 means: don’t stop at all if self。stoplineno == -1: return False return frame。f_lineno >= self。stoplineno # step:當前只要追溯到botframe,就等待執行。 while frame is not None and frame is not self。stopframe: if frame is self。botframe: return True frame = frame。f_back return False

除錯命令大體上分兩類:

1。過程控制:如setp、next、continue等這些執行後馬上進入下階段的程式碼執行

2。資訊獲取/設定:如args、p、list等獲取當前資訊的,也不會影響cmd狀態

以下重點講解幾個最常見的用於過程控制的除錯命令的中斷控制實現原理:

s(step)

命令定義

執行下一條命令,如果本句是函式呼叫,則 s 會執行到函式的第一句。

程式碼分析

pdb中實現邏輯為順序執行每一個幀frame並等待執行,它的執行粒度和settrace一樣。

def stop_here(self, frame): 。。。 # stopframe為None if frame is self。stopframe: 。。。 # 當前frame一定會追溯到botframe,返回true while frame is not None and frame is not self。stopframe: if frame is self。botframe: return True frame = frame。f_back return False

step會將stopframe設定為None,因此只要當前frame能向後一直追溯到底層frame(botframe),就表示可以等待執行了,也就是pdb處於互動等待狀態。

因為step的執行粒度和settrace一樣,所以執行到每一幀都會等待執行。

n(next)

命令定義

執行下一條語句,如果本句是函式呼叫,則執行函式,接著執行當前執行語句的下一條。

程式碼分析

pdb中實現邏輯為,執行至當前frame的下一次跟蹤中斷,但進入到下一個frame(函式呼叫)中不會中斷。

def stop_here(self, frame): 。。。 # 如果frame還沒跳出stopframe,永遠返回true if frame is self。stopframe: if self。stoplineno == -1: return False return frame。f_lineno >= self。stoplineno # 如果frame跳出了stopframe,進入下一個frame,則執行不會中斷,一直到跳出到stopframe # 還有一種情況,如果在return事件中斷執行了next,下一次跟蹤在上一級frame中,此時上一級frame能跟蹤到botframe,中斷 while frame is not None and frame is not self。stopframe: if frame is self。botframe: return True frame = frame。f_back return False

next會設定stopframe為當前frame,也就是除非在當前frame內,進入其他的frame都不會執行中斷。

c

命令定義

繼續執行,直到遇到下一條斷點

程式碼分析

stopframe設定為botframe,stoplineno設定為-1。stop_here總返回false,執行不會中斷,直到遇到斷點(break_here條件成立)

def stop_here(self, frame): 。。。 # 如果在botframe中,stoplineno為-1返回false if frame is self。stopframe: if self。stoplineno == -1: return False return frame。f_lineno >= self。stoplineno # 如果在非botframe中,會先追溯到stopframe,返回false while frame is not None and frame is not self。stopframe: if frame is self。botframe: return True frame = frame。f_back return False

r(return)

命令定義

執行當前執行函式到結束。

程式碼分析

return命令僅在執行到frame結束(函式呼叫)時中斷,也就是遇到return事件時中斷。

pdb會設定stopframe為上一幀frame,returnframe為當前frame。如果是非return事件,stop_here永遠返回false,不會中斷;

def stop_here(self, frame): 。。。 # 會先追溯到stopframe,返回false while frame is not None and frame is not self。stopframe: if frame is self。botframe: return True frame = frame。f_back return False

如果是return事件,stop_here仍然返回false,但是returnframe為當前frame判斷成立,會執行中斷。

def dispatch_return(self, frame, arg): if self。stop_here(frame) or frame == self。returnframe: self。user_return(frame, arg) if self。quitting: raise BdbQuit return self。trace_dispatch

unt(until)

命令定義

執行到下一行,和next的區別就在於for迴圈只會跟蹤一次

程式碼分析

設定stopframe和returnframe為當前frame,stoplineno為當前lineno+1。

def stop_here(self, frame): 。。。 # 如果當前幀程式碼順序執行,下一個frame的lineno==stoplineno # 如果執行到for迴圈的最後一行,下一個frame(for迴圈第一行)的lineno= self。stoplineno # 如果在非botframe中,會先追溯到stopframe,返回false,同next while frame is not None and frame is not self。stopframe: if frame is self。botframe: return True frame = frame。f_back return False

如果在當前frame中有for迴圈,只會從上向下執行一次。

如果是函式返回return事件,下一個frame的lineno有可能小於stoplineno,所以把returnframe設定為當前frame,這樣函式執行就和next表現一樣了。

u(up)/d(down)

命令定義

切換到上/下一個棧幀

程式碼分析

棧幀資訊

棧幀包含程式碼呼叫路徑上的每一級frame資訊,每次命令執行中斷都會重新整理,可以透過u/d命令上下切換frame。

棧幀獲取主要透過get_stack方法,第一個引數是frame,第二個引數是traceback object。

traceback object是在exception事件產生的,exception事件會帶一個arg引數:

exc_type, exc_value, exc_traceback = arg(, (2, ‘No such file or directory’, ‘wdwrg’),

traceback object有幾個常用的屬性:

tb_frame:當前exception發生在的frame

tb_lineno:當前exception發生在的frame的行號,即frame。tb_lineno

tb_next:指向堆疊下一級呼叫的exc_traceback(traceback object),如果是最頂層則為None

Python Pdb 原始碼解析

棧幀資訊由兩部分組成,frame的呼叫棧和異常棧(如有),順序為:botframe -> frame1 -> frame2 -> tb1 -> tb2(出錯tb)

def get_stack(self, f, t): stack = [] if t and t。tb_frame is f: t = t。tb_next # frame呼叫棧,從底到頂 while f is not None: stack。append((f, f。f_lineno)) if f is self。botframe: break f = f。f_back stack。reverse() i = max(0, len(stack) - 1) # 異常棧,從底到頂(出錯棧) while t is not None: stack。append((t。tb_frame, t。tb_lineno)) t = t。tb_next if f is None: i = max(0, len(stack) - 1) return stack, i

pdb每次執行中斷都會更新呼叫的棧幀表,以及當前的棧幀資訊,堆疊切換隻要向上/下切換索引即可。

def setup(self, f, t): self。forget() self。stack, self。curindex = self。get_stack(f, t) self。curframe_locals = self。curframe。f_locals ……def do_up(self, arg): if self。curindex == 0: print >>self。stdout, ‘*** Oldest frame’ else: self。curindex = self。curindex - 1 self。curframe = self。stack[self。curindex][0] self。curframe_locals = self。curframe。f_locals self。print_stack_entry(self。stack[self。curindex]) self。lineno = None

b(break)

區別於過程控制的除錯命令,break命令用來設定斷點,不會馬上影響程式中斷狀態,但可能會影響後續的中斷。

在line事件發生的時候,除了stop_here會增加break_here的條件判斷,設定斷點的實現比較簡單,這裡主要介紹對函式設定斷點的時候,是怎麼讓程式碼執行到函式第一行中斷的。

設定斷點時,斷點的lineno為了函式的第一行:

# 函式斷點示例:break funcdef do_break(self, arg, temporary = 0): 。。。 if hasattr(func, ‘im_func’): func = func。im_func funcname = code。co_name lineno = code。co_firstlineno filename = code。co_filename

當line事件執行到函式的第一行程式碼時,這一行沒有主動設定過斷點,但是函式第一行co_firstlineno命中斷點,所以會繼續判斷斷點有效性。

def break_here(self, frame): 。。。 lineno = frame。f_lineno if not lineno in self。breaks[filename]: lineno = frame。f_code。co_firstlineno if not lineno in self。breaks[filename]: return False # flag says ok to delete temp。 bp (bp, flag) = effective(filename, lineno, frame)

斷點的有效性判斷透過effective方法,其中處理了ignore、enabled這些配置,對函式斷點的有效性判斷透過checkfuncname方法:

def checkfuncname(b, frame): “”“Check whether we should break here because of `b。funcname`。”“” 。。。 # Breakpoint set via function name。 。。。 # We are in the right frame。 if not b。func_first_executable_line: # The function is entered for the 1st time。 b。func_first_executable_line = frame。f_lineno if b。func_first_executable_line != frame。f_lineno: # But we are not at the first line number: don‘t break。 return False return True

在line事件在函式第一行發生時,func_first_executable_line還沒有,於是設定為當前行號,並且斷點生效,因此函式執行到第一行中斷。

接下來line到行數的後面行時,因為func_first_executable_line已經有值,並且肯定不等於當前行號,所以break_here判斷為無效,不會中斷。

例項分析

以下結合一個很簡單的Python程式碼除錯的例子,複習下上述命令的實現原理:

Python Pdb 原始碼解析

在控制檯中,命令列執行快照:

Python Pdb 原始碼解析

命令列中執行Python test。py,Python程式碼實際是從第一行開始執行的,但因為pdb。set_trace()是在__main__中呼叫的,所以實際是從set_trace的下一行才掛載到pdb的跟蹤函式,開始frame的中斷控制。

這段Python程式碼執行會經過經過3個frame:

1。底層根frame0,即_main_所在的frame0,其中包含一斷for迴圈程式碼,frame0的back frame為None

2。第二層frame1,進入func方法所在的frame1,frame1的back frame為frame0

3。頂層frame2,進入add方法所在的frame2,frame2的back frame為frame1

除錯過程:

1。跟蹤_main_所在的frame(根frame0),在20行觸發line事件

2。使用者輸入unt命令回車,frame0在21行觸發line事件,行號等於上一次跟蹤行號+1,stop_here成立,中斷等待

3。使用者輸入unt命令回車,同2,在22行中斷

4。使用者輸入unt命令回車,程式碼跟蹤至frame0在20行觸發line事件,行號小於上一次跟蹤行號+1(23),stop_here不成立,繼續執行

5。在24行觸發line事件,行號大於上一次跟蹤行號+1(23),stop_here成立,中斷等待

6。使用者輸入s命令回車,程式碼跟蹤至frame1在12行觸發call事件,step執行粒度和sys。settrace一樣,在12行中斷等待

7。使用者設定add函式斷點,斷點列表中會加入add函式的第一行(第7行)的斷點

8。使用者輸入c命令回車,stop_here總返回false,繼續跟蹤執行直到在第8行觸發line事件,雖然第8行不再斷點列表中,但當前函式幀firstlineno在,並且有效,所以在第8行中斷等待

9。使用者輸入r命令回車,後面的line事件處理中stop_here都返回false,直到在第10行觸發return事件,此時returnframe為當前frame,在10行中斷等待

10。使用者輸入up命令,棧幀向前切換索引,回到上一幀frame1,也就是第13行func中呼叫add的地方

11。使用者輸入down命令,棧幀向前後切換索引,回到當前幀

12。使用者輸入n命令,執行至下一次跟蹤14行(line事件),這一次跟蹤在frame1上,能追溯到botframe,所以在14行中斷

13。使用者輸入n命令,執行至下一次跟蹤14行(return事件),還在當前frame1中,中斷

14。使用者輸入n命令,執行至下一次跟蹤24行(return事件),這一次跟蹤就是botframe(frame0),中斷

15。使用者輸入n命令,frame0執行結束。

小結

Python標準庫提供的pdb的實現並不複雜,本文對原始碼中的核心的邏輯做了講解,如果你瞭解其原理,也可以自己定製或重寫一個Python偵錯程式。

事實上,業界的很多通用IDE如pycharm、vscode等都沒有使用標準的pdb,他們開發了自己的Python偵錯程式來更好的適配IDE。

不過了解pdb原理,在pdb上改寫和定製偵錯程式來滿足除錯需求,也是一種成本低而有效的方式。

Top