python源码解读_Python源码剖析[16] —— Pyc文件解析
Python源碼剖析[16] —— Pyc文件解析
Python源碼剖析[16]?——?Pyc文件解析
2008-02-28?18:29:55|??分類:?Python?|舉報(bào)|字號(hào)?訂閱
Python源碼剖析
——Pyc文件解析
本文作者:?Robert?Chen?(search.pythoner@gmail.com?)
1.??????PyCodeObject與Pyc文件
通常認(rèn)為,Python是一種解釋性的語言,但是這種說法是不正確的,實(shí)際上,Python在執(zhí)行時(shí),首先會(huì)將.py文件中的源代碼編譯成Python的byte?code(字節(jié)碼),然后再由Python?Virtual?Machine來執(zhí)行這些編譯好的byte?code。這種機(jī)制的基本思想跟Java,.NET是一致的。然而,Python?Virtual?Machine與Java或.NET的Virtual?Machine不同的是,Python的Virtual?Machine是一種更高級(jí)的Virtual?Machine。這里的高級(jí)并不是通常意義上的高級(jí),不是說Python的Virtual?Machine比Java或.NET的功能更強(qiáng)大,更拽,而是說和Java或.NET相比,Python的Virtual?Machine距離真實(shí)機(jī)器的距離更遠(yuǎn)。或者可以這么說,Python的Virtual?Machine是一種抽象層次更高的Virtual?Machine。
我們來考慮下面的Python代碼:
[demo.py]
class?A:
pass
def?Fun():
pass
value?=?1
str?=?“Python”
a?=?A()
Fun()
Python在執(zhí)行CodeObject.py時(shí),首先需要進(jìn)行的動(dòng)作就是對(duì)其進(jìn)行編譯,編譯的結(jié)果是什么呢?當(dāng)然有字節(jié)碼,否則Python也就沒辦法在玩下去了。然而除了字節(jié)碼之外,還包含其它一些結(jié)果,這些結(jié)果也是Python運(yùn)行的時(shí)候所必需的。看一下我們的demo.py,用我們的眼睛來解析一下,從這個(gè)文件中,我們可以看到,其中包含了一些字符串,一些常量值,還有一些操作。當(dāng)然,Python對(duì)操作的處理結(jié)果就是自己碼。那么Python的編譯過程對(duì)字符串和常量值的處理結(jié)果是什么呢?實(shí)際上,這些在Python源代碼中包含的靜態(tài)的信息都會(huì)被Python收集起來,編譯的結(jié)果中包含了字符串,常量值,字節(jié)碼等等在源代碼中出現(xiàn)的一切有用的靜態(tài)信息。而這些信息最終會(huì)被存儲(chǔ)在Python運(yùn)行期的一個(gè)對(duì)象中,當(dāng)Python運(yùn)行結(jié)束后,這些信息甚至還會(huì)被存儲(chǔ)在一種文件中。這個(gè)對(duì)象和文件就是我們這章探索的重點(diǎn):PyCodeObject對(duì)象和Pyc文件。
可以說,PyCodeObject就是Python源代碼編譯之后的關(guān)于程序的靜態(tài)信息的集合:
[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?addrlineno?mapping)?*/
}?PyCodeObject;
在對(duì)Python源代碼進(jìn)行編譯的時(shí)候,對(duì)于一段Code(Code?Block),會(huì)創(chuàng)建一個(gè)PyCodeObject與這段Code對(duì)應(yīng)。那么如何確定多少代碼算是一個(gè)Code?Block呢,事實(shí)上,當(dāng)進(jìn)入新的作用域時(shí),就開始了新的一段Code。也就是說,對(duì)于下面的這一段Python源代碼:
[CodeObject.py]
class?A:
pass
def?Fun():
pass
a?=?A()
Fun()
在Python編譯完成后,一共會(huì)創(chuàng)建3個(gè)PyCodeObject對(duì)象,一個(gè)是對(duì)應(yīng)CodeObject.py的,一個(gè)是對(duì)應(yīng)class?A這段Code(作用域),而最后一個(gè)是對(duì)應(yīng)def?Fun這段Code的。每一個(gè)PyCodeObject對(duì)象中都包含了每一個(gè)代碼塊經(jīng)過編譯后得到的byte?code。但是不幸的是,Python在執(zhí)行完這些byte?code后,會(huì)銷毀PyCodeObject,所以下次再次執(zhí)行這個(gè).py文件時(shí),Python需要重新編譯源代碼,創(chuàng)建三個(gè)PyCodeObject,然后執(zhí)行byte?code。
很不爽,對(duì)不對(duì)?Python應(yīng)該提供一種機(jī)制,保存編譯的中間結(jié)果,即byte?code,或者更準(zhǔn)確地說,保存PyCodeObject。事實(shí)上,Python確實(shí)提供了這樣一種機(jī)制——Pyc文件。
Python中的pyc文件正是保存PyCodeObject的關(guān)鍵所在,我們對(duì)Python解釋器的分析就從pyc文件,從pyc文件的格式開始。
在分析pyc的文件格式之前,我們先來看看如何產(chǎn)生pyc文件。在執(zhí)行一個(gè).py文件中的源代碼之后,Python并不會(huì)自動(dòng)生成與該.py文件對(duì)應(yīng)的.pyc文件。我們需要自己觸發(fā)Python來創(chuàng)建pyc文件。下面我們提供一種使Python創(chuàng)建pyc文件的方法,其實(shí)很簡(jiǎn)單,就是利用Python的import機(jī)制。
在Python運(yùn)行的過程中,如果碰到import?abc,這樣的語句,那么Python將到設(shè)定好的path中尋找abc.pyc或abc.dll文件,如果沒有這些文件,而只是發(fā)現(xiàn)了abc.py,那么Python會(huì)首先將abc.py編譯成相應(yīng)的PyCodeObject的中間結(jié)果,然后創(chuàng)建abc.pyc文件,并將中間結(jié)果寫入該文件。接下來,Python才會(huì)對(duì)abc.pyc文件進(jìn)行一個(gè)import的動(dòng)作,實(shí)際上也就是將abc.pyc文件中的PyCodeObject重新在內(nèi)存中復(fù)制出來。了解了這個(gè)過程,我們很容易利用下面所示的generator.py來創(chuàng)建上面那段代碼(CodeObjectt.py)對(duì)應(yīng)的pyc文件了。
generator.py
CodeObject.py
import?test
print?"Done"
class?A:
pass
def?Fun():
pass
a?=?A()
Fun()
圖1所示的是Python產(chǎn)生的pyc文件:
可以看到,pyc是一個(gè)二進(jìn)制文件,那么Python如何解釋這一堆看上去毫無意義的字節(jié)流就至關(guān)重要了。這也就是pyc文件的格式。
要了解pyc文件的格式,首先我們必須要清楚PyCodeObject中每一個(gè)域都表示什么含義,這一點(diǎn)是無論如何不能繞過去的。
Field
Content
co_argcount
Code?Block的參數(shù)的個(gè)數(shù),比如說一個(gè)函數(shù)的參數(shù)
co_nlocals
Code?Block中局部變量的個(gè)數(shù)
co_stacksize
執(zhí)行該段Code?Block需要的棧空間
co_flags
N/A
co_code
Code?Block編譯所得的byte?code。以PyStringObject的形式存在
co_consts
PyTupleObject對(duì)象,保存該Block中的常量
co_names
PyTupleObject對(duì)象,保存該Block中的所有符號(hào)
co_varnames
N/A
co_freevars
N/A
co_cellvars
N/A
co_filename
Code?Block所對(duì)應(yīng)的.py文件的完整路徑
co_name
Code?Block的名字,通常是函數(shù)名或類名
co_firstlineno
Code?Block在對(duì)應(yīng)的.py文件中的起始行
co_lnotab
byte?code與.py文件中source?code行號(hào)的對(duì)應(yīng)關(guān)系,以PyStringObject的形式存在
需要說明一下的是co_lnotab域。在Python2.3以前,有一個(gè)byte?code,喚做SET_LINENO,這個(gè)byte?code會(huì)記錄.py文件中source?code的位置信息,這個(gè)信息對(duì)于調(diào)試和顯示異常信息都有用。但是,從Python2.3之后,Python在編譯時(shí)不會(huì)再產(chǎn)生這個(gè)byte?code,相應(yīng)的,Python在編譯時(shí),將這個(gè)信息記錄到了co_lnotab中。
co_lnotab中的byte?code和source?code的對(duì)應(yīng)信息是以u(píng)nsigned?bytes的數(shù)組形式存在的,數(shù)組的形式可以看作(byte?code在co_code中位置增量,代碼行數(shù)增量)形式的一個(gè)list。比如對(duì)于下面的例子:
Byte?code在co_code中的偏移
.py文件中源代碼的行數(shù)
0
1
6
2
50
7
這里有一個(gè)小小的技巧,Python不會(huì)直接記錄這些信息,相反,它會(huì)記錄這些信息間的增量值,所以,對(duì)應(yīng)的co_lnotab就應(yīng)該是:0,1,?6,1,?44,5。
2.??????Pyc文件的生成
前面我們提到,Python在import時(shí),如果沒有找到相應(yīng)的pyc文件或dll文件,就會(huì)在py文件的基礎(chǔ)上自動(dòng)創(chuàng)建pyc文件。那么,要想了解pyc的格式到底是什么樣的,我們只需要考察Python在將編譯得到的PyCodeObject寫入到pyc文件中時(shí)到底進(jìn)行了怎樣的動(dòng)作就可以了。下面的函數(shù)就是我們的切入點(diǎn):
[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當(dāng)然是pyc文件的絕對(duì)路徑。首先我們看到會(huì)將pyc_magic這個(gè)值寫入到文件的開頭。實(shí)際上,pyc?_magic對(duì)應(yīng)一個(gè)MAGIC的值。MAGIC是用來保證Python兼容性的一個(gè)措施。比如說要防止Python2.4的運(yùn)行環(huán)境加載由Python1.5產(chǎn)生的pyc文件,那么只需要將Python2.4和Python1.5的MAGIC設(shè)為不同的值就可以了。Python在加載pyc文件時(shí)會(huì)首先檢查這個(gè)MAGIC值,從而拒絕加載不兼容的pyc文件。那么pyc文件為什么會(huì)不兼容了,一個(gè)最主要的原因是byte?code的變化,由于Python一直在不斷地改進(jìn),有一些byte?code退出了歷史舞臺(tái),比如上面提到的SET_LINENO;或者由于一些新的語法特性會(huì)加入新的byte?code,這些都會(huì)導(dǎo)致Python的不兼容問題。
pyc文件的寫入動(dòng)作最后會(huì)集中到下面所示的幾個(gè)函數(shù)中(這里假設(shè)代碼只處理寫入到文件,即p->fp是有效的。因此代碼有刪減,另有一個(gè)w_short未列出。缺失部分,請(qǐng)參考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);
}
在調(diào)用PyMarshal_WriteLongToFile時(shí),會(huì)直接調(diào)用w_long,但是在調(diào)用PyMarshal_WriteObjectToFile時(shí),還會(huì)通過一個(gè)間接的函數(shù):w_object。需要特別注意的是PyMarshal_WriteObjectToFile的第一個(gè)參數(shù),這個(gè)參數(shù)正是Python編譯出來的PyCodeObject對(duì)象。
w_object的代碼非常長(zhǎng),這里就不全部列出。其實(shí)w_object的邏輯非常簡(jiǎn)單,就是對(duì)應(yīng)不同的對(duì)象,比如string,int,list等,會(huì)有不同的寫的動(dòng)作,然而其最終目的都是通過最基本的w_long或w_string將整個(gè)PyCodeObject寫入到pyc文件中。
對(duì)于PyCodeObject,很顯然,會(huì)遍歷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);
}
……
}
而對(duì)于一個(gè)PyListObject對(duì)象,想象一下會(huì)有什么動(dòng)作?沒錯(cuò),還是遍歷!!!:
[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?
{
w_object(PyList_GET_ITEM(v,?i),?p);
}
}
……
而如果是PyIntObject,嗯,那太簡(jiǎn)單了,幾乎沒有什么可說的:
[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這樣的標(biāo)志?pyc文件正是利用這些標(biāo)志來表示一個(gè)新的對(duì)象的開始,當(dāng)加載pyc文件時(shí),加載器才能知道在什么時(shí)候應(yīng)該進(jìn)行什么樣的加載動(dòng)作。這些標(biāo)志同樣也是在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對(duì)于中間結(jié)果的導(dǎo)出實(shí)際是不復(fù)雜的。實(shí)際上在write的動(dòng)作中,不論面臨PyCodeObject還是PyListObject這些復(fù)雜對(duì)象,最后都會(huì)歸結(jié)為簡(jiǎn)單的兩種形式,一個(gè)是對(duì)數(shù)值的寫入,一個(gè)是對(duì)字符串的寫入。上面其實(shí)我們已經(jīng)看到了對(duì)數(shù)值的寫入過程。在寫入字符串時(shí),有一套比較復(fù)雜的機(jī)制。在了解字符串的寫入機(jī)制前,我們首先需要了解一個(gè)寫入過程中關(guān)鍵的結(jié)構(gòu)體WFILE(有刪節(jié)):
[marshal.c]
typedef?struct?{
FILE?*fp;
int?error;
int?depth;
PyObject?*strings;?/*?dict?on?marshal,?list?on?unmarshal?*/
}?WFILE;
這里我們也只考慮fp有效,即寫入到文件,的情況。WFILE可以看作是一個(gè)對(duì)FILE*的簡(jiǎn)單包裝,但是在WFILE里,出現(xiàn)了一個(gè)奇特的strings域。這個(gè)域是在pyc文件中寫入或讀出字符串的關(guān)鍵所在,當(dāng)向pyc中寫入時(shí),string會(huì)是一個(gè)PyDictObject對(duì)象;而從pyc中讀出時(shí),string則會(huì)是一個(gè)PyListObject對(duì)象。
[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在真正開始寫入之前,就已經(jīng)被創(chuàng)建了。在w_object中對(duì)于字符串的處理部分,我們可以看到對(duì)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);
}
……
真正有趣的事發(fā)生在這個(gè)字符串是一個(gè)需要被進(jìn)行INTERN操作的字符串時(shí)。可以看到,WFILE的strings域?qū)嶋H上是一個(gè)從string映射到int的一個(gè)PyDictObject對(duì)象。這個(gè)int值是什么呢,這個(gè)int值是表示對(duì)應(yīng)的string是第幾個(gè)被加入到WFILE.strings中的字符串。
這個(gè)int值看上去似乎沒有必要,記錄一個(gè)string被加入到WFILE.strings中的序號(hào)有什么意義呢?好,讓我們來考慮下面的情形:
假設(shè)我們需要向pyc文件中寫入三個(gè)string:”Jython”,?“Ruby”,?“Jython”,而且這三個(gè)string都需要被進(jìn)行INTERN操作。對(duì)于前兩個(gè)string,沒有任何問題,閉著眼睛寫入就是了。完成了前兩個(gè)string的寫入后,WFILE.strings與pyc文件的情況如圖2所示:
在寫入第三個(gè)字符串的時(shí)候,麻煩來了。對(duì)于這個(gè)“Jython”,我們應(yīng)該怎么處理呢?
是按照上兩個(gè)string一樣嗎?如果這樣的話,那么寫入后,WFILE.strings和pyc的情況如圖3所示:
我們可以不管WFILE.strings怎么樣了,但是一看pyc文件,我們就知道,問題來了。在pyc文件中,出現(xiàn)了重復(fù)的內(nèi)容,關(guān)于“Jython”的信息重復(fù)了兩次,這會(huì)引起什么麻煩呢?想象一下在python代碼中,我們創(chuàng)建了一個(gè)button,在此之后,多次使用了button,這樣,在代碼中,“button”將出現(xiàn)多次。想象一下吧,我們的pyc文件會(huì)變得多么臃腫,而其中充斥的只是毫無價(jià)值的冗余信息。如果你是Guido,你能忍受這樣的設(shè)計(jì)嗎?當(dāng)然不能!!于是Guido給了我們TYPE_STRINGREF這個(gè)東西。在解析pyc文件時(shí),這個(gè)標(biāo)志表明后面的一個(gè)數(shù)值表示了一個(gè)索引值,根據(jù)這個(gè)索引值到WFILE.strings中去查找,就能找到需要的string了。
有了TYPE_STRINGREF,我們的pyc文件就能變得苗條了,如圖4所示:
看一下加載pyc文件的過程,我們就能對(duì)這個(gè)機(jī)制更加地明了了。前面我們提到,在讀入pyc文件時(shí),WFILE.strings是一個(gè)PyListObject對(duì)象,所以在讀入前兩個(gè)字符串后,WFILE.strings的情形如圖5所示:
在加載緊接著的(R,0)時(shí),因?yàn)榻馕龅绞且粋€(gè)TYPE_STRINGREF標(biāo)志,所以直接以標(biāo)志后面的數(shù)值0位索引訪問WFILE.strings,立刻可得到字符串“Jython”。
3.??????一個(gè)PyCodeObject,多個(gè)PyCodeObject?
到了這里,關(guān)于PyCodeObject與pyc文件,我們只剩下最后一個(gè)有趣的話題了。還記得前面那個(gè)test.py嗎?我們說那段簡(jiǎn)單的什么都做不了的python代碼就要產(chǎn)生三個(gè)PyCodeObject。而在write_compiled_module中我們又親眼看到,Python運(yùn)行環(huán)境只會(huì)對(duì)一個(gè)PyCodeObject對(duì)象調(diào)用PyMarshal_WriteObjectToFile操作。剎那間,我們竟然看到了兩個(gè)遺失的PyCodeObject對(duì)象。
Python顯然不會(huì)犯這樣低級(jí)的錯(cuò)誤,想象一下,如果你是Guido,這個(gè)問題該如何解決?首先我們會(huì)假想,有兩個(gè)PyCodeObject對(duì)象一定是包含在另一個(gè)PyCodeObject中的。沒錯(cuò),確實(shí)如此,還記得我們最開始指出的Python是如何確定一個(gè)Code?Block的嗎?對(duì)嘍,就是作用域。仔細(xì)看一下test.py,你會(huì)發(fā)現(xiàn)作用域呈現(xiàn)出一種嵌套的結(jié)構(gòu),這種結(jié)構(gòu)也正是PyCodeObject對(duì)象之間的結(jié)構(gòu)。所以到現(xiàn)在清楚了,與Fun和A對(duì)應(yīng)得PyCodeObject對(duì)象一定是包含在與全局作用域?qū)?yīng)的PyCodeObject對(duì)象中的,而PyCodeObject結(jié)構(gòu)中的co_consts域正是這兩個(gè)PyCodeObject對(duì)象的藏身之處,如圖6所示:
在對(duì)一個(gè)PyCodeObject對(duì)象進(jìn)行寫入到pyc文件的操作時(shí),如果碰到它包含的另一個(gè)PyCodeObject對(duì)象,那么就會(huì)遞歸地執(zhí)行寫入PyCodeObject對(duì)象的操作。如此下去,最終所有的PyCodeObject對(duì)象都會(huì)被寫入到pyc文件中去。而且pyc文件中的PyCodeObject對(duì)象也是以一種嵌套的關(guān)系聯(lián)系在一起的。
4.??????Python字節(jié)碼
Python源代碼在執(zhí)行前會(huì)被編譯為Python的byte?code,Python的執(zhí)行引擎就是根據(jù)這些byte?code來進(jìn)行一系列的操作,從而完成對(duì)Python程序的執(zhí)行。在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
所有這些字節(jié)碼的操作含義在Python自帶的文檔中有專門的一頁進(jìn)行描述,當(dāng)然,也可以到下面的網(wǎng)址察看:http://docs.python.org/lib/bytecodes.html。
細(xì)心的你一定發(fā)現(xiàn)了,byte?code的編碼卻到了143。沒錯(cuò),Python2.4.1中byte?code的編碼并沒有按順序增長(zhǎng),比如編碼為5的ROT_FOUR之后就是編碼為9的NOP。這可能是歷史遺留下來的,你知道,在咱們這行,歷史問題不是什么好東西,搞得現(xiàn)在還有許多人不得不很郁悶地面對(duì)MFC?:)
Python的143條byte?code中,有一部分是需要參數(shù)的,另一部分是沒有參數(shù)的。所有需要參數(shù)的byte?code的編碼都大于或等于90。Python中提供了專門的宏來判斷一條byte?code是否需要參數(shù):
[opcode.h]
#define?HAS_ARG(op)?((op)?>=?HAVE_ARGUMENT)
好了,到了現(xiàn)在,關(guān)于PyCodeObject和pyc文件的一切我們都已了如指掌了,關(guān)于Python的現(xiàn)在我們可以做一些非常有趣的事了。呃,在我看來,最有趣的事莫過于自己寫一個(gè)pyc文件的解析器。沒錯(cuò),利用我們現(xiàn)在所知道的一切,我們真的可以這么做了。圖7展現(xiàn)的是對(duì)本章前面的那個(gè)test.py的解析結(jié)果:
更進(jìn)一步,我們還可以解析byte?code。前面我們已經(jīng)知道,Python在生成pyc文件時(shí),會(huì)將PyCodeObject對(duì)象中的byte?code也寫入到pyc文件中,而且這個(gè)pyc文件中還記錄了每一條byte?code與Python源代碼的對(duì)應(yīng)關(guān)系,嗯,就是那個(gè)co_lnotab啦。假如現(xiàn)在我們知道了byte?code在co_code中的偏移地址,那么與這條byte?code對(duì)應(yīng)的Python源代碼的位置可以通過下面的算法得到(Python偽代碼):
lineno?=?addr?=?0
for?addr_incr,?line_incr?in?c_lnotab:
addr?+=?addr_incr
if?addr?>?A:
return?lineno
lineno?+=?line_incr
下面是對(duì)一段Python源代碼反編譯為byte?code的結(jié)果,這個(gè)結(jié)果也將作為下一章對(duì)Python執(zhí)行引擎的分析的開始:
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
再往前想一想,從現(xiàn)在到達(dá)的地方出發(fā),實(shí)際上我們就可以做出一個(gè)Python的執(zhí)行引擎了,哇,這是多么激動(dòng)人心的事啊。遙遠(yuǎn)的天空,一抹朝陽,緩緩升起了……
事實(shí)上,Python標(biāo)準(zhǔn)庫(kù)中提供了對(duì)python進(jìn)行反編譯的工具dis,利用這個(gè)工具,可以很容易地得到我們?cè)谶@里得到的結(jié)果,當(dāng)然,還要更詳細(xì)一些,圖8展示了利用dis工具對(duì)CodeObject.py進(jìn)行反編譯的結(jié)果:
在圖8顯示的結(jié)果中,最左面一列顯示的是CodeObject.py中源代碼的行數(shù),左起第二列顯示的是當(dāng)前的字節(jié)碼指令在co_code中的偏移位置。
在以后的分析中,我們大部分將采用dis工具的反編譯結(jié)果,在有些特殊情況下會(huì)使用我們自己的反編譯結(jié)果。
原文鏈接:http://blog.163.com/dailongquan@126/blog/static/52600902200812862955306/
總結(jié)
以上是生活随笔為你收集整理的python源码解读_Python源码剖析[16] —— Pyc文件解析的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: SQLServer LIKE 通配符
- 下一篇: Python源码剖析 电子书 配套资源