一般認爲,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。html
咱們來考慮下面的Python代碼:python
[demo.py]算法
class A:數組
passapp
def Fun():函數
pass工具
value = 1ui
str = 「Python」編碼
a = A()lua
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須要的棧空間 |
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。
前面咱們提到,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操做的字符串時。能夠看到,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所示:
![](http://static.javashuo.com/static/loading.gif)
咱們能夠無論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」。
到了這裏,關於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對象也是以一種嵌套的關係聯繫在一塊兒的。
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工具的反編譯結果,在有些特殊狀況下會使用咱們本身的反編譯結果。