1. 程式人生 > 實用技巧 >Python原始碼剖析——02虛擬機器

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指示棧頂。