自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

虛擬機是怎么執(zhí)行字節(jié)碼的?背后都經歷了哪些過程

開發(fā) 前端
本篇文章我們從宏觀的角度介紹了虛擬機執(zhí)行字節(jié)碼的流程,說白了虛擬機就是把自己當成一個 CPU,在棧楨中執(zhí)行指令。通過遍歷字節(jié)碼指令集,基于不同的指令執(zhí)行不同的處理邏輯。

楔子

當解釋器啟動后,首先會進行運行時環(huán)境的初始化,注意這里的運行時環(huán)境,它和之前說的執(zhí)行環(huán)境是不同的概念。運行時環(huán)境是一個全局的概念,而執(zhí)行環(huán)境是一個棧幀。

關于運行時環(huán)境的初始化是一個很復雜的過程,涉及到 Python 進程、線程的創(chuàng)建,類型對象的完善等非常多的內容,我們暫時先不討論。這里就假設初始化動作已經完成,我們已經站在了虛擬機的門檻外面,只需要輕輕推動第一張骨牌,整個執(zhí)行過程就像多米諾骨牌一樣,一環(huán)扣一環(huán)地展開。

在介紹字節(jié)碼的時候我們說過,解釋器可以看成是:編譯器+虛擬機,編譯器負責將源代碼編譯成 PyCodeObject 對象,而虛擬機則負責執(zhí)行。整個過程如下:

圖片圖片

所以我們的重點就是虛擬機是怎么執(zhí)行 PyCodeObject 對象的?整個過程是什么,掌握了這些,你對虛擬機會有一個更深的理解。

虛擬機的運行框架

在介紹棧幀的時候我們說過,Python 是一門動態(tài)語言,一個變量指向什么對象需要在運行時才能確定,這些信息不可能靜態(tài)存儲在 PyCodeObject 對象中。

所以虛擬機在運行時會基于 PyCodeObject 對象動態(tài)創(chuàng)建出一個棧幀對象,然后在棧幀里面執(zhí)行字節(jié)碼。而創(chuàng)建棧幀,主要使用以下兩個函數(shù):

// Python/ceval.c

/* 基于 PyCodeObject、全局名字空間、局部名字空間,創(chuàng)建棧幀
 * 參數(shù)非常簡單,所以它一般適用于模塊這種參數(shù)不復雜的場景
 * 我們說模塊也會對應一個棧幀,并且它位于棧幀鏈的最頂層 
 */
PyObject *
PyEval_EvalCode(PyObject *co, PyObject *globals, PyObject *locals);

/* 相比 PyEval_EvalCode 多了很多的參數(shù)
 * 比如里面有位置參數(shù)以及個數(shù),關鍵字參數(shù)以及個數(shù)
 * 還有默認參數(shù)以及個數(shù),閉包等等,顯然它用于函數(shù)等復雜場景 
 * 注:此 API 已廢棄,內部不再使用
 */
PyObject *
PyEval_EvalCodeEx(PyObject *_co, PyObject *globals, PyObject *locals,
                  PyObject *const *args, int argcount,
                  PyObject *const *kws, int kwcount,
                  PyObject *const *defs, int defcount,
                  PyObject *kwdefs, PyObject *closure);

這兩個函數(shù)最終都會調用 _PyEval_Vector 函數(shù),這個函數(shù)內部的邏輯我們一會兒再看??傊坏瑢ο蟪跏蓟戤?,那么就要進行處理了,在棧幀中執(zhí)行字節(jié)碼(幀評估)。

關于在棧幀中執(zhí)行字節(jié)碼,虛擬機內部有兩個重要的 C 函數(shù)。

// Python/ceval.c
PyObject *
PyEval_EvalFrame(PyFrameObject *f)
{
    PyThreadState *tstate = _PyThreadState_GET();
    return _PyEval_EvalFrame(tstate, f->f_frame, 0);
}

PyObject *
PyEval_EvalFrameEx(PyFrameObject *f, int throwflag)
{
    PyThreadState *tstate = _PyThreadState_GET();
    return _PyEval_EvalFrame(tstate, f->f_frame, throwflag);
}

當然啦,上面這兩個函數(shù)屬于高層的封裝,最終都會調用 _PyEval_EvalFrame 函數(shù)。

// Include/internal/pycore_ceval.h
static inline PyObject*
_PyEval_EvalFrame(PyThreadState *tstate, 
                  struct _PyInterpreterFrame *frame, 
                  int throwflag)
{   
    EVAL_CALL_STAT_INC(EVAL_CALL_TOTAL);
    // 如果 tstate->interp->eval_frame 為 NULL
    // 會調用 _PyEval_EvalFrameDefault 函數(shù)執(zhí)行字節(jié)碼
    if (tstate->interp->eval_frame == NULL) {
        return _PyEval_EvalFrameDefault(tstate, frame, throwflag);
    }
    // 另外虛擬機也支持自定義,允許我們替換掉 _PyEval_EvalFrameDefault
    // 只需將具體實現(xiàn)賦值給 tstate->interp->eval_frame 即可
    // 因此這個做法就使得性能分析器、調試器等工具的實現(xiàn)成為可能
    return tstate->interp->eval_frame(tstate, frame, throwflag);
}

因此虛擬機最終會通過 _PyEval_EvalFrameDefault 函數(shù)來完成字節(jié)碼的執(zhí)行。

目前出現(xiàn)了好幾個函數(shù),我們用一張圖來描述一下它們的關系:

圖片圖片

當然啦,_PyEval_Vector 函數(shù)內部在拿到棧幀之后,其實會直接調用 _PyEval_EvalFrame。

// Python/ceval.c
PyObject *
_PyEval_Vector(PyThreadState *tstate, PyFunctionObject *func,
               PyObject *locals,
               PyObject* const* args, size_t argcount,
               PyObject *kwnames)
{
    // 解釋一下相關參數(shù)的含義
    /* tstate:線程狀態(tài)對象(在后續(xù)的章節(jié)剖析)
     * func:函數(shù)對象(在后續(xù)的章節(jié)剖析)
     * locals:局部名字空間
     * args:調用函數(shù)時傳遞的參數(shù)
     * argcount:通過位置參數(shù)傳遞的參數(shù)的個數(shù)
     * kwnames:通過關鍵字參數(shù)傳遞的參數(shù)的名稱

     舉個例子,比如調用是 func(1, 2, 3, c=4, d=5)
     那么 args 就是 (1, 2, 3, 4, 5)
     argcount 就是 3,因為有 3 個參數(shù)通過位置參數(shù)的方式傳遞
     kwname 則是 ("c", "d")
    */

    // 增加引用計數(shù)
    Py_INCREF(func);  
    Py_XINCREF(locals);
    // 給通過位置參數(shù)傳遞的參數(shù),增加引用計數(shù)
    for (size_t i = 0; i < argcount; i++) {
        Py_INCREF(args[i]);
    }
    // 給通過關鍵字參數(shù)傳遞的參數(shù),增加引用計數(shù)
    if (kwnames) {
        Py_ssize_t kwcount = PyTuple_GET_SIZE(kwnames);
        for (Py_ssize_t i = 0; i < kwcount; i++) {
            Py_INCREF(args[i+argcount]);
        }
    }
    // 為即將執(zhí)行的函數(shù)調用,創(chuàng)建一個新的 _PyInterpreterFrame 結構體實例
    // 然后推入虛擬機為其準備的 Stack 中
    _PyInterpreterFrame *frame = _PyEvalFramePushAndInit(
        tstate, func, locals, args, argcount, kwnames);
    if (frame == NULL) {
        return NULL;
    }
    // 計數(shù)器,統(tǒng)計 _PyEval_Vector 的調用次數(shù),用于跟蹤,我們不用關心
    EVAL_CALL_STAT_INC(EVAL_CALL_VECTOR);
    // 調用 _PyEval_EvalFrame 函數(shù)
    return _PyEval_EvalFrame(tstate, frame, 0);
}

然后在 _PyEval_EvalFrame 內部又會調用 _PyEval_EvalFrameDefault 函數(shù),整個過程非常好理解。

注:棧幀指的是 PyFrameObject,它是一個 Python 對象,而 _PyInterpreterFrame 是一個只包含棧幀核心信息的普通 C 結構體,前面我們介紹過的。 

但為了描述方便,后續(xù)在提到 _PyInterpreterFrame 時,我們也會稱它為棧幀,這一點大家心里清楚就好。

所以不難發(fā)現(xiàn) _PyEval_EvalFrameDefault 函數(shù)是虛擬機運行的核心,該函數(shù)較為復雜,我們會在下一篇文章中分析它的具體實現(xiàn)。至于本篇文章就先從宏觀的角度來描述一下虛擬機執(zhí)行字節(jié)碼的流程,并對之前的內容做一個補充,將背后涉及的概念闡述一遍,這樣后續(xù)看源碼的時候也會事半功倍。

首先棧幀中有一個 f_code 字段,它指向 PyCodeObject 對象,該對象的 co_code 字段則保存著字節(jié)碼指令序列。而虛擬機執(zhí)行字節(jié)碼就是從頭到尾遍歷整個 co_code,對指令逐條執(zhí)行的過程。

另外也不要覺得字節(jié)碼指令(簡稱指令)有多神秘,說白了它就是個 uint8 整數(shù),而一個程序肯定會包含多條指令,它們整體便構成了指令集,或者指令序列。那么顯然,使用 bytes 對象來表示指令序列再合適不過了,如果站在 C 的角度,則就是一個普普通通的字符數(shù)組,一條指令就是一個字符、或者說一個整數(shù)。

另外我們說指令序列里面包含的不僅僅是指令,還有指令參數(shù),因為每個指令都會帶一個參數(shù)。因此索引為 0 2 4 6 8 ··· 的整數(shù)表示指令,索引為 1 3 5 7 9 ··· 的整數(shù)表示指令參數(shù)。

我們用 Python 來演示一下:

code_string = """
a = 1
b = 2
c = a + b
"""
code_object = compile(code_string, "<file>", "exec")
# 查看常量池
print(code_object.co_consts)
"""
(1, 2, None)
"""
# 查看符號表
print(code_object.co_names)
"""
('a', 'b', 'c')
"""

這些都比較簡單,再來看一下反編譯的結果,直接 dis.dis(code_object) 即可。

/* 常量池:(1, 2, None)
 * 符號表:('a', 'b', 'c')
 */
 
// 第一列表示源代碼行號
// 第二列表示指令的偏移量
// 第三列表示指令,在 C 中它們都是宏,表示整數(shù)
// 第四列表示指令參數(shù)
// 第五列是 dis 模塊為了方便我們閱讀而補充的提示信息

0           0 RESUME                   0

// 指令:LOAD_CONST,指令參數(shù):0
// 表示從常量池中加載索引為 0 的常量,并壓入運行時棧(關于運行時棧,一會兒詳細說明)
// 索引為 0 的常量顯然是 1,而括號里面的提示信息顯示的也是 1
2           2 LOAD_CONST               0 (1)
// 指令:STORE_NAME,指令參數(shù):0
// 表示從符號表中加載索引為 0 的符號,顯然結果是 "a"
// 然后彈出運行時棧的棧頂元素,顯然是上一條指令壓入的 1
// 將 "a" 和 1 組成鍵值對,存儲在當前的名字空間中
// 到此 a = 1 這條語句便完成了,或者說完成了變量和值的綁定
            4 STORE_NAME               0 (a)

// 從常量池中加載索引為 1 的常量(結果是 2),并壓入運行時棧
3           6 LOAD_CONST               1 (2)
// 從符號表中加載索引為 1 的符號(結果是 "b")
// 然后從棧頂彈出元素 2,將 "b" 和 2 綁定起來
            8 STORE_NAME               1 (b)

// 加載符號表中索引為 0 的符號對應的值,并壓入運行時棧
4          10 LOAD_NAME                0 (a)
// 加載符號表中索引為 1 的符號對應的值,并壓入運行時棧
           12 LOAD_NAME                1 (b)
// 將運行時棧的兩個元素彈出,并執(zhí)行加法運算
// 將運算之后的結果(a + b)再壓入運行時棧           
           14 BINARY_OP                0 (+)
// 從符號表中加載索引為 2 的符號,結果是 "c"           
// 將運行時棧的棧頂元素彈出,這里是 a + b 的運算結果
// 然后進行綁定,完成 c = a + b 這條賦值語句              
           18 STORE_NAME               2 (c)
// 從常量池中加載索引為 2 的元素并返回,有一個隱式的 return None
           20 RETURN_CONST             2 (None)

這些指令的源碼實現(xiàn)后續(xù)都會說,但是不難發(fā)現(xiàn),程序的主干邏輯都體現(xiàn)在字節(jié)碼中,而依賴的信息則由其它字段來維護。所謂執(zhí)行源代碼,其實就是虛擬機執(zhí)行編譯之后的字節(jié)碼。通過遍歷 co_code,然后基于不同的指令,執(zhí)行不同的邏輯。

然后我們基于上面這些輸出信息,看看能否將字節(jié)碼指令集還原出來,當然在還原之前首先要知道這些指令代表的數(shù)值是多少。

圖片圖片

下面我們來進行還原。

"""
 0 RESUME                   0
 2 LOAD_CONST               0
 4 STORE_NAME               0
 6 LOAD_CONST               1
 8 STORE_NAME               1
10 LOAD_NAME                0
12 LOAD_NAME                1
14 BINARY_OP                0 (+)
18 STORE_NAME               2
20 RETURN_CONST             2
"""
CACHE = 0
STORE_NAME = 90
LOAD_CONST = 100
LOAD_NAME = 101
RETURN_CONST = 121
BINARY_OP = 122
RESUME = 151

codes = [
    RESUME, 0,

    # a = 1
    LOAD_CONST, 0,
    STORE_NAME, 0,

    # b = 2
    LOAD_CONST, 1,
    STORE_NAME, 1,

    # c = a + b
    LOAD_NAME, 0,  # 加載 a
    LOAD_NAME, 1,  # 加載 b
    BINARY_OP, 0,  # 計算 a + b
    # 從 dis 的輸出信息可以看到,索引為 16 的指令沒有顯示
    # 這是因為索引為 16 的指令和相應的指令參數(shù)都是 0
    # 也就是說,在將 c 和 a + b 綁定之前,插入了一條 CACHE 指令
    # 這條指令是做什么的,我們后續(xù)再探討
    CACHE, 0,
    STORE_NAME, 2,

    # 所有代碼塊都隱式地包含了一個 return None
    RETURN_CONST, 2
]

print(bytes(codes))
"""
b'\x97\x00d\x00Z\x00d\x01Z\x01e\x00e\x01z\x00\x00\x00Z\x02y\x02'
"""

那么字節(jié)碼是不是我們還原的這個樣子呢?來對比一下。

結果是一樣的,到此相信你對 Python 源代碼的執(zhí)行過程應該有更深的了解了,簡單來講,其實就是以下幾個步驟。

1)源代碼被編譯成 PyCodeObject 對象,該對象的 co_code 字段指向字節(jié)碼指令序列,它包含了程序執(zhí)行的主干邏輯,剩余字段則保存常量池、符號表等其它靜態(tài)信息。

2)虛擬機在 PyCodeObject 對象的基礎上構建棧楨對象。

3)虛擬機在棧楨對象內部執(zhí)行字節(jié)碼(幀評估),具體流程就是遍歷指令集和,根據(jù)不同指令執(zhí)行不同的處理邏輯,而這一過程便由 _PyEval_EvalFrameDefault 函數(shù)負責完成。

什么是運行時棧

之前一直提到一個概念,叫運行時棧,那什么是運行時棧呢?別急,我們先來回顧一下棧楨的基本結構。

圖片圖片

大部分字段都很好理解,因為之前通過 Python 代碼演示過。但有幾個字段是虛擬機用于執(zhí)行指令的,后續(xù)會遇到,所以這里再拿出來解釋一下。

prev_instr

指向上一條剛執(zhí)行完的字節(jié)碼指令,因為每個指令要帶一個參數(shù),所以該字段的類型為 uint16 *,前 8 字節(jié)表示指令,后 8 字節(jié)表示參數(shù)。假設虛擬機要執(zhí)行偏移量為 14 的指令,那么 prev_instr 便指向偏移量為 12 的指令。

localsplus

一個柔性數(shù)組,它的內存大小被分為 4 個部分。

注:localsplus 是一個數(shù)組,所以它是一段連續(xù)的內存,只不過按照用途被分成了 4 個部分。如果用新一團團長丁偉的說法:每個部分之間是雞犬相聞,但又老死不相往來。

然后再著重強調一下運行時棧,虛擬機在執(zhí)行字節(jié)碼指令時高度依賴它,因為一個指令只能帶一個參數(shù),那么剩余的參數(shù)就必須通過運行時棧給出。比如 a = 1 會對應兩條字節(jié)碼:LOAD_CONST 和 STORE_NAME。

STORE_NAME 的作用是從符號表中獲取符號,或者說變量名,然后和值綁定起來。而要加載符號,那么必須要知道它在符號表中的索引,顯然這可以通過指令參數(shù)給出,但問題是與之綁定的值怎么獲取?

毫無疑問,要通過運行時棧,因此 LOAD_CONST 將值讀取進來之后,還要壓入運行時棧。然后 STORE_NAME 會將值從運行時棧中彈出,從而完成符號(變量)和值的綁定。關于運行時棧,我們再看個復雜的例子:

圖片圖片

偏移量為 8 的指令表示要構建一個字典,指令參數(shù) 2 表示構建的字典的長度為 2,但問題是字典的鍵值對在什么地方?顯然它們已經被提前壓入了運行時棧,執(zhí)行 BUILD_CONST_KEY_MAP 的時候直接彈出即可。

所以這就是運行時棧的作用,如果某個指令需要 n 個參數(shù),那么其中的 n - 1 個必須要提前壓入運行時棧,然后在該指令執(zhí)行的時候依次彈出,因為一個指令只能帶一個參數(shù)。

stacktop

運行時棧的棧頂相對于 localsplus 數(shù)組的偏移量,那么顯然 localsplus 加上 stacktop 便指向運行時棧的棧頂。

我們通過源碼驗證一下:

// Inlcude/internal/pycore_frame.h

// 獲取 localsplus 字段
static inline PyObject**
_PyFrame_GetLocalsArray(_PyInterpreterFrame *frame)
{
    return frame->localsplus;
}

// 獲取指針(PyObject **),它指向運行時棧的棧頂,我們稱之為 stack_pointer
// 而元素的入棧和出棧,顯然都是通過操作 stack_pointer 完成的
// 執(zhí)行 *stack_pointer++ = v,一個元素就入棧了
// 執(zhí)行 v = *--stack_pointer,一個元素就出棧了
static inline PyObject**
_PyFrame_GetStackPointer(_PyInterpreterFrame *frame)
{
    // localsplus + stacktop 便指向運行時棧的棧頂
    // 因為 stacktop 的含義就是棧頂相對于 localsplus 數(shù)組(首元素的地址)的偏移量
    PyObject **sp = frame->localsplus + frame->stacktop;
    frame->stacktop = -1;
    return sp;
}

// 隨著元素的入棧和出棧,運行時棧的棧頂、或者說 stack_pointer 也在不斷變化
// 而 frame->stacktop 表示棧頂相對于 localsplus 的偏移量
// 那么顯然,由于棧頂發(fā)生變化,后續(xù)還要對 frame->stacktop 進行更新
static inline void
_PyFrame_SetStackPointer(_PyInterpreterFrame *frame, PyObject **stack_pointer)
{
    frame->stacktop = (int)(stack_pointer - frame->localsplus);
}

這就是 stacktop 字段的作用,用于獲取運行時棧的棧頂指針。另外我們說 localsplus 數(shù)組被分成了 4 份,最后一份給了運行時棧,因此雖然我們稱之為棧,但它其實就是一個數(shù)組,而且還是數(shù)組的一部分。

而對于數(shù)組而言,內存地址從左往右是增大的。

這是 localsplus 的示意圖,我們只看運行時棧,目前棧里面有兩個元素,stack_pointer 指向棧頂。

這時如果要添加一個元素 3,那么直接 *stack_pointer++ = 3 即可。

如果要將棧頂元素彈出,那么執(zhí)行 v = *--stack_pointer 即可。

圖片圖片

而 stack_pointer - localsplus 便是 stacktop。

運行時棧的一些 API

相信你對運行時棧的了解已經足夠深了,說白了它就是參數(shù)的容身之所,比如虛擬機在執(zhí)行 a + b 的時候,通過指令參數(shù)可以判斷這是一個加法操作。但在執(zhí)行加法的時候,加號兩邊的值要怎么獲取呢?這時候就需要一個棧來專門保存相應的參數(shù)。

在執(zhí)行加法之前,先將 a 和 b 壓入棧中,等執(zhí)行加法的時候,再將 a 和 b 從棧里面彈出來即可,非常簡單。

然后再來看看運行時棧相關的一些 API。

圖片圖片

API 非常多,它們都定義在 Python/ceval_macros.h 中,我們來逐一介紹。假設目前運行時棧內部有三個元素,從棧底到棧頂分別是整數(shù) 1、2、3,那么運行時棧的結構就是下面這樣。

圖片圖片

localsplus 數(shù)組被分成 4 個區(qū)域,運行時棧占據(jù)最后一個,因此圖中的灰色區(qū)域便是運行時棧之前的內存。由于我們是研究運行時棧,所以這部分區(qū)域后續(xù)就不再畫了。

然后看一下這些和運行時棧相關的 API 都是干嘛的。

STACK_LEVEL()

返回運行時棧的元素個數(shù),那么問題來了,通過 stack_pointer 可以獲取棧頂指針,但棧底指針怎么獲取呢?畢竟有了棧底指針,兩者相減就完事了。

#define STACK_LEVEL()  ((int)(stack_pointer - _PyFrame_Stackbase(frame)))

顯然 _PyFrame_Stackbase 負責獲取運行時棧的棧底(基址),看一下它的具體實現(xiàn)。

// Include/internal/pycore_frame.h

// PyCodeObject 對象里面有以下幾個字段
// co_nlocals:代碼塊中局部變量的個數(shù),也包括參數(shù)
// co_ncellvars:cell 變量的個數(shù),即 co_cellvars 的長度
// co_nfreevars:free 變量的個數(shù),即 co_freevars 的長度
// co_nlocalsplus:局部變量、cell 變量、free 變量的個數(shù)之和
static inline PyObject **_PyFrame_Stackbase(_PyInterpreterFrame *f) {
    // 由于 co_nlocalsplus = co_nlocals + co_ncellvars + co_nfreevars
    // 那么顯然,f->localsplus + f->f_code->co_nlocalsplus
    // 便指向運行時棧的棧底,或者說運行時棧對應的內存區(qū)域的首元素
    return f->localsplus + f->f_code->co_nlocalsplus;
}

在之前的版本中(比如 3.8),棧楨內部會有一個 f_valuestack 字段,專門負責指向運行時棧的棧底。注:STACK_LEVEL() 是動態(tài)變化的,因為 stack_pointer 會動態(tài)變化。

STACK_SIZE()

運行時棧最多能存儲多少個元素,或者說運行時棧的長度。

#define STACK_SIZE()      (frame->f_code->co_stacksize)

我們看到它返回了 PyCodeObject 的 co_stacksize 字段,之前介紹 PyCodeObject 對象時,我們說 co_stacksize 表示執(zhí)行代碼塊所需要的??臻g。這個描述其實會讓人感到困惑,不過當了解完運行時棧之后就清晰了,其實它表示運行時棧最多能存儲多少個元素,或者說運行時棧的長度。也可以理解為要想執(zhí)行這段代碼塊,后續(xù)創(chuàng)建棧楨時,應該給 localsplus 數(shù)組中表示運行時棧的區(qū)域分配能存儲多少個元素的內存。

比如 co_stacksize 是 1,那么表示應該給運行時棧分配能存儲 1 個元素的內存,即運行時棧的長度為 1。我們通過反編譯的方式,實際演示一下。

import dis

def some_func():
    a = 1
    b = 2
    c = 3

# 這里我們只保留字節(jié)碼指令
dis.dis(some_func)
"""
RESUME
LOAD_CONST    # 將元素壓入運行時棧,棧里的元素個數(shù)為 1
STORE_FAST    # 將元素從棧頂彈出,棧里的元素個數(shù)為 0
LOAD_CONST    # 將元素壓入運行時棧,棧里的元素個數(shù)為 1
STORE_FAST    # 將元素從棧頂彈出,棧里的元素個數(shù)為 0 
LOAD_CONST    # 將元素壓入運行時棧,棧里的元素個數(shù)為 1 
STORE_FAST    # 將元素從棧頂彈出,棧里的元素個數(shù)為 0 
RETURN_CONST
"""
# 也就是說,運行時棧只要能容納一個元素,即可執(zhí)行這段代碼
print(some_func.__code__.co_stacksize)  # 1

我們再來看個例子。

import dis

def some_func():
    a = 1
    b = 2
    c = 3
    lst = [a, b, c]

dis.dis(some_func)
"""
RESUME       
LOAD_CONST    # 將元素壓入運行時棧,棧里的元素個數(shù)為 1     
STORE_FAST    # 將元素從棧頂彈出,棧里的元素個數(shù)為 0  
LOAD_CONST    # 將元素壓入運行時棧,棧里的元素個數(shù)為 1   
STORE_FAST    # 將元素從棧頂彈出,棧里的元素個數(shù)為 0   
LOAD_CONST    # 將元素壓入運行時棧,棧里的元素個數(shù)為 1   
STORE_FAST    # 將元素從棧頂彈出,棧里的元素個數(shù)為 0    

LOAD_FAST     # 將元素壓入運行時棧,棧里的元素個數(shù)為 1 
LOAD_FAST     # 將元素壓入運行時棧,棧里的元素個數(shù)為 2   
LOAD_FAST     # 將元素壓入運行時棧,棧里的元素個數(shù)為 3   
BUILD_LIST    # 將棧里的三個元素彈出,構建列表并入棧(此時元素個數(shù)為 1)
STORE_FAST    # 將元素從棧頂彈出,棧里的元素個數(shù)為 0      
RETURN_CONST        
"""
# 不難看出,要想執(zhí)行這段代碼,運行時棧要能容納 3 個元素
print(some_func.__code__.co_stacksize)  # 3

相信你現(xiàn)在應該理解 co_stacksize 的作用了,它表示運行時棧最多能容納多少個元素,也就是運行時棧的長度。以上面代碼為例,由于最多會壓入 3 個元素,所以運行時棧的長度就是 3,即最多能容納 3 個元素。并且這個長度在編譯之后就已經確定了,因為可以通過遍歷指令集靜態(tài)計算出來。

我們畫一張圖描述一下執(zhí)行上面的代碼時,運行時棧的變化過程。

圖片圖片

整個過程應該很清晰,當然上面只是運行時棧的變化,localsplus 中存儲局部變量的內存區(qū)域也在變化。另外如果代碼塊位于全局作用域,那么變化的就是全局名字空間。

EMPTY()

#define EMPTY()           (STACK_LEVEL() == 0)

判斷運行時棧是否為空,顯然只需判斷運行時棧的元素個數(shù)是否為 0 即可。

TOP()

#define TOP()             (stack_pointer[-1])

查看當前運行時棧的棧頂元素。

SECOND()

#define SECOND()       (stack_pointer[-2])

查看從棧頂元素開始的第二個元素。

THIRD()

#define THIRD()        (stack_pointer[-3])

查看從棧頂元素開始的第三個元素。

FOURTH()

#define FOURTH()      (stack_pointer[-4])

查看從棧頂元素開始的第四個元素。

PEEK(n):

#define PEEK(n)       (stack_pointer[-(n)])

查看從棧頂元素開始的第 n 個元素。

SET_TOP(v)

#define SET_TOP(v)    (stack_pointer[-1] = (v))

將當前運行時棧的棧頂元素設置成 v。

SET_SECOND(v)

#define SET_SECOND(v)    (stack_pointer[-2] = (v))

將從棧頂元素開始的第二個元素設置成 v。

POKE(n)

#define POKE(n, v)        (stack_pointer[-(n)] = (v))

將從棧頂元素開始的第 n 個元素設置成 v。

PUSH(v)

#define PUSH(v)           BASIC_PUSH(v)
#define BASIC_PUSH(v)     (*stack_pointer++ = (v))

往運行時棧中壓入一個元素,并且壓入之后,棧中已存儲的元素個數(shù)一定不超過 co_stacksize。換句話說,STACK_LEVEL() 永遠小于等于 STACK_SIZE()。

然后入棧和出棧,都是通過操作 stack_pointer 完成的。假設當前棧里有一個元素 1,然后添加一個元素 2。

圖片圖片

Python 的變量都是一個指針,所以 stack_pointer 是一個二級指針,它永遠指向棧頂位置,只不過棧頂位置會變。

注:運行時棧的內存一開始就申請好了,初始狀態(tài)下,里面的元素全部為 NULL。而往棧里面壓入一個元素,其實就是修改 stack_pointer 指向的內存單元,然后執(zhí)行 stack_pointer++。

POP()

#define POP()             BASIC_POP()
#define BASIC_POP()       (*--stack_pointer)

彈出棧頂元素,注意它和 TOP 的區(qū)別,TOP 是返回棧頂元素,但不彈出。

圖片圖片

stack_pointer 指向棧頂位置,所以它向棧底移動一個位置,就相當于元素被彈出了。

關于彈出元素需要做一個說明,所謂彈出元素本質上就是將 stack_pointer 向棧底移動一個位置。我們看一下上圖,一開始棧里面有兩個元素,分別是整數(shù) 1 和整數(shù) 2,當然準確來說應該是指向它們的指針,但為了描述方便,我們就用對象本身代替了。

然后執(zhí)行 POP(),將整數(shù) 2 彈出,但我們發(fā)現(xiàn) POP() 之后,整數(shù) 2 還在棧里面。關于這一點很好理解,因為 stack_pointer 始終指向棧頂位置,而它向棧底移動了一個位置,那么整數(shù) 2 就已經不是棧頂元素了。當下一個元素入棧時,會把整數(shù) 2 替換掉。因此雖然整數(shù) 2 還在運行時棧里面,但和不在沒有任何區(qū)別,此時我們依舊認為整數(shù) 2 被彈出了。

不過在后續(xù)的文章中,在畫運行時棧的時候,我們會這么畫。

圖片圖片

為了閱讀清晰,stack_pointer 之后的元素就不寫了。另外還要注意一點,運行時棧的內存一開始就已經申請好了,是固定的,彈出元素只是改變棧頂指針 stack_pointer 的指向,內存區(qū)域的大小不變。

當然這些內容都比較簡單,但為了避免出現(xiàn)歧義,這里單獨解釋一下。

STACK_GROW(n)

#define STACK_GROW(n)          BASIC_STACKADJ(n)
#define BASIC_STACKADJ(n)      (stack_pointer += n)

改變運行時棧的棧頂,注:運行時棧的大小是固定的,但棧頂是由 stack_pointer 決定的。

圖片圖片

那么問題來了,假設要往運行時棧壓入兩個元素,分別是 2、3,該怎么做呢?首先肯定可以通過 PUSH 實現(xiàn)。

PUSH(2);
PUSH(3);

但如果不讓你用 PUSH,該怎么做呢?

STACK_GROW(2);
// 設置元素
POKE(1, 3);  // stack_pointer[-1] = 3
POKE(2, 2);  // stack_pointer[-2] = 2

兩種做法都是可以的。

STACK_SHRINK(n)

#define STACK_SHRINK(n)        BASIC_STACKADJ(-(n))
#define BASIC_STACKADJ(n)      (stack_pointer += n)

它的作用和 STACK_GROWN 正好相反。

圖片圖片

以上就是運行時棧的一些 API,后續(xù)再看源碼的時候,會經常遇到。

小結

本篇文章我們從宏觀的角度介紹了虛擬機執(zhí)行字節(jié)碼的流程,說白了虛擬機就是把自己當成一個 CPU,在棧楨中執(zhí)行指令。通過遍歷字節(jié)碼指令集,基于不同的指令執(zhí)行不同的處理邏輯。

然后是運行時棧,因為一個指令只能帶一個參數(shù),那么剩余的參數(shù)就需要通過運行時棧給出。比如 LOAD_CONST 指令,它在加載完常量之后,會將常量壓入運行時棧,然后 STORE_NAME 或 STORE_FAST 指令再將常量從運行時棧的頂部彈出,和某個符號(變量)綁定起來。另外關于這些指令,我們后面會詳細說。

最后我們介紹了運行時棧的一些 API,或者說宏,因為執(zhí)行指令的時候會反復操作運行時棧,所以底層封裝了很多的宏。

責任編輯:武曉燕 來源: 古明地覺的編程教室
相關推薦

2024-11-15 16:27:58

函數(shù)結構存儲

2018-04-04 15:05:17

虛擬機字節(jié)碼引擎

2024-05-21 12:51:06

Python對象PyObject

2024-05-22 13:04:46

Python對象關系

2024-08-29 12:37:11

2017-11-14 16:43:13

Java虛擬機線程

2023-09-03 17:05:20

虛擬機

2023-01-09 18:30:53

架構JVM

2023-09-02 21:35:39

Linux虛擬機

2010-06-18 10:13:17

虛擬機消失

2010-02-24 10:39:28

Python虛擬機

2013-09-17 10:35:17

Python執(zhí)行原理

2024-11-11 11:21:30

虛擬機Python跳轉表

2021-05-28 23:04:23

Python利器執(zhí)行

2012-08-16 09:07:57

Erlang

2010-10-13 10:21:37

物理機虛擬機遷移

2010-09-25 15:13:40

JVMJava虛擬機

2014-12-18 09:41:44

虛擬化遷移

2016-08-24 08:47:19

kali linuxvirtualbox虛擬機

2009-08-10 10:49:27

Linux下安裝VMw
點贊
收藏

51CTO技術棧公眾號