Python原始碼剖析——02虛擬機器
《Python原始碼剖析》筆記
第七章:編譯結果
1、大概過程
執行一個Python程式會經歷以下幾個步驟:
- 由直譯器對原始檔(.py)進行編譯,得到位元組碼(.pyc檔案)
- 然後由虛擬機器按照位元組碼一條一條執行對應的指令
2、PyCodeObject
程式執行時,Python會將編譯結果都存放在記憶體中的PyCodeObject物件中。每一個名字空間都對應著一個PyCodeObject物件。
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) See Objects/lnotab_notes.txt for details. */ void *co_zombieframe; /* for optimization only (see frameobject.c) */ PyObject *co_weakreflist; /* to support weakrefs to code objects */ } PyCodeObject;
可以看到裡面定義了一大堆東西,其中位元組碼指令序列儲存在co_code。
3、Pyc檔案
Pyc檔案是PyCodeObject物件的二進位制檔案形式,但是生成pyc檔案的時候,除了PyCodeObject物件還有其他資料。
值得注意的是,並不是所有的PyCodeObject物件都會被儲存為pyc檔案,如果只是簡單的使用python demo.py
並不會產生一個demo.pyc,因為這是沒有必要的。想得到pyc檔案由很多方法,其中import操作是最常見的觸發機制。
檔案建立:
在寫入pyc時,會先寫入magic number和時間資訊。magic number的作用是保證相容性(位元組碼指令變化等原因),可相容版本的magic number會被設定為相同,那麼呼叫時通過檢視magic number就可以知道程式是否相容。時間資訊是用來保證pyc的有效性,可以讓原始檔和pyc隨時保持同步。
Python在把物件寫進pyc時,所有資料都是位元組流,顯然應該有一種機制去區分資料,所以Python在寫入一個物件之前,會先寫入TYPE_LIST、TYPE_CODE、TYPE_INT等等這樣的標識,這樣在讀取的時候就能獲得物件的型別和起止位置。
寫入一個物件的函式為w_object,其實現非常暴力,就是不斷的if...else if...判斷物件型別,然後遍歷物件內部,然後依次寫入。所以這裡簡單的認為Python中的諸多型別可由整型和字串構成,其寫入方法分別為w_long、w_string。
在寫入整型時,直接簡單的一個位元組一個位元組寫入即可。
[Objects/marshal.c]
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);
}
在寫入字串時稍微複雜一點。
typedef struct { FILE *fp; PyObject *strings; /* dict on marshal, list on unmarshal */ } WFILE;
這裡簡化了WFILE的定義,只看這兩個。
對於字串來說,其實現了intern機制。那麼我們在將字串物件寫入pyc時,如果只是單純的寫入一個字串然後標記是否需要intern,那麼可想而知,pyc檔案中可能會出現大量的冗餘字串,而這些其實是不必要的。所以,Python實現了一個strings指標,在寫入pyc時,strings會指向一個PyDictObject物件,這個物件實際維護著(PyStringObject,PyIntObject)鍵值對,以表示某個字串插入intern的順序(序號)。這樣每次寫入一個需要intern的字串就先標記TYPE_INTERNED,然後寫入字串;寫入一個重複的字串時,只需要標識TYPE_STRINGREF,再寫入對應序號即可。但是PyDictObject不能按索引隨機查詢,寫入序號有什麼用?那麼其實在讀Pyc的時候,strings不再是一個PyDictObject,Python會把他初始化為PyListObject,然後在遇到TYPE_INTERNED時append到List後面,這樣字串就能按順序插入到List中,讀到TYPE_STRINGREF時即可按序號獲取物件。
第八章:虛擬機器框架
1、PyFrameObject
在C中,不同執行環境中對相同的變數名可以有不同的解釋,這是因為C能夠區分執行環境。但是根據上面所提到的PyCodeObject,顯然他只能提供某個作用域中的靜態資訊,並不能提供執行環境,所以虛擬機器執行的並不是PyCodeObject物件,而是一個PyFrameObject物件,它可以模擬x86平臺上的棧幀的效果。
[Objects/frameobject.h]
typedef struct _frame {
PyObject_VAR_HEAD
struct _frame *f_back; /* previous frame, or NULL */
PyCodeObject *f_code; /* code segment */
PyObject *f_builtins; /* builtin symbol table (PyDictObject) */
PyObject *f_globals; /* global symbol table (PyDictObject) */
PyObject *f_locals; /* local symbol table (any mapping) */
PyObject **f_valuestack; /* points after the last local */
/* Next free slot in f_valuestack. Frame creation sets to f_valuestack. Frame evaluation usually NULLs it, but a frame that yields sets it to the current stack top. */
PyObject **f_stacktop;
PyObject *f_trace; /* Trace function */
/* If an exception is raised in this frame, the next three are used to * record the exception info (if any) originally in the thread state. See * comments before set_exc_info() -- it's not obvious. * Invariant: if _type is NULL, then so are _value and _traceback. * Desired invariant: all three are NULL, or all three are non-NULL. That * one isn't currently true, but "should be". */
PyObject *f_exc_type, *f_exc_value, *f_exc_traceback;
PyThreadState *f_tstate;
int f_lasti; /* Last instruction if called */
/* Call PyFrame_GetLineNumber() instead of reading this field directly. As of 2.3 f_lineno is only valid when tracing is active (i.e. when f_trace is set). At other times we use PyCode_Addr2Line to calculate the line from the current bytecode index. */
int f_lineno; /* Current line number */
int f_iblock; /* index in f_blockstack */
PyTryBlock f_blockstack[CO_MAXBLOCKS]; /* for try and loop blocks */
PyObject *f_localsplus[1]; /* locals+stack, dynamically sized */
} PyFrameObject;
PyFrameObject類似於x86的棧幀,其定義如上。
f_back可以指向上一個PyFrameObject物件的位置,以此形成了一條執行環境鏈。
f_code存放PyCodeObject物件。
f_localsplus是由PyFrameObject維護的一個動態記憶體,主要給變數和棧使用。f_localsplus會先將一些需要使用的變數放進動態記憶體,剩餘部分給棧使用。由f_valuestack指示棧底,f_stacktop指示棧頂。