偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

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

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

楔子

當(dāng)解釋器啟動(dòng)后,首先會(huì)進(jìn)行運(yùn)行時(shí)環(huán)境的初始化,注意這里的運(yùn)行時(shí)環(huán)境,它和之前說(shuō)的執(zhí)行環(huán)境是不同的概念。運(yùn)行時(shí)環(huán)境是一個(gè)全局的概念,而執(zhí)行環(huán)境是一個(gè)棧幀。

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

在介紹字節(jié)碼的時(shí)候我們說(shuō)過(guò),解釋器可以看成是:編譯器+虛擬機(jī),編譯器負(fù)責(zé)將源代碼編譯成 PyCodeObject 對(duì)象,而虛擬機(jī)則負(fù)責(zé)執(zhí)行。整個(gè)過(guò)程如下:

圖片圖片

所以我們的重點(diǎn)就是虛擬機(jī)是怎么執(zhí)行 PyCodeObject 對(duì)象的?整個(gè)過(guò)程是什么,掌握了這些,你對(duì)虛擬機(jī)會(huì)有一個(gè)更深的理解。

虛擬機(jī)的運(yùn)行框架

在介紹棧幀的時(shí)候我們說(shuō)過(guò),Python 是一門(mén)動(dòng)態(tài)語(yǔ)言,一個(gè)變量指向什么對(duì)象需要在運(yùn)行時(shí)才能確定,這些信息不可能靜態(tài)存儲(chǔ)在 PyCodeObject 對(duì)象中。

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

// Python/ceval.c

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

/* 相比 PyEval_EvalCode 多了很多的參數(shù)
 * 比如里面有位置參數(shù)以及個(gè)數(shù),關(guān)鍵字參數(shù)以及個(gè)數(shù)
 * 還有默認(rèn)參數(shù)以及個(gè)數(shù),閉包等等,顯然它用于函數(shù)等復(fù)雜場(chǎng)景 
 * 注:此 API 已廢棄,內(nèi)部不再使用
 */
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);

這兩個(gè)函數(shù)最終都會(huì)調(diào)用 _PyEval_Vector 函數(shù),這個(gè)函數(shù)內(nèi)部的邏輯我們一會(huì)兒再看??傊坏瑢?duì)象初始化完畢,那么就要進(jìn)行處理了,在棧幀中執(zhí)行字節(jié)碼(幀評(píng)估)。

關(guān)于在棧幀中執(zhí)行字節(jié)碼,虛擬機(jī)內(nèi)部有兩個(gè)重要的 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);
}

當(dāng)然啦,上面這兩個(gè)函數(shù)屬于高層的封裝,最終都會(huì)調(diào)用 _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
    // 會(huì)調(diào)用 _PyEval_EvalFrameDefault 函數(shù)執(zhí)行字節(jié)碼
    if (tstate->interp->eval_frame == NULL) {
        return _PyEval_EvalFrameDefault(tstate, frame, throwflag);
    }
    // 另外虛擬機(jī)也支持自定義,允許我們替換掉 _PyEval_EvalFrameDefault
    // 只需將具體實(shí)現(xiàn)賦值給 tstate->interp->eval_frame 即可
    // 因此這個(gè)做法就使得性能分析器、調(diào)試器等工具的實(shí)現(xiàn)成為可能
    return tstate->interp->eval_frame(tstate, frame, throwflag);
}

因此虛擬機(jī)最終會(huì)通過(guò) _PyEval_EvalFrameDefault 函數(shù)來(lái)完成字節(jié)碼的執(zhí)行。

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

圖片圖片

當(dāng)然啦,_PyEval_Vector 函數(shù)內(nèi)部在拿到棧幀之后,其實(shí)會(huì)直接調(diào)用 _PyEval_EvalFrame。

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

     舉個(gè)例子,比如調(diào)用是 func(1, 2, 3, c=4, d=5)
     那么 args 就是 (1, 2, 3, 4, 5)
     argcount 就是 3,因?yàn)橛?3 個(gè)參數(shù)通過(guò)位置參數(shù)的方式傳遞
     kwname 則是 ("c", "d")
    */

    // 增加引用計(jì)數(shù)
    Py_INCREF(func);  
    Py_XINCREF(locals);
    // 給通過(guò)位置參數(shù)傳遞的參數(shù),增加引用計(jì)數(shù)
    for (size_t i = 0; i < argcount; i++) {
        Py_INCREF(args[i]);
    }
    // 給通過(guò)關(guān)鍵字參數(shù)傳遞的參數(shù),增加引用計(jì)數(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ù)調(diào)用,創(chuàng)建一個(gè)新的 _PyInterpreterFrame 結(jié)構(gòu)體實(shí)例
    // 然后推入虛擬機(jī)為其準(zhǔn)備的 Stack 中
    _PyInterpreterFrame *frame = _PyEvalFramePushAndInit(
        tstate, func, locals, args, argcount, kwnames);
    if (frame == NULL) {
        return NULL;
    }
    // 計(jì)數(shù)器,統(tǒng)計(jì) _PyEval_Vector 的調(diào)用次數(shù),用于跟蹤,我們不用關(guān)心
    EVAL_CALL_STAT_INC(EVAL_CALL_VECTOR);
    // 調(diào)用 _PyEval_EvalFrame 函數(shù)
    return _PyEval_EvalFrame(tstate, frame, 0);
}

然后在 _PyEval_EvalFrame 內(nèi)部又會(huì)調(diào)用 _PyEval_EvalFrameDefault 函數(shù),整個(gè)過(guò)程非常好理解。

注:棧幀指的是 PyFrameObject,它是一個(gè) Python 對(duì)象,而 _PyInterpreterFrame 是一個(gè)只包含棧幀核心信息的普通 C 結(jié)構(gòu)體,前面我們介紹過(guò)的。 

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

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

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

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

另外我們說(shuō)指令序列里面包含的不僅僅是指令,還有指令參數(shù),因?yàn)槊總€(gè)指令都會(huì)帶一個(gè)參數(shù)。因此索引為 0 2 4 6 8 ··· 的整數(shù)表示指令,索引為 1 3 5 7 9 ··· 的整數(shù)表示指令參數(shù)。

我們用 Python 來(lái)演示一下:

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

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

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

0           0 RESUME                   0

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

// 從常量池中加載索引為 1 的常量(結(jié)果是 2),并壓入運(yùn)行時(shí)棧
3           6 LOAD_CONST               1 (2)
// 從符號(hào)表中加載索引為 1 的符號(hào)(結(jié)果是 "b")
// 然后從棧頂彈出元素 2,將 "b" 和 2 綁定起來(lái)
            8 STORE_NAME               1 (b)

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

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

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

圖片圖片

下面我們來(lái)進(jìn)行還原。

"""
 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,  # 計(jì)算 a + b
    # 從 dis 的輸出信息可以看到,索引為 16 的指令沒(méi)有顯示
    # 這是因?yàn)樗饕秊?16 的指令和相應(yīng)的指令參數(shù)都是 0
    # 也就是說(shuō),在將 c 和 a + b 綁定之前,插入了一條 CACHE 指令
    # 這條指令是做什么的,我們后續(xù)再探討
    CACHE, 0,
    STORE_NAME, 2,

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

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

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

結(jié)果是一樣的,到此相信你對(duì) Python 源代碼的執(zhí)行過(guò)程應(yīng)該有更深的了解了,簡(jiǎn)單來(lái)講,其實(shí)就是以下幾個(gè)步驟。

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

2)虛擬機(jī)在 PyCodeObject 對(duì)象的基礎(chǔ)上構(gòu)建棧楨對(duì)象。

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

什么是運(yùn)行時(shí)棧

之前一直提到一個(gè)概念,叫運(yùn)行時(shí)棧,那什么是運(yùn)行時(shí)棧呢?別急,我們先來(lái)回顧一下棧楨的基本結(jié)構(gòu)。

圖片圖片

大部分字段都很好理解,因?yàn)橹巴ㄟ^(guò) Python 代碼演示過(guò)。但有幾個(gè)字段是虛擬機(jī)用于執(zhí)行指令的,后續(xù)會(huì)遇到,所以這里再拿出來(lái)解釋一下。

prev_instr

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

localsplus

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

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

然后再著重強(qiáng)調(diào)一下運(yùn)行時(shí)棧,虛擬機(jī)在執(zhí)行字節(jié)碼指令時(shí)高度依賴它,因?yàn)橐粋€(gè)指令只能帶一個(gè)參數(shù),那么剩余的參數(shù)就必須通過(guò)運(yùn)行時(shí)棧給出。比如 a = 1 會(huì)對(duì)應(yīng)兩條字節(jié)碼:LOAD_CONST 和 STORE_NAME。

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

毫無(wú)疑問(wèn),要通過(guò)運(yùn)行時(shí)棧,因此 LOAD_CONST 將值讀取進(jìn)來(lái)之后,還要壓入運(yùn)行時(shí)棧。然后 STORE_NAME 會(huì)將值從運(yùn)行時(shí)棧中彈出,從而完成符號(hào)(變量)和值的綁定。關(guān)于運(yùn)行時(shí)棧,我們?cè)倏磦€(gè)復(fù)雜的例子:

圖片圖片

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

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

stacktop

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

我們通過(guò)源碼驗(yàn)證一下:

// Inlcude/internal/pycore_frame.h

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

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

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

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

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

這是 localsplus 的示意圖,我們只看運(yùn)行時(shí)棧,目前棧里面有兩個(gè)元素,stack_pointer 指向棧頂。

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

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

圖片圖片

而 stack_pointer - localsplus 便是 stacktop。

運(yùn)行時(shí)棧的一些 API

相信你對(duì)運(yùn)行時(shí)棧的了解已經(jīng)足夠深了,說(shuō)白了它就是參數(shù)的容身之所,比如虛擬機(jī)在執(zhí)行 a + b 的時(shí)候,通過(guò)指令參數(shù)可以判斷這是一個(gè)加法操作。但在執(zhí)行加法的時(shí)候,加號(hào)兩邊的值要怎么獲取呢?這時(shí)候就需要一個(gè)棧來(lái)專門(mén)保存相應(yīng)的參數(shù)。

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

然后再來(lái)看看運(yùn)行時(shí)棧相關(guān)的一些 API。

圖片圖片

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

圖片圖片

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

然后看一下這些和運(yùn)行時(shí)棧相關(guān)的 API 都是干嘛的。

STACK_LEVEL()

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

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

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

// Include/internal/pycore_frame.h

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

在之前的版本中(比如 3.8),棧楨內(nèi)部會(huì)有一個(gè) f_valuestack 字段,專門(mén)負(fù)責(zé)指向運(yùn)行時(shí)棧的棧底。注:STACK_LEVEL() 是動(dòng)態(tài)變化的,因?yàn)?stack_pointer 會(huì)動(dòng)態(tài)變化。

STACK_SIZE()

運(yùn)行時(shí)棧最多能存儲(chǔ)多少個(gè)元素,或者說(shuō)運(yùn)行時(shí)棧的長(zhǎng)度。

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

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

比如 co_stacksize 是 1,那么表示應(yīng)該給運(yùn)行時(shí)棧分配能存儲(chǔ) 1 個(gè)元素的內(nèi)存,即運(yùn)行時(shí)棧的長(zhǎng)度為 1。我們通過(guò)反編譯的方式,實(shí)際演示一下。

import dis

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

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

我們?cè)賮?lái)看個(gè)例子。

import dis

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

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

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

相信你現(xiàn)在應(yīng)該理解 co_stacksize 的作用了,它表示運(yùn)行時(shí)棧最多能容納多少個(gè)元素,也就是運(yùn)行時(shí)棧的長(zhǎng)度。以上面代碼為例,由于最多會(huì)壓入 3 個(gè)元素,所以運(yùn)行時(shí)棧的長(zhǎng)度就是 3,即最多能容納 3 個(gè)元素。并且這個(gè)長(zhǎng)度在編譯之后就已經(jīng)確定了,因?yàn)榭梢酝ㄟ^(guò)遍歷指令集靜態(tài)計(jì)算出來(lái)。

我們畫(huà)一張圖描述一下執(zhí)行上面的代碼時(shí),運(yùn)行時(shí)棧的變化過(guò)程。

圖片圖片

整個(gè)過(guò)程應(yīng)該很清晰,當(dāng)然上面只是運(yùn)行時(shí)棧的變化,localsplus 中存儲(chǔ)局部變量的內(nèi)存區(qū)域也在變化。另外如果代碼塊位于全局作用域,那么變化的就是全局名字空間。

EMPTY()

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

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

TOP()

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

查看當(dāng)前運(yùn)行時(shí)棧的棧頂元素。

SECOND()

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

查看從棧頂元素開(kāi)始的第二個(gè)元素。

THIRD()

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

查看從棧頂元素開(kāi)始的第三個(gè)元素。

FOURTH()

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

查看從棧頂元素開(kāi)始的第四個(gè)元素。

PEEK(n):

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

查看從棧頂元素開(kāi)始的第 n 個(gè)元素。

SET_TOP(v)

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

將當(dāng)前運(yùn)行時(shí)棧的棧頂元素設(shè)置成 v。

SET_SECOND(v)

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

將從棧頂元素開(kāi)始的第二個(gè)元素設(shè)置成 v。

POKE(n)

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

將從棧頂元素開(kāi)始的第 n 個(gè)元素設(shè)置成 v。

PUSH(v)

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

往運(yùn)行時(shí)棧中壓入一個(gè)元素,并且壓入之后,棧中已存儲(chǔ)的元素個(gè)數(shù)一定不超過(guò) co_stacksize。換句話說(shuō),STACK_LEVEL() 永遠(yuǎn)小于等于 STACK_SIZE()。

然后入棧和出棧,都是通過(guò)操作 stack_pointer 完成的。假設(shè)當(dāng)前棧里有一個(gè)元素 1,然后添加一個(gè)元素 2。

圖片圖片

Python 的變量都是一個(gè)指針,所以 stack_pointer 是一個(gè)二級(jí)指針,它永遠(yuǎn)指向棧頂位置,只不過(guò)棧頂位置會(huì)變。

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

POP()

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

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

圖片圖片

stack_pointer 指向棧頂位置,所以它向棧底移動(dòng)一個(gè)位置,就相當(dāng)于元素被彈出了。

關(guān)于彈出元素需要做一個(gè)說(shuō)明,所謂彈出元素本質(zhì)上就是將 stack_pointer 向棧底移動(dòng)一個(gè)位置。我們看一下上圖,一開(kāi)始棧里面有兩個(gè)元素,分別是整數(shù) 1 和整數(shù) 2,當(dāng)然準(zhǔn)確來(lái)說(shuō)應(yīng)該是指向它們的指針,但為了描述方便,我們就用對(duì)象本身代替了。

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

不過(guò)在后續(xù)的文章中,在畫(huà)運(yùn)行時(shí)棧的時(shí)候,我們會(huì)這么畫(huà)。

圖片圖片

為了閱讀清晰,stack_pointer 之后的元素就不寫(xiě)了。另外還要注意一點(diǎn),運(yùn)行時(shí)棧的內(nèi)存一開(kāi)始就已經(jīng)申請(qǐng)好了,是固定的,彈出元素只是改變棧頂指針 stack_pointer 的指向,內(nèi)存區(qū)域的大小不變。

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

STACK_GROW(n)

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

改變運(yùn)行時(shí)棧的棧頂,注:運(yùn)行時(shí)棧的大小是固定的,但棧頂是由 stack_pointer 決定的。

圖片圖片

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

PUSH(2);
PUSH(3);

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

STACK_GROW(2);
// 設(shè)置元素
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 正好相反。

圖片圖片

以上就是運(yùn)行時(shí)棧的一些 API,后續(xù)再看源碼的時(shí)候,會(huì)經(jīng)常遇到。

小結(jié)

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

然后是運(yùn)行時(shí)棧,因?yàn)橐粋€(gè)指令只能帶一個(gè)參數(shù),那么剩余的參數(shù)就需要通過(guò)運(yùn)行時(shí)棧給出。比如 LOAD_CONST 指令,它在加載完常量之后,會(huì)將常量壓入運(yùn)行時(shí)棧,然后 STORE_NAME 或 STORE_FAST 指令再將常量從運(yùn)行時(shí)棧的頂部彈出,和某個(gè)符號(hào)(變量)綁定起來(lái)。另外關(guān)于這些指令,我們后面會(huì)詳細(xì)說(shuō)。

最后我們介紹了運(yùn)行時(shí)棧的一些 API,或者說(shuō)宏,因?yàn)閳?zhí)行指令的時(shí)候會(huì)反復(fù)操作運(yùn)行時(shí)棧,所以底層封裝了很多的宏。

責(zé)任編輯:武曉燕 來(lái)源: 古明地覺(jué)的編程教室
相關(guān)推薦

2024-11-15 16:27:58

函數(shù)結(jié)構(gòu)存儲(chǔ)

2018-04-04 15:05:17

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

2024-05-22 13:04:46

Python對(duì)象關(guān)系

2024-05-21 12:51:06

Python對(duì)象PyObject

2024-08-29 12:37:11

2017-11-14 16:43:13

Java虛擬機(jī)線程

2023-09-03 17:05:20

虛擬機(jī)

2023-01-09 18:30:53

架構(gòu)JVM

2023-09-02 21:35:39

Linux虛擬機(jī)

2010-06-18 10:13:17

虛擬機(jī)消失

2013-09-17 10:35:17

Python執(zhí)行原理

2010-02-24 10:39:28

Python虛擬機(jī)

2024-11-11 11:21:30

虛擬機(jī)Python跳轉(zhuǎn)表

2012-08-16 09:07:57

Erlang

2021-05-28 23:04:23

Python利器執(zhí)行

2014-12-18 09:41:44

虛擬化遷移

2010-09-25 15:13:40

JVMJava虛擬機(jī)

2010-10-13 10:21:37

物理機(jī)虛擬機(jī)遷移

2016-08-24 08:47:19

kali linuxvirtualbox虛擬機(jī)

2009-08-10 10:49:27

Linux下安裝VMw
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)