Python源码剖析[16] —— Pyc文件解析
Python源碼剖析[16] —— Pyc文件解析??
2008-02-28 18:29:55|??分類: Python |舉報 |字號?訂閱
Python源碼剖析
——Pyc文件解析
本文作者: Robert Chen (search.pythoner@gmail.com )
1.????? PyCodeObject與Pyc文件
通常認為,Python是一種解釋性的語言,但是這種說法是不正確的,實際上,Python在執行時,首先會將.py文件中的源代碼編譯成Python的byte code(字節碼),然后再由Python Virtual Machine來執行這些編譯好的byte code。這種機制的基本思想跟Java,.NET是一致的。然而,Python Virtual Machine與Java或.NET的Virtual Machine不同的是,Python的Virtual Machine是一種更高級的Virtual Machine。這里的高級并不是通常意義上的高級,不是說Python的Virtual Machine比Java或.NET的功能更強大,更拽,而是說和Java或.NET相比,Python的Virtual Machine距離真實機器的距離更遠。或者可以這么說,Python的Virtual Machine是一種抽象層次更高的Virtual Machine。
?????? 我們來考慮下面的Python代碼:
[demo.py]
class A:
??? pass
?
def Fun():
??? pass
?
value = 1
str = “Python”
a = A()
Fun()
??????
Python在執行CodeObject.py時,首先需要進行的動作就是對其進行編譯,編譯的結果是什么呢?當然有字節碼,否則Python也就沒辦法在玩下去了。然而除了字節碼之外,還包含其它一些結果,這些結果也是Python運行的時候所必需的。看一下我們的demo.py,用我們的眼睛來解析一下,從這個文件中,我們可以看到,其中包含了一些字符串,一些常量值,還有一些操作。當然,Python對操作的處理結果就是自己碼。那么Python的編譯過程對字符串和常量值的處理結果是什么呢?實際上,這些在Python源代碼中包含的靜態的信息都會被Python收集起來,編譯的結果中包含了字符串,常量值,字節碼等等在源代碼中出現的一切有用的靜態信息。而這些信息最終會被存儲在Python運行期的一個對象中,當Python運行結束后,這些信息甚至還會被存儲在一種文件中。這個對象和文件就是我們這章探索的重點:PyCodeObject對象和Pyc文件。
可以說,PyCodeObject就是Python源代碼編譯之后的關于程序的靜態信息的集合:
[compile.h] /* Bytecode object */ typedef struct { ??? PyObject_HEAD ??? int co_argcount;??????? /* #arguments, except *args */ ??? int co_nlocals;???? /* #local variables */ ??? int co_stacksize;?????? /* #entries needed for evaluation stack */ ??? int co_flags;?????? /* CO_..., see below */ ??? PyObject *co_code;????? /* instruction opcodes */ ??? PyObject *co_consts;??? /* list (constants used) */ ??? PyObject *co_names;???? /* list of strings (names used) */ ??? PyObject *co_varnames;? /* tuple of strings (local variable names) */ ??? PyObject *co_freevars;? /* tuple of strings (free variable names) */ ??? PyObject *co_cellvars;????? /* tuple of strings (cell variable names) */ ??? /* The rest doesn't count for hash/cmp */ ??? PyObject *co_filename;? /* string (where it was loaded from) */ ??? PyObject *co_name;????? /* string (name, for reference) */ ??? int co_firstlineno;???? /* first source line number */ ??? PyObject *co_lnotab;??? /* string (encoding addr<->lineno mapping) */ } PyCodeObject;
?
在對Python源代碼進行編譯的時候,對于一段Code(Code Block),會創建一個PyCodeObject與這段Code對應。那么如何確定多少代碼算是一個Code Block呢,事實上,當進入新的作用域時,就開始了新的一段Code。也就是說,對于下面的這一段Python源代碼:
[CodeObject.py]
class A:
??? pass
?
def Fun():
??? pass
?
a = A()
Fun()
?
在Python編譯完成后,一共會創建3個PyCodeObject對象,一個是對應CodeObject.py的,一個是對應class A這段Code(作用域),而最后一個是對應def Fun這段Code的。每一個PyCodeObject對象中都包含了每一個代碼塊經過編譯后得到的byte code。但是不幸的是,Python在執行完這些byte code后,會銷毀PyCodeObject,所以下次再次執行這個.py文件時,Python需要重新編譯源代碼,創建三個PyCodeObject,然后執行byte code。
很不爽,對不對?Python應該提供一種機制,保存編譯的中間結果,即byte code,或者更準確地說,保存PyCodeObject。事實上,Python確實提供了這樣一種機制——Pyc文件。
Python中的pyc文件正是保存PyCodeObject的關鍵所在,我們對Python解釋器的分析就從pyc文件,從pyc文件的格式開始。
在分析pyc的文件格式之前,我們先來看看如何產生pyc文件。在執行一個.py文件中的源代碼之后,Python并不會自動生成與該.py文件對應的.pyc文件。我們需要自己觸發Python來創建pyc文件。下面我們提供一種使Python創建pyc文件的方法,其實很簡單,就是利用Python的import機制。
在Python運行的過程中,如果碰到import abc,這樣的語句,那么Python將到設定好的path中尋找abc.pyc或abc.dll文件,如果沒有這些文件,而只是發現了abc.py,那么Python會首先將abc.py編譯成相應的PyCodeObject的中間結果,然后創建abc.pyc文件,并將中間結果寫入該文件。接下來,Python才會對abc.pyc文件進行一個import的動作,實際上也就是將abc.pyc文件中的PyCodeObject重新在內存中復制出來。了解了這個過程,我們很容易利用下面所示的generator.py來創建上面那段代碼(CodeObjectt.py)對應的pyc文件了。
| generator.py | CodeObject.py |
| import test print "Done"
? | class A: pass
? def Fun(): pass
? a = A() Fun() |
?
圖1所示的是Python產生的pyc文件:
可以看到,pyc是一個二進制文件,那么Python如何解釋這一堆看上去毫無意義的字節流就至關重要了。這也就是pyc文件的格式。
要了解pyc文件的格式,首先我們必須要清楚PyCodeObject中每一個域都表示什么含義,這一點是無論如何不能繞過去的。
| Field | Content |
| co_argcount | Code Block的參數的個數,比如說一個函數的參數 |
| co_nlocals | Code Block中局部變量的個數 |
| co_stacksize | 執行該段Code Block需要的??臻g |
| co_flags | N/A |
| co_code | Code Block編譯所得的byte code。以PyStringObject的形式存在 |
| co_consts | PyTupleObject對象,保存該Block中的常量 |
| co_names | PyTupleObject對象,保存該Block中的所有符號 |
| co_varnames | N/A |
| co_freevars | N/A |
| co_cellvars | N/A |
| co_filename | Code Block所對應的.py文件的完整路徑 |
| co_name | Code Block的名字,通常是函數名或類名 |
| co_firstlineno | Code Block在對應的.py文件中的起始行 |
| co_lnotab | byte code與.py文件中source code行號的對應關系,以PyStringObject的形式存在 |
需要說明一下的是co_lnotab域。在Python2.3以前,有一個byte code,喚做SET_LINENO,這個byte code會記錄.py文件中source code的位置信息,這個信息對于調試和顯示異常信息都有用。但是,從Python2.3之后,Python在編譯時不會再產生這個byte code,相應的,Python在編譯時,將這個信息記錄到了co_lnotab中。
co_lnotab中的byte code和source code的對應信息是以unsigned bytes的數組形式存在的,數組的形式可以看作(byte code在co_code中位置增量,代碼行數增量)形式的一個list。比如對于下面的例子:
| Byte code在co_code中的偏移 | .py文件中源代碼的行數 |
| 0 | 1 |
| 6 | 2 |
| 50 | 7 |
這里有一個小小的技巧,Python不會直接記錄這些信息,相反,它會記錄這些信息間的增量值,所以,對應的co_lnotab就應該是:0,1, 6,1, 44,5。
2.????? Pyc文件的生成
前面我們提到,Python在import時,如果沒有找到相應的pyc文件或dll文件,就會在py文件的基礎上自動創建pyc文件。那么,要想了解pyc的格式到底是什么樣的,我們只需要考察Python在將編譯得到的PyCodeObject寫入到pyc文件中時到底進行了怎樣的動作就可以了。下面的函數就是我們的切入點:
[import.c] static void write_compiled_module(PyCodeObject *co, char *cpathname, long mtime) { ??? FILE *fp; ??? fp = open_exclusive(cpathname); ??? PyMarshal_WriteLongToFile(pyc_magic, fp, Py_MARSHAL_VERSION); ??? ????/* First write a 0 for mtime */ ??? PyMarshal_WriteLongToFile(0L, fp, Py_MARSHAL_VERSION); ??? PyMarshal_WriteObjectToFile((PyObject *)co, fp, Py_MARSHAL_VERSION); ??? ????/* Now write the true mtime */ ??? fseek(fp, 4L, 0); ??? PyMarshal_WriteLongToFile(mtime, fp, Py_MARSHAL_VERSION); ??? fflush(fp); ??? fclose(fp); }
?
這里的cpathname當然是pyc文件的絕對路徑。首先我們看到會將pyc_magic這個值寫入到文件的開頭。實際上,pyc?_magic對應一個MAGIC的值。MAGIC是用來保證Python兼容性的一個措施。比如說要防止Python2.4的運行環境加載由Python1.5產生的pyc文件,那么只需要將Python2.4和Python1.5的MAGIC設為不同的值就可以了。Python在加載pyc文件時會首先檢查這個MAGIC值,從而拒絕加載不兼容的pyc文件。那么pyc文件為什么會不兼容了,一個最主要的原因是byte code的變化,由于Python一直在不斷地改進,有一些byte code退出了歷史舞臺,比如上面提到的SET_LINENO;或者由于一些新的語法特性會加入新的byte code,這些都會導致Python的不兼容問題。
pyc文件的寫入動作最后會集中到下面所示的幾個函數中(這里假設代碼只處理寫入到文件,即p->fp是有效的。因此代碼有刪減,另有一個w_short未列出。缺失部分,請參考Python源代碼):
[marshal.c] typedef struct { ??? FILE *fp; ??? int error; ??? int depth; ??? PyObject *strings; /* dict on marshal, list on unmarshal */ } WFILE;?
#define w_byte(c, p) putc((c), (p)->fp)?
static void w_long(long x, WFILE *p) { ??? w_byte((char)( x????? & 0xff), p); ??? w_byte((char)((x>> 8) & 0xff), p); ??? w_byte((char)((x>>16) & 0xff), p); ??? w_byte((char)((x>>24) & 0xff), p); }?
static void w_string(char *s, int n, WFILE *p) { ??? fwrite(s, 1, n, p->fp); }
?
在調用PyMarshal_WriteLongToFile時,會直接調用w_long,但是在調用PyMarshal_WriteObjectToFile時,還會通過一個間接的函數:w_object。需要特別注意的是PyMarshal_WriteObjectToFile的第一個參數,這個參數正是Python編譯出來的PyCodeObject對象。
w_object的代碼非常長,這里就不全部列出。其實w_object的邏輯非常簡單,就是對應不同的對象,比如string,int,list等,會有不同的寫的動作,然而其最終目的都是通過最基本的w_long或w_string將整個PyCodeObject寫入到pyc文件中。
對于PyCodeObject,很顯然,會遍歷PyCodeObject中的所有域,將這些域依次寫入:
[marshal.c] static void w_object(PyObject *v, WFILE *p) { ??? …… ??? else if (PyCode_Check(v)) ????{ ??????? PyCodeObject *co = (PyCodeObject *)v; ??????? w_byte(TYPE_CODE, p); ??????? w_long(co->co_argcount, p); ??????? w_long(co->co_nlocals, p); ??????? w_long(co->co_stacksize, p); ??????? w_long(co->co_flags, p); ??????? w_object(co->co_code, p); ??????? w_object(co->co_consts, p); ??????? w_object(co->co_names, p); ??????? w_object(co->co_varnames, p); ??????? w_object(co->co_freevars, p); ??????? w_object(co->co_cellvars, p); ??????? w_object(co->co_filename, p); ??????? w_object(co->co_name, p); ??????? w_long(co->co_firstlineno, p); ??????? w_object(co->co_lnotab, p); } …… }
?
而對于一個PyListObject對象,想象一下會有什么動作?沒錯,還是遍歷!!!:
[w_object() in marshal.c] …… else if (PyList_Check(v)) ????{ ??????? w_byte(TYPE_LIST, p); ??????? n = PyList_GET_SIZE(v); ??????? w_long((long)n, p); ??????? for (i = 0; i < n; i++) ????????{ ??????????? w_object(PyList_GET_ITEM(v, i), p); ??????? } } ……
?
而如果是PyIntObject,嗯,那太簡單了,幾乎沒有什么可說的:
[w_object() in marshal.c]……
else if (PyInt_Check(v)) ????{ ??????? w_byte(TYPE_INT, p); ??????? w_long(x, p); ??? }……
?
有沒有注意到TYPE_LIST,TYPE_CODE,TYPE_INT這樣的標志?pyc文件正是利用這些標志來表示一個新的對象的開始,當加載pyc文件時,加載器才能知道在什么時候應該進行什么樣的加載動作。這些標志同樣也是在import.c中定義的:
[import.c] #define TYPE_NULL?? '0' #define TYPE_NONE?? 'N' 。。。。。。 #define TYPE_INT??? 'i' #define TYPE_STRING 's' #define TYPE_INTERNED?? 't' #define TYPE_STRINGREF? 'R' #define TYPE_TUPLE? '(' #define TYPE_LIST?? '[' #define TYPE_CODE?? 'c'
?
到了這里,可以看到,Python對于中間結果的導出實際是不復雜的。實際上在write的動作中,不論面臨PyCodeObject還是PyListObject這些復雜對象,最后都會歸結為簡單的兩種形式,一個是對數值的寫入,一個是對字符串的寫入。上面其實我們已經看到了對數值的寫入過程。在寫入字符串時,有一套比較復雜的機制。在了解字符串的寫入機制前,我們首先需要了解一個寫入過程中關鍵的結構體WFILE(有刪節):
[marshal.c] typedef struct { ??? FILE *fp; ??? int error; ??? int depth; ??? PyObject *strings; /* dict on marshal, list on unmarshal */ } WFILE;
?
這里我們也只考慮fp有效,即寫入到文件,的情況。WFILE可以看作是一個對FILE*的簡單包裝,但是在WFILE里,出現了一個奇特的strings域。這個域是在pyc文件中寫入或讀出字符串的關鍵所在,當向pyc中寫入時,string會是一個PyDictObject對象;而從pyc中讀出時,string則會是一個PyListObject對象。
[marshal.c] void PyMarshal_WriteObjectToFile(PyObject *x, FILE *fp, int version) { ??? WFILE wf; ??? wf.fp = fp; ??? wf.error = 0; ??? wf.depth = 0; ??? wf.strings = (version > 0) ? PyDict_New() : NULL; ??? w_object(x, &wf); }
?
可以看到,strings在真正開始寫入之前,就已經被創建了。在w_object中對于字符串的處理部分,我們可以看到對strings的使用:
[w_object() in marshal.c]……
else if (PyString_Check(v)) ????{ ??????? if (p->strings && PyString_CHECK_INTERNED(v)) ????????{ ??????????? PyObject *o = PyDict_GetItem(p->strings, v); ??????????? if (o) ????????????{ ??????????????? long w = PyInt_AsLong(o); ??????????????? w_byte(TYPE_STRINGREF, p); ??????????????? w_long(w, p); ??????????????? goto exit; ??????????? } ??????????? else ????????????{ ??????????????? o = PyInt_FromLong(PyDict_Size(p->strings)); ??????????????? PyDict_SetItem(p->strings, v, o); ??????????????? Py_DECREF(o); ??????????????? w_byte(TYPE_INTERNED, p); ??????????? } ??????? } ??????? else ????????{ ??????????? w_byte(TYPE_STRING, p); ??????? } ??????? n = PyString_GET_SIZE(v); ??????? w_long((long)n, p); ??????? w_string(PyString_AS_STRING(v), n, p); }……
?
真正有趣的事發生在這個字符串是一個需要被進行INTERN操作的字符串時??梢钥吹?#xff0c;WFILE的strings域實際上是一個從string映射到int的一個PyDictObject對象。這個int值是什么呢,這個int值是表示對應的string是第幾個被加入到WFILE.strings中的字符串。
這個int值看上去似乎沒有必要,記錄一個string被加入到WFILE.strings中的序號有什么意義呢?好,讓我們來考慮下面的情形:
假設我們需要向pyc文件中寫入三個string:”Jython”, “Ruby”, “Jython”,而且這三個string都需要被進行INTERN操作。對于前兩個string,沒有任何問題,閉著眼睛寫入就是了。完成了前兩個string的寫入后,WFILE.strings與pyc文件的情況如圖2所示:
在寫入第三個字符串的時候,麻煩來了。對于這個“Jython”,我們應該怎么處理呢? 是按照上兩個string一樣嗎?如果這樣的話,那么寫入后,WFILE.strings和pyc的情況如圖3所示:我們可以不管WFILE.strings怎么樣了,但是一看pyc文件,我們就知道,問題來了。在pyc文件中,出現了重復的內容,關于“Jython”的信息重復了兩次,這會引起什么麻煩呢?想象一下在python代碼中,我們創建了一個button,在此之后,多次使用了button,這樣,在代碼中,“button”將出現多次。想象一下吧,我們的pyc文件會變得多么臃腫,而其中充斥的只是毫無價值的冗余信息。如果你是Guido,你能忍受這樣的設計嗎?當然不能!!于是Guido給了我們TYPE_STRINGREF這個東西。在解析pyc文件時,這個標志表明后面的一個數值表示了一個索引值,根據這個索引值到WFILE.strings中去查找,就能找到需要的string了。
有了TYPE_STRINGREF,我們的pyc文件就能變得苗條了,如圖4所示:
看一下加載pyc文件的過程,我們就能對這個機制更加地明了了。前面我們提到,在讀入pyc文件時,WFILE.strings是一個PyListObject對象,所以在讀入前兩個字符串后,WFILE.strings的情形如圖5所示:
在加載緊接著的(R,0)時,因為解析到是一個TYPE_STRINGREF標志,所以直接以標志后面的數值0位索引訪問WFILE.strings,立刻可得到字符串“Jython”。
3.????? 一個PyCodeObject,多個PyCodeObject?
到了這里,關于PyCodeObject與pyc文件,我們只剩下最后一個有趣的話題了。還記得前面那個test.py嗎?我們說那段簡單的什么都做不了的python代碼就要產生三個PyCodeObject。而在write_compiled_module中我們又親眼看到,Python運行環境只會對一個PyCodeObject對象調用PyMarshal_WriteObjectToFile操作。剎那間,我們竟然看到了兩個遺失的PyCodeObject對象。
Python顯然不會犯這樣低級的錯誤,想象一下,如果你是Guido,這個問題該如何解決?首先我們會假想,有兩個PyCodeObject對象一定是包含在另一個PyCodeObject中的。沒錯,確實如此,還記得我們最開始指出的Python是如何確定一個Code Block的嗎?對嘍,就是作用域。仔細看一下test.py,你會發現作用域呈現出一種嵌套的結構,這種結構也正是PyCodeObject對象之間的結構。所以到現在清楚了,與Fun和A對應得PyCodeObject對象一定是包含在與全局作用域對應的PyCodeObject對象中的,而PyCodeObject結構中的co_consts域正是這兩個PyCodeObject對象的藏身之處,如圖6所示:
在對一個PyCodeObject對象進行寫入到pyc文件的操作時,如果碰到它包含的另一個PyCodeObject對象,那么就會遞歸地執行寫入PyCodeObject對象的操作。如此下去,最終所有的PyCodeObject對象都會被寫入到pyc文件中去。而且pyc文件中的PyCodeObject對象也是以一種嵌套的關系聯系在一起的。
4.????? Python字節碼
Python源代碼在執行前會被編譯為Python的byte code,Python的執行引擎就是根據這些byte code來進行一系列的操作,從而完成對Python程序的執行。在Python2.4.1中,一共定義了103條byte code:
[opcode.h]
#define STOP_CODE?? 0
#define POP_TOP???? 1
#define ROT_TWO???? 2
……
#define CALL_FUNCTION_KW?????????? 141
#define CALL_FUNCTION_VAR_KW?????? 142
#define EXTENDED_ARG? 143
?
?????? 所有這些字節碼的操作含義在Python自帶的文檔中有專門的一頁進行描述,當然,也可以到下面的網址察看:http://docs.python.org/lib/bytecodes.html。
細心的你一定發現了,byte code的編碼卻到了143。沒錯,Python2.4.1中byte code的編碼并沒有按順序增長,比如編碼為5的ROT_FOUR之后就是編碼為9的NOP。這可能是歷史遺留下來的,你知道,在咱們這行,歷史問題不是什么好東西,搞得現在還有許多人不得不很郁悶地面對MFC :)
Python的143條byte code中,有一部分是需要參數的,另一部分是沒有參數的。所有需要參數的byte code的編碼都大于或等于90。Python中提供了專門的宏來判斷一條byte code是否需要參數:
[opcode.h]
#define HAS_ARG(op) ((op) >= HAVE_ARGUMENT)
?
好了,到了現在,關于PyCodeObject和pyc文件的一切我們都已了如指掌了,關于Python的現在我們可以做一些非常有趣的事了。呃,在我看來,最有趣的事莫過于自己寫一個pyc文件的解析器。沒錯,利用我們現在所知道的一切,我們真的可以這么做了。圖7展現的是對本章前面的那個test.py的解析結果:
?
?
更進一步,我們還可以解析byte code。前面我們已經知道,Python在生成pyc文件時,會將PyCodeObject對象中的byte code也寫入到pyc文件中,而且這個pyc文件中還記錄了每一條byte code與Python源代碼的對應關系,嗯,就是那個co_lnotab啦。假如現在我們知道了byte code在co_code中的偏移地址,那么與這條byte code對應的Python源代碼的位置可以通過下面的算法得到(Python偽代碼):
lineno = addr = 0
for addr_incr, line_incr in c_lnotab:
??? ?addr += addr_incr
??? ?if addr > A:
???????? return lineno
? lineno += line_incr
?
下面是對一段Python源代碼反編譯為byte code的結果,這個結果也將作為下一章對Python執行引擎的分析的開始:
i = 1
#?? LOAD_CONST?? 0
#?? STORE_NAME?? 0
?
s = "Python"
#?? LOAD_CONST?? 1
#?? STORE_NAME?? 1
?
d = {}
#?? BUILD_MAP?? 0
#?? STORE_NAME?? 2
?
l = []
#?? BUILD_LIST?? 0
#?? STORE_NAME?? 3
#?? LOAD_CONST?? 2
#?? RETURN_VALUE?? none
?
再往前想一想,從現在到達的地方出發,實際上我們就可以做出一個Python的執行引擎了,哇,這是多么激動人心的事啊。遙遠的天空,一抹朝陽,緩緩升起了……
事實上,Python標準庫中提供了對python進行反編譯的工具dis,利用這個工具,可以很容易地得到我們在這里得到的結果,當然,還要更詳細一些,圖8展示了利用dis工具對CodeObject.py進行反編譯的結果:
在圖8顯示的結果中,最左面一列顯示的是CodeObject.py中源代碼的行數,左起第二列顯示的是當前的字節碼指令在co_code中的偏移位置。
在以后的分析中,我們大部分將采用dis工具的反編譯結果,在有些特殊情況下會使用我們自己的反編譯結果。
總結
以上是生活随笔為你收集整理的Python源码剖析[16] —— Pyc文件解析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 中科院遗传所钱文峰组发表新冠病毒源于自然
- 下一篇: 临床外显子组测序分析中的那些坑(上)