Python源碼剖析--Pyc文件解析

1.      PyCodeObjectPyc文件

一般認爲,Python是一種解釋性的語言,可是這種說法是不正確的,實際上,Python在執行時,首先會將.py文件中的源代碼編譯成Pythonbyte code(字節碼),而後再由Python Virtual Machine來執行這些編譯好的byte code。這種機制的基本思想跟Java.NET是一致的。然而,Python Virtual MachineJava.NETVirtual Machine不一樣的是,PythonVirtual Machine是一種更高級的Virtual Machine。這裏的高級並非一般意義上的高級,不是說PythonVirtual MachineJava.NET的功能更強大,更拽,而是說和Java.NET相比,PythonVirtual Machine距離真實機器的距離更遠。或者能夠這麼說,PythonVirtual Machine是一種抽象層次更高的Virtual Machinehtml

       咱們來考慮下面的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源代碼進行編譯的時候,對於一段CodeCode Block),會建立一個PyCodeObject與這段Code對應。那麼如何肯定多少代碼算是一個Code Block呢,事實上,當進入新的做用域時,就開始了新的一段Code。也就是說,對於下面的這一段Python源代碼:

[CodeObject.py]

class A:

    pass

 

def Fun():

    pass

 

a = A()

Fun()

 

Python編譯完成後,一共會建立3PyCodeObject對象,一個是對應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文件的方法,其實很簡單,就是利用Pythonimport機制。

Python運行的過程當中,若是碰到import abc,這樣的語句,那麼Python將到設定好的path中尋找abc.pycabc.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 codesource code的對應信息是以unsigned bytes的數組形式存在的,數組的形式能夠看做(byte codeco_code中位置增量,代碼行數增量)形式的一個list。好比對於下面的例子:

Byte codeco_code中的偏移

.py文件中源代碼的行數

0

1

6

2

50

7

這裏有一個小小的技巧,Python不會直接記錄這些信息,相反,它會記錄這些信息間的增量值,因此,對應的co_lnotab就應該是01 61 445

2.      Pyc文件的生成

前面咱們提到,Pythonimport時,若是沒有找到相應的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.4Python1.5MAGIC設爲不一樣的值就能夠了。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的邏輯很是簡單,就是對應不一樣的對象,好比stringintlist等,會有不一樣的寫的動做,然而其最終目的都是經過最基本的w_longw_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_LISTTYPE_CODETYPE_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操做的字符串時。能夠看到,WFILEstrings域其實是一個從string映射到int的一個PyDictObject對象。這個int值是什麼呢,這個int值是表示對應的string是第幾個被加入到WFILE.strings中的字符串。

這個int值看上去彷佛沒有必要,記錄一個string被加入到WFILE.strings中的序號有什麼意義呢?好,讓咱們來考慮下面的情形:

假設咱們須要向pyc文件中寫入三個string」Jython」, 「Ruby」, 「Jython」,並且這三個string都須要被進行INTERN操做。對於前兩個string,沒有任何問題,閉着眼睛寫入就是了。完成了前兩個string的寫入後,WFILE.stringspyc文件的狀況如圖2所示:

 

在寫入第三個字符串的時候,麻煩來了。對於這個「Jython」,咱們應該怎麼處理呢?
是按照上兩個string同樣嗎?若是這樣的話,那麼寫入後,WFILE.stringspyc的狀況如圖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所示:

在加載緊接着的(R0)時,由於解析到是一個TYPE_STRINGREF標誌,因此直接以標誌後面的數值0位索引訪問WFILE.strings,馬上可獲得字符串「Jython」。

3.      一個PyCodeObject,多個PyCodeObject

到了這裏,關於PyCodeObjectpyc文件,咱們只剩下最後一個有趣的話題了。還記得前面那個test.py嗎?咱們說那段簡單的什麼都作不了的python代碼就要產生三個PyCodeObject。而在write_compiled_module中咱們又親眼看到,Python運行環境只會對一個PyCodeObject對象調用PyMarshal_WriteObjectToFile操做。剎那間,咱們居然看到了兩個遺失的PyCodeObject對象。

Python顯然不會犯這樣低級的錯誤,想象一下,若是你是Guido,這個問題該如何解決?首先咱們會假想,有兩個PyCodeObject對象必定是包含在另外一個PyCodeObject中的。沒錯,確實如此,還記得咱們最開始指出的Python是如何肯定一個Code Block的嗎?對嘍,就是做用域。仔細看一下test.py,你會發現做用域呈現出一種嵌套的結構,這種結構也正是PyCodeObject對象之間的結構。因此到如今清楚了,與FunA對應得PyCodeObject對象必定是包含在與全局做用域對應的PyCodeObject對象中的,而PyCodeObject結構中的co_consts域正是這兩個PyCodeObject對象的藏身之處,如圖6所示:

在對一個PyCodeObject對象進行寫入到pyc文件的操做時,若是碰到它包含的另外一個PyCodeObject對象,那麼就會遞歸地執行寫入PyCodeObject對象的操做。如此下去,最終全部的PyCodeObject對象都會被寫入到pyc文件中去。並且pyc文件中的PyCodeObject對象也是以一種嵌套的關係聯繫在一塊兒的。

4.      Python字節碼

Python源代碼在執行前會被編譯爲Pythonbyte codePython的執行引擎就是根據這些byte code來進行一系列的操做,從而完成對Python程序的執行。在Python2.4.1中,一共定義了103byte 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.1byte code的編碼並無按順序增加,好比編碼爲5ROT_FOUR以後就是編碼爲9NOP。這多是歷史遺留下來的,你知道,在我們這行,歷史問題不是什麼好東西,搞得如今還有許多人不得不很鬱悶地面對MFC :)

Python143byte code中,有一部分是須要參數的,另外一部分是沒有參數的。全部須要參數的byte code的編碼都大於或等於90Python中提供了專門的宏來判斷一條byte code是否須要參數:

[opcode.h]

#define HAS_ARG(op) ((op) >= HAVE_ARGUMENT)

 

好了,到了如今,關於PyCodeObjectpyc文件的一切咱們都已瞭如指掌了,關於Python的如今咱們能夠作一些很是有趣的事了。呃,在我看來,最有趣的事莫過於本身寫一個pyc文件的解析器。沒錯,利用咱們如今所知道的一切,咱們真的能夠這麼作了。圖7展示的是對本章前面的那個test.py的解析結果:

 

 

更進一步,咱們還能夠解析byte code。前面咱們已經知道,Python在生成pyc文件時,會將PyCodeObject對象中的byte code也寫入到pyc文件中,並且這個pyc文件中還記錄了每一條byte codePython源代碼的對應關係,嗯,就是那個co_lnotab啦。假如如今咱們知道了byte codeco_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工具的反編譯結果,在有些特殊狀況下會使用咱們本身的反編譯結果。

相關文章
相關標籤/搜索