国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

[譯] 從底層理解 Python 的執(zhí)行

wmui / 2055人閱讀

摘要:接下來(lái),我們將注入到函數(shù)的字節(jié)碼中。首先我們來(lái)看一下幀的參數(shù)所能提供的信息,如下所示當(dāng)前幀將執(zhí)行的當(dāng)前的操作中的字節(jié)碼字符串的索引經(jīng)過(guò)我們的處理我們可以得知之后要被執(zhí)行的操作碼,這對(duì)我們聚合數(shù)據(jù)并展示是相當(dāng)有用的。

原文鏈接: Understanding Python execution from inside: A Python assembly tracer

以下為譯文


最近我在學(xué)習(xí) Python 的運(yùn)行模型。我對(duì) Python 的一些內(nèi)部機(jī)制很是好奇,比如 Python 是怎么實(shí)現(xiàn)類似 YIELDVALUEYIELDFROM 這樣的操作碼的;對(duì)于 遞推式構(gòu)造列表(List Comprehensions)、生成器表達(dá)式(generator expressions)以及其他一些有趣的 Python 特性是怎么編譯的;從字節(jié)碼的層面來(lái)看,當(dāng)異常拋出的時(shí)候都發(fā)生了什么事情。翻閱 CPython 的代碼對(duì)于解答這些問(wèn)題當(dāng)然是很有幫助的,但我仍然覺(jué)得以這樣的方式來(lái)做的話對(duì)于理解字節(jié)碼的執(zhí)行和堆棧的變化還是缺少點(diǎn)什么。GDB 是個(gè)好選擇,但是我懶,而且只想使用一些比較高階的接口寫(xiě)點(diǎn) Python 代碼來(lái)完成這件事。

所以呢,我的目標(biāo)就是創(chuàng)建一個(gè)字節(jié)碼級(jí)別的追蹤 API,類似 sys.setrace 所提供的那樣,但相對(duì)而言會(huì)有更好的粒度。這充分鍛煉了我編寫(xiě) Python 實(shí)現(xiàn)的 C 代碼的編碼能力。我們所需要的有如下幾項(xiàng),在這篇文章中所用的 Python 版本為 3.5。

一個(gè)新的 Cpython 解釋器操作碼

一種將操作碼注入到 Python 字節(jié)碼的方法

一些用于處理操作碼的 Python 代碼

一個(gè)新的 Cpython 操作碼 新操作碼:DEBUG_OP

這個(gè)新的操作碼 DEBUG_OP 是我第一次嘗試寫(xiě) CPython 實(shí)現(xiàn)的 C 代碼,我將盡可能的讓它保持簡(jiǎn)單。 我們想要達(dá)成的目的是,當(dāng)我們的操作碼被執(zhí)行的時(shí)候我能有一種方式來(lái)調(diào)用一些 Python 代碼。同時(shí),我們也想能夠追蹤一些與執(zhí)行上下文有關(guān)的數(shù)據(jù)。我們的操作碼會(huì)把這些信息當(dāng)作參數(shù)傳遞給我們的回調(diào)函數(shù)。通過(guò)操作碼能辨識(shí)出的有用信息如下:

堆棧的內(nèi)容

執(zhí)行 DEBUG_OP 的幀對(duì)象信息

所以呢,我們的操作碼需要做的事情是:

找到回調(diào)函數(shù)

創(chuàng)建一個(gè)包含堆棧內(nèi)容的列表

調(diào)用回調(diào)函數(shù),并將包含堆棧內(nèi)容的列表和當(dāng)前幀作為參數(shù)傳遞給它

聽(tīng)起來(lái)挺簡(jiǎn)單的,現(xiàn)在開(kāi)始動(dòng)手吧!聲明:下面所有的解釋說(shuō)明和代碼是經(jīng)過(guò)了大量段錯(cuò)誤調(diào)試之后總結(jié)得到的結(jié)論。首先要做的是給操作碼定義一個(gè)名字和相應(yīng)的值,因此我們需要在 Include/opcode.h中添加代碼。

    /** My own comments begin by "**" **/  
    /** From: Includes/opcode.h **/  

    /* Instruction opcodes for compiled code */  

    /** We just have to define our opcode with a free value  
        0 was the first one I found **/  
    #define DEBUG_OP                0  

    #define POP_TOP                 1  
    #define ROT_TWO                 2  
    #define ROT_THREE               3  

這部分工作就完成了,現(xiàn)在我們?nèi)ゾ帉?xiě)操作碼真正干活的代碼。

實(shí)現(xiàn) DEBUG_OP

在考慮如何實(shí)現(xiàn)DEBUG_OP之前我們需要了解的是 DEBUG_OP 提供的接口將長(zhǎng)什么樣。 擁有一個(gè)可以調(diào)用其他代碼的新操作碼是相當(dāng)酷眩的,但是究竟它將調(diào)用哪些代碼捏?這個(gè)操作碼如何找到回調(diào)函數(shù)的捏?我選擇了一種最簡(jiǎn)單的方法:在幀的全局區(qū)域?qū)懰篮瘮?shù)名。那么問(wèn)題就變成了,我該怎么從字典中找到一個(gè)固定的 C 字符串?為了回答這個(gè)問(wèn)題我們來(lái)看看在 Python 的 main loop 中使用到的和上下文管理相關(guān)的標(biāo)識(shí)符 enterexit

我們可以看到這兩標(biāo)識(shí)符被使用在操作碼 SETUP_WITH 中:

    /** From: Python/ceval.c **/  
    TARGET(SETUP_WITH) {  
    _Py_IDENTIFIER(__exit__);  
    _Py_IDENTIFIER(__enter__);  
    PyObject *mgr = TOP();  
    PyObject *exit = special_lookup(mgr, &PyId___exit__), *enter;  
    PyObject *res;  

現(xiàn)在,看一眼宏 _Py_IDENTIFIER 定義

/** From: Include/object.h **/

/********************* String Literals ****************************************/
/* This structure helps managing static strings. The basic usage goes like this:
   Instead of doing

       r = PyObject_CallMethod(o, "foo", "args", ...);

   do

       _Py_IDENTIFIER(foo);
       ...
       r = _PyObject_CallMethodId(o, &PyId_foo, "args", ...);

   PyId_foo is a static variable, either on block level or file level. On first
   usage, the string "foo" is interned, and the structures are linked. On interpreter
   shutdown, all strings are released (through _PyUnicode_ClearStaticStrings).

   Alternatively, _Py_static_string allows to choose the variable name.
   _PyUnicode_FromId returns a borrowed reference to the interned string.
   _PyObject_{Get,Set,Has}AttrId are __getattr__ versions using _Py_Identifier*.
*/
typedef struct _Py_Identifier {
    struct _Py_Identifier *next;
    const char* string;
    PyObject *object;
} _Py_Identifier;

#define _Py_static_string_init(value) { 0, value, 0 }
#define _Py_static_string(varname, value)  static _Py_Identifier varname = _Py_static_string_init(value)
#define _Py_IDENTIFIER(varname) _Py_static_string(PyId_##varname, #varname)

嗯,注釋部分已經(jīng)說(shuō)明得很清楚了。通過(guò)一番查找,我們發(fā)現(xiàn)了可以用來(lái)從字典找固定字符串的函數(shù) _PyDict_GetItemId,所以我們操作碼的查找部分的代碼就是長(zhǎng)這樣滴。

     /** Our callback function will be named op_target **/  
    PyObject *target = NULL;  
    _Py_IDENTIFIER(op_target);  
    target = _PyDict_GetItemId(f->f_globals, &PyId_op_target);  
    if (target == NULL && _PyErr_OCCURRED()) {  
        if (!PyErr_ExceptionMatches(PyExc_KeyError))  
            goto error;  
        PyErr_Clear();  
        DISPATCH();  
    }  

為了方便理解,對(duì)這一段代碼做一些說(shuō)明:

f 是當(dāng)前的幀,f->f_globals 是它的全局區(qū)域

如果我們沒(méi)有找到 op_target,我們將會(huì)檢查這個(gè)異常是不是 KeyError

goto error; 是一種在 main loop 中拋出異常的方法

PyErr_Clear() 抑制了當(dāng)前異常的拋出,而 DISPATCH() 觸發(fā)了下一個(gè)操作碼的執(zhí)行

下一步就是收集我們想要的堆棧信息。

    /** This code create a list with all the values on the current   stack **/  
    PyObject *value = PyList_New(0);  
    for (i = 1 ; i <= STACK_LEVEL(); i++) {  
        tmp = PEEK(i);  
        if (tmp == NULL) {  
            tmp = Py_None;  
        }  
        PyList_Append(value, tmp);  
    }  

最后一步就是調(diào)用我們的回調(diào)函數(shù)!我們用 call_function 來(lái)搞定這件事,我們通過(guò)研究操作碼 CALL_FUNCTION 的實(shí)現(xiàn)來(lái)學(xué)習(xí)怎么使用 call_function 。

    /** From: Python/ceval.c **/  
    TARGET(CALL_FUNCTION) {  
        PyObject **sp, *res;  
        /** stack_pointer is a local of the main loop.  
            It"s the pointer to the stacktop of our frame **/  
        sp = stack_pointer;  
        res = call_function(&sp, oparg);  
        /** call_function handles the args it consummed on the stack     for us **/  
        stack_pointer = sp;  
        PUSH(res);  
        /** Standard exception handling **/  
        if (res == NULL)  
            goto error;  
        DISPATCH();  
    }  

有了上面這些信息,我們終于可以搗鼓出一個(gè)操作碼DEBUG_OP的草稿了:

    TARGET(DEBUG_OP) {  
        PyObject *value = NULL;  
        PyObject *target = NULL;  
        PyObject *res = NULL;  
        PyObject **sp = NULL;  
        PyObject *tmp;  
        int i;  
        _Py_IDENTIFIER(op_target);  

        target = _PyDict_GetItemId(f->f_globals, &PyId_op_target);  
        if (target == NULL && _PyErr_OCCURRED()) {  
            if (!PyErr_ExceptionMatches(PyExc_KeyError))  
                goto error;  
            PyErr_Clear();  
            DISPATCH();  
        }  
        value = PyList_New(0);  
        Py_INCREF(target);  
        for (i = 1 ; i <= STACK_LEVEL(); i++) {  
            tmp = PEEK(i);  
            if (tmp == NULL)  
                tmp = Py_None;  
            PyList_Append(value, tmp);  
        }  

        PUSH(target);  
        PUSH(value);  
        Py_INCREF(f);  
        PUSH(f);  
        sp = stack_pointer;  
        res = call_function(&sp, 2);  
        stack_pointer = sp;  
        if (res == NULL)  
            goto error;  
        Py_DECREF(res);  
        DISPATCH();  
    }

在編寫(xiě) CPython 實(shí)現(xiàn)的 C 代碼方面我確實(shí)沒(méi)有什么經(jīng)驗(yàn),有可能我漏掉了些細(xì)節(jié)。如果您有什么建議還請(qǐng)您糾正,我期待您的反饋。

編譯它,成了!

一切看起來(lái)很順利,但是當(dāng)我們嘗試去使用我們定義的操作碼 DEBUG_OP 的時(shí)候卻失敗了。自從 2008 年之后,Python 使用預(yù)先寫(xiě)好的 goto(你也可以從 這里獲取更多的訊息)。故,我們需要更新下 goto jump table,我們?cè)?Python/opcode_targets.h 中做如下修改。

    /** From: Python/opcode_targets.h **/  
    /** Easy change since DEBUG_OP is the opcode number 1 **/  
    static void *opcode_targets[256] = {  
        //&&_unknown_opcode,  
        &&TARGET_DEBUG_OP,  
        &&TARGET_POP_TOP,  
        /** ... **/  

這就完事了,我們現(xiàn)在就有了一個(gè)可以工作的新操作碼。唯一的問(wèn)題就是這貨雖然存在,但是沒(méi)有被人調(diào)用過(guò)。接下來(lái),我們將DEBUG_OP注入到函數(shù)的字節(jié)碼中。

在 Python 字節(jié)碼中注入操作碼 DEBUG_OP

有很多方式可以在 Python 字節(jié)碼中注入新的操作碼:

使用 peephole optimizer, Quarkslab就是這么干的

在生成字節(jié)碼的代碼中動(dòng)些手腳

在運(yùn)行時(shí)直接修改函數(shù)的字節(jié)碼(這就是我們將要干的事兒)

為了創(chuàng)造出一個(gè)新操作碼,有了上面的那一堆 C 代碼就夠了。現(xiàn)在讓我們回到原點(diǎn),開(kāi)始理解奇怪甚至神奇的 Python!

我們將要做的事兒有:

得到我們想要追蹤函數(shù)的 code object

重寫(xiě)字節(jié)碼來(lái)注入 DEBUG_OP

將新生成的 code object 替換回去

和 code object 有關(guān)的小貼士

如果你從沒(méi)聽(tīng)說(shuō)過(guò) code object,這里有一個(gè)簡(jiǎn)單的介紹網(wǎng)路上也有一些相關(guān)的文檔可供查閱,可以直接 Ctrl+F 查找 code object

還有一件事情需要注意的是在這篇文章所指的環(huán)境中 code object 是不可變的:

    Python 3.4.2 (default, Oct  8 2014, 10:45:20)  
    [GCC 4.9.1] on linux  
    Type "help", "copyright", "credits" or "license" for more      information.  
    >>> x = lambda y : 2  
    >>> x.__code__  
     at 0x7f481fd88390, file "", line 1>      
    >>> x.__code__.co_name  
    ""  
    >>> x.__code__.co_name = "truc"  
    Traceback (most recent call last):  
      File "", line 1, in   
    AttributeError: readonly attribute  
    >>> x.__code__.co_consts = ("truc",)  
    Traceback (most recent call last):  
      File "", line 1, in   
    AttributeError: readonly attribute  

但是不用擔(dān)心,我們將會(huì)找到方法繞過(guò)這個(gè)問(wèn)題的。

使用的工具

為了修改字節(jié)碼我們需要一些工具:

dis模塊用來(lái)反編譯和分析字節(jié)碼

dis.BytecodePython 3.4新增的一個(gè)特性,對(duì)于反編譯和分析字節(jié)碼特別有用

一個(gè)能夠簡(jiǎn)單修改 code object 的方法

用 dis.Bytecode 反編譯 code object 能告訴我們一些有關(guān)操作碼、參數(shù)和上下文的信息。

    # Python3.4  
    >>> import dis  
    >>> f = lambda x: x + 3  
    >>> for i in dis.Bytecode(f.__code__): print (i)  
    ...  
    Instruction(opname="LOAD_FAST", opcode=124, arg=0, argval="x",       argrepr="x", offset=0, starts_line=1, is_jump_target=False)  
    Instruction(opname="LOAD_CONST", opcode=100, arg=1, argval=3,        argrepr="3", offset=3, starts_line=None, is_jump_target=False)  
    Instruction(opname="BINARY_ADD", opcode=23, arg=None,            argval=None, argrepr="", offset=6, starts_line=None,     is_jump_target=False)  
    Instruction(opname="RETURN_VALUE", opcode=83, arg=None,       argval=None, argrepr="", offset=7, starts_line=None,    is_jump_target=False)  

為了能夠修改 code object,我定義了一個(gè)很小的類用來(lái)復(fù)制 code object,同時(shí)能夠按我們的需求修改相應(yīng)的值,然后重新生成一個(gè)新的 code object。

    class MutableCodeObject(object):  
        args_name = ("co_argcount", "co_kwonlyargcount",  "co_nlocals", "co_stacksize", "co_flags", "co_code",  
                      "co_consts", "co_names", "co_varnames",     "co_filename", "co_name", "co_firstlineno",  
                       "co_lnotab", "co_freevars", "co_cellvars")  

        def __init__(self, initial_code):  
            self.initial_code = initial_code  
            for attr_name in self.args_name:  
                attr = getattr(self.initial_code, attr_name)  
                if isinstance(attr, tuple):  
                    attr = list(attr)  
                setattr(self, attr_name, attr)  

        def get_code(self):  
            args = []  
            for attr_name in self.args_name:  
                attr = getattr(self, attr_name)  
                if isinstance(attr, list):  
                    attr = tuple(attr)  
                args.append(attr)  
            return self.initial_code.__class__(*args)  

這個(gè)類用起來(lái)很方便,解決了上面提到的 code object 不可變的問(wèn)題。

    >>> x = lambda y : 2  
    >>> m = MutableCodeObject(x.__code__)  
    >>> m  
      
    >>> m.co_consts  
    [None, 2]  
    >>> m.co_consts[1] = "3"  
    >>> m.co_name = "truc"  
    >>> m.get_code()  
    ", line 1>  
測(cè)試我們的新操作碼

我們現(xiàn)在擁有了注入 DEBUG_OP 的所有工具,讓我們來(lái)驗(yàn)證下我們的實(shí)現(xiàn)是否可用。我們將我們的操作碼注入到一個(gè)最簡(jiǎn)單的函數(shù)中:

    from new_code import MutableCodeObject  

    def op_target(*args):  
        print("WOOT")  
        print("op_target called with args <{0}>".format(args))  

    def nop():  
       pass  

    new_nop_code = MutableCodeObject(nop.__code__)  
    new_nop_code.co_code = b"x00" + new_nop_code.co_code[0:3] +  b"x00" + new_nop_code.co_code[-1:]  
    new_nop_code.co_stacksize += 3  

    nop.__code__ = new_nop_code.get_code()  

    import dis  
    dis.dis(nop)  
    nop()  


    # Don"t forget that ./python is our custom Python implementing       DEBUG_OP  
    hakril@computer ~/python/CPython3.5 % ./python proof.py  
      8           0 <0>  
                  1 LOAD_CONST               0 (None)  
                  4 <0>  
                  5 RETURN_VALUE  
    WOOT  
    op_target called with args <([], )>  
    WOOT  
    op_target called with args <([None], )>  

看起來(lái)它成功了!有一行代碼需要說(shuō)明一下 new_nop_code.co_stacksize += 3

co_stacksize 表示 code object 所需要的堆棧的大小

操作碼DEBUG_OP往堆棧中增加了三項(xiàng),所以我們需要為這些增加的項(xiàng)預(yù)留些空間

現(xiàn)在我們可以將我們的操作碼注入到每一個(gè) Python 函數(shù)中了!

重寫(xiě)字節(jié)碼

正如我們?cè)谏厦娴睦又兴吹降哪菢樱貙?xiě) Pyhton 的字節(jié)碼似乎 so easy。為了在每一個(gè)操作碼之間注入我們的操作碼,我們需要獲取每一個(gè)操作碼的偏移量,然后將我們的操作碼注入到這些位置上(把我們操作碼注入到參數(shù)上是有壞處大大滴)。這些偏移量也很容易獲取,使用 dis.Bytecode,就像這樣。

    def add_debug_op_everywhere(code_obj):  
         # We get every instruction offset in the code object  
        offsets = [instr.offset for instr in dis.Bytecode(code_obj)]   
        # And insert a DEBUG_OP at every offset  
        return insert_op_debug_list(code_obj, offsets)  

    def insert_op_debug_list(code, offsets):  
         # We insert the DEBUG_OP one by one  
        for nb, off in enumerate(sorted(offsets)):  
            # Need to ajust the offsets by the number of opcodes          already inserted before  
            # That"s why we sort our offsets!  
            code = insert_op_debug(code, off + nb)  
        return code  

    # Last problem: what does insert_op_debug looks like?  

基于上面的例子,有人可能會(huì)想我們的 insert_op_debug 會(huì)在指定的偏移量增加一個(gè)"x00",這尼瑪是個(gè)坑啊!我們第一個(gè) DEBUG_OP 注入的例子中被注入的函數(shù)是沒(méi)有任何的分支的,為了能夠?qū)崿F(xiàn)完美一個(gè)函數(shù)注入函數(shù) insert_op_debug 我們需要考慮到存在分支操作碼的情況。

Python 的分支一共有兩種:

絕對(duì)分支:看起來(lái)是類似這樣子的 Instruction_Pointer = argument(instruction)

相對(duì)分支:看起來(lái)是類似這樣子的 Instruction_Pointer += argument(instruction)

相對(duì)分支總是向前的

我們希望這些分支在我們插入操作碼之后仍然能夠正常工作,為此我們需要修改一些指令參數(shù)。以下是其邏輯流程:

對(duì)于每一個(gè)在插入偏移量之前的相對(duì)分支而言

如果目標(biāo)地址是嚴(yán)格大于我們的插入偏移量的話,將指令參數(shù)增加 1

如果相等,則不需要增加 1 就能夠在跳轉(zhuǎn)操作和目標(biāo)地址之間執(zhí)行我們的操作碼DEBUG_OP

如果小于,插入我們的操作碼的話并不會(huì)影響到跳轉(zhuǎn)操作和目標(biāo)地址之間的距離

對(duì)于 code object 中的每一個(gè)絕對(duì)分支而言

如果目標(biāo)地址是嚴(yán)格大于我們的插入偏移量的話,將指令參數(shù)增加 1

如果相等,那么不需要任何修改,理由和相對(duì)分支部分是一樣的

如果小于,插入我們的操作碼的話并不會(huì)影響到跳轉(zhuǎn)操作和目標(biāo)地址之間的距離

下面是實(shí)現(xiàn):

    # Helper  
    def bytecode_to_string(bytecode):  
        if bytecode.arg is not None:  
            return struct.pack(" offset:  
                    res_codestring +=      bytecode_to_string(DummyInstr(instr.opcode, instr.arg + 1))  
                    continue  
            res_codestring += bytecode_to_string(instr)  
        # replace_bytecode just replaces the original code co_code  
        return replace_bytecode(code, res_codestring)  

讓我們看一下效果如何:

    >>> def lol(x):  
    ...     for i in range(10):  
    ...         if x == i:  
    ...             break  

    >>> dis.dis(lol)  
    101           0 SETUP_LOOP              36 (to 39)  
                  3 LOAD_GLOBAL              0 (range)  
                  6 LOAD_CONST               1 (10)  
                  9 CALL_FUNCTION            1 (1 positional, 0    keyword pair)  
                 12 GET_ITER  
            >>   13 FOR_ITER                22 (to 38)  
                 16 STORE_FAST               1 (i)  

    102          19 LOAD_FAST                0 (x)  
                 22 LOAD_FAST                1 (i)  
                 25 COMPARE_OP               2 (==)  
                 28 POP_JUMP_IF_FALSE       13  

    103          31 BREAK_LOOP  
                 32 JUMP_ABSOLUTE           13  
                 35 JUMP_ABSOLUTE           13  
            >>   38 POP_BLOCK  
            >>   39 LOAD_CONST               0 (None)  
                 42 RETURN_VALUE  
    >>> lol.__code__ = transform_code(lol.__code__,        add_debug_op_everywhere, add_stacksize=3)  


    >>> dis.dis(lol)  
    101           0 <0>  
                  1 SETUP_LOOP              50 (to 54)  
                  4 <0>  
                  5 LOAD_GLOBAL              0 (range)  
                  8 <0>  
                  9 LOAD_CONST               1 (10)  
                 12 <0>  
                 13 CALL_FUNCTION            1 (1 positional, 0   keyword pair)  
                 16 <0>  
                 17 GET_ITER  
            >>   18 <0>  

    102          19 FOR_ITER                30 (to 52)  
                 22 <0>  
                 23 STORE_FAST               1 (i)  
                 26 <0>  
                 27 LOAD_FAST                0 (x)  
                 30 <0>  

    103          31 LOAD_FAST                1 (i)  
                 34 <0>  
                 35 COMPARE_OP               2 (==)  
                 38 <0>  
                 39 POP_JUMP_IF_FALSE       18  
                 42 <0>  
                 43 BREAK_LOOP  
                 44 <0>  
                 45 JUMP_ABSOLUTE           18  
                 48 <0>  
                 49 JUMP_ABSOLUTE           18  
            >>   52 <0>  
                 53 POP_BLOCK  
            >>   54 <0>  
                 55 LOAD_CONST               0 (None)  
                 58 <0>  
                 59 RETURN_VALUE  

     # Setup the simplest handler EVER  
    >>> def op_target(stack, frame):  
    ...     print (stack)  

    # GO  
    >>> lol(2)  
    []  
    []  
    []  
    [10, ]  
    [range(0, 10)]  
    []  
    [0, ]  
    []  
    [2, ]  
    [0, 2, ]  
    [False, ]  
    []  
    [1, ]  
    []  
    [2, ]  
    [1, 2, ]  
    [False, ]  
    []  
    [2, ]  
    []  
    [2, ]  
    [2, 2, ]  
    [True, ]  
    []  
    []  
    [None]  

甚好!現(xiàn)在我們知道了如何獲取堆棧信息和 Python 中每一個(gè)操作對(duì)應(yīng)的幀信息。上面結(jié)果所展示的結(jié)果目前而言并不是很實(shí)用。在最后一部分中讓我們對(duì)注入做進(jìn)一步的封裝。

增加 Python 封裝

正如您所見(jiàn)到的,所有的底層接口都是好用的。我們最后要做的一件事是讓 op_target 更加方便使用(這部分相對(duì)而言比較空泛一些,畢竟在我看來(lái)這不是整個(gè)項(xiàng)目中最有趣的部分)。

首先我們來(lái)看一下幀的參數(shù)所能提供的信息,如下所示:

f_code當(dāng)前幀將執(zhí)行的 code object

f_lasti當(dāng)前的操作(code object 中的字節(jié)碼字符串的索引)

經(jīng)過(guò)我們的處理我們可以得知 DEBUG_OP 之后要被執(zhí)行的操作碼,這對(duì)我們聚合數(shù)據(jù)并展示是相當(dāng)有用的。

新建一個(gè)用于追蹤函數(shù)內(nèi)部機(jī)制的類:

改變函數(shù)自身的 co_code

設(shè)置回調(diào)函數(shù)作為 op_debug 的目標(biāo)函數(shù)

一旦我們知道下一個(gè)操作,我們就可以分析它并修改它的參數(shù)。舉例來(lái)說(shuō)我們可以增加一個(gè) auto-follow-called-functions 的特性。

    def op_target(l, f, exc=None):  
        if op_target.callback is not None:  
            op_target.callback(l, f, exc)  

    class Trace:  
        def __init__(self, func):  
            self.func = func  

        def call(self, *args, **kwargs):  
             self.add_func_to_trace(self.func)  
            # Activate Trace callback for the func call  
            op_target.callback = self.callback  
            try:  
                res = self.func(*args, **kwargs)  
            except Exception as e:  
                res = e  
            op_target.callback = None  
            return res  

        def add_func_to_trace(self, f):  
            # Is it code? is it already transformed?  
            if not hasattr(f ,"op_debug") and hasattr(f, "__code__"):  
                f.__code__ = transform_code(f.__code__,    transform=add_everywhere, add_stacksize=ADD_STACK)  
                f.__globals__["op_target"] = op_target  
                f.op_debug = True  

        def do_auto_follow(self, stack, frame):  
            # Nothing fancy: FrameAnalyser is just the wrapper that  gives the next executed instruction  
            next_instr = FrameAnalyser(frame).next_instr()  
            if "CALL" in next_instr.opname:  
                arg = next_instr.arg  
                f_index = (arg & 0xff) + (2 * (arg >> 8))  
                called_func = stack[f_index]  

                # If call target is not traced yet: do it  
                if not hasattr(called_func, "op_debug"):  
                    self.add_func_to_trace(called_func)  

現(xiàn)在我們實(shí)現(xiàn)一個(gè) Trace 的子類,在這個(gè)子類中增加 callback 和 doreport 這兩個(gè)方法。callback 方法將在每一個(gè)操作之后被調(diào)用。doreport 方法將我們收集到的信息打印出來(lái)。

這是一個(gè)偽函數(shù)追蹤器實(shí)現(xiàn):

    class DummyTrace(Trace):  
        def __init__(self, func):  
            self.func = func  
            self.data = collections.OrderedDict()  
            self.last_frame = None  
            self.known_frame = []  
            self.report = []  

        def callback(self, stack, frame, exc):  
             if frame not in self.known_frame:  
                self.known_frame.append(frame)  
                self.report.append(" === Entering New Frame {0} ({1})     ===".format(frame.f_code.co_name, id(frame)))  
                self.last_frame = frame  
            if frame != self.last_frame:  
                self.report.append(" === Returning to Frame {0}     {1}===".format(frame.f_code.co_name, id(frame)))  
                self.last_frame = frame  

            self.report.append(str(stack))  
            instr = FrameAnalyser(frame).next_instr()  
            offset = str(instr.offset).rjust(8)  
            opname = str(instr.opname).ljust(20)  
            arg = str(instr.arg).ljust(10)  
            self.report.append("{0}  {1} {2} {3}".format(offset,    opname, arg, instr.argval))  
            self.do_auto_follow(stack, frame)  

        def do_report(self):  
            print("
".join(self.report))  

這里有一些實(shí)現(xiàn)的例子和使用方法。格式有些不方便觀看,畢竟我并不擅長(zhǎng)于搞這種對(duì)用戶友好的報(bào)告的事兒。

例1自動(dòng)追蹤堆棧信息和已經(jīng)執(zhí)行的指令

例2上下文管理

遞推式構(gòu)造列表(List Comprehensions)的追蹤示例。

例3偽追蹤器的輸出

例4輸出收集的堆棧信息

總結(jié)

這個(gè)小項(xiàng)目是一個(gè)了解 Python 底層的良好途徑,包括解釋器的 main loop,Python 實(shí)現(xiàn)的 C 代碼編程、Python 字節(jié)碼。通過(guò)這個(gè)小工具我們可以看到 Python 一些有趣構(gòu)造函數(shù)的字節(jié)碼行為,例如生成器、上下文管理和遞推式構(gòu)造列表。

這里是這個(gè)小項(xiàng)目的完整代碼。更進(jìn)一步的,我們還可以做的是修改我們所追蹤的函數(shù)的堆棧。我雖然不確定這個(gè)是否有用,但是可以肯定是這一過(guò)程是相當(dāng)有趣的。


本文作者系OneAPM工程師編譯整理。想閱讀更多技術(shù)文章,請(qǐng)?jiān)L問(wèn)OneAPM官方技術(shù)博客。

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://m.specialneedsforspecialkids.com/yun/45364.html

相關(guān)文章

  • PHPer書(shū)單

    摘要:想提升自己,還得多看書(shū)多看書(shū)多看書(shū)下面是我收集到的一些程序員應(yīng)該看得書(shū)單及在線教程,自己也沒(méi)有全部看完。共勉吧當(dāng)然,如果你有好的書(shū)想分享給大家的或者覺(jué)得書(shū)單不合理,可以去通過(guò)進(jìn)行提交。講師溫銘,軟件基金會(huì)主席,最佳實(shí)踐作者。 想提升自己,還得多看書(shū)!多看書(shū)!多看書(shū)!下面是我收集到的一些PHP程序員應(yīng)該看得書(shū)單及在線教程,自己也沒(méi)有全部看完。共勉吧!當(dāng)然,如果你有好的書(shū)想分享給大家的或者...

    jimhs 評(píng)論0 收藏0
  • [] Node.js 架構(gòu)概覽

    摘要:文件系統(tǒng)請(qǐng)求和相關(guān)請(qǐng)求都會(huì)放進(jìn)這個(gè)線程池處理其他的請(qǐng)求,如網(wǎng)絡(luò)平臺(tái)特性相關(guān)的請(qǐng)求會(huì)分發(fā)給相應(yīng)的系統(tǒng)處理單元參見(jiàn)設(shè)計(jì)概覽。 譯者按:在 Medium 上看到這篇文章,行文脈絡(luò)清晰,闡述簡(jiǎn)明利落,果斷點(diǎn)下翻譯按鈕。第一小節(jié)背景鋪陳略啰嗦,可以略過(guò)。剛開(kāi)始我給這部分留了個(gè) blah blah blah 直接翻后面的,翻完之后回頭看,考慮完整性才把第一節(jié)給補(bǔ)上。接下來(lái)的內(nèi)容干貨滿滿,相信對(duì) N...

    antyiwei 評(píng)論0 收藏0
  • PyTips 0x13 - Python 線程與協(xié)程(2)

    摘要:項(xiàng)目地址我之前翻譯了協(xié)程原理這篇文章之后嘗試用了模式下的協(xié)程進(jìn)行異步開(kāi)發(fā),確實(shí)感受到協(xié)程所帶來(lái)的好處至少是語(yǔ)法上的。 項(xiàng)目地址:https://git.io/pytips 我之前翻譯了Python 3.5 協(xié)程原理這篇文章之后嘗試用了 Tornado + Motor 模式下的協(xié)程進(jìn)行異步開(kāi)發(fā),確實(shí)感受到協(xié)程所帶來(lái)的好處(至少是語(yǔ)法上的:D)。至于協(xié)程的 async/await 語(yǔ)法是如...

    史占廣 評(píng)論0 收藏0
  • 只看不敲,神也學(xué)不好C---------計(jì)算機(jī)經(jīng)典書(shū)籍經(jīng)驗(yàn)分享

    摘要:學(xué)單片機(jī)多去官網(wǎng)上查資料,下載手冊(cè),像我入門(mén)的單片機(jī)經(jīng)常去官網(wǎng),還有學(xué)的系列板子,公司的官網(wǎng)的官方例程給的很詳細(xì),在英文視角閱讀對(duì)你大有益處。 目錄 1.C語(yǔ)言經(jīng)典 2.單片機(jī)系列 3.Python方面 4.嵌入式LWip協(xié)議 5.Android 6.C++經(jīng)典書(shū)籍 7.Linux開(kāi)發(fā) ...

    FleyX 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<