欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

從底層簡(jiǎn)析Python程序的執(zhí)行過(guò)程

 更新時(shí)間:2015年06月03日 10:43:59   作者:hakril  
這篇文章主要介紹了從底層簡(jiǎn)析Python程序的執(zhí)行過(guò)程,包括注入操作碼和封裝程序等解釋器執(zhí)行層面的知識(shí),需要的朋友可以參考下

最近我在學(xué)習(xí) Python 的運(yùn)行模型。我對(duì) Python 的一些內(nèi)部機(jī)制很是好奇,比如 Python 是怎么實(shí)現(xiàn)類(lèi)似 YIELDVALUE、YIELDFROM 這樣的操作碼的;對(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)做的話(huà)對(duì)于理解字節(jié)碼的執(zhí)行和堆棧的變化還是缺少點(diǎn)什么。GDB 是個(gè)好選擇,但是我懶,而且只想使用一些比較高階的接口寫(xiě)點(diǎn) Python 代碼來(lái)完成這件事。

所以呢,我的目標(biāo)就是創(chuàng)建一個(gè)字節(jié)碼級(jí)別的追蹤 API,類(lèi)似 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í)符 enter 和 exit。

我們可以看到這兩標(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 代碼就夠了?,F(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__ 
  <code object <lambda> at 0x7f481fd88390, file "<stdin>", line 1>   
  >>> x.__code__.co_name 
  '<lambda>' 
  >>> x.__code__.co_name = 'truc' 
  Traceback (most recent call last): 
   File "<stdin>", line 1, in <module> 
  AttributeError: readonly attribute 
  >>> x.__code__.co_consts = ('truc',) 
  Traceback (most recent call last): 
   File "<stdin>", line 1, in <module> 
  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)用來(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)用起來(lái)很方便,解決了上面提到的 code object 不可變的問(wèn)題。

  >>> x = lambda y : 2 
  >>> m = MutableCodeObject(x.__code__) 
  >>> m 
  <new_code.MutableCodeObject object at 0x7f3f0ea546a0> 
  >>> m.co_consts 
  [None, 2] 
  >>> m.co_consts[1] = '3' 
  >>> m.co_name = 'truc' 
  >>> m.get_code() 
  <code object truc at 0x7f3f0ea2bc90, file "<stdin>", 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 <([], <frame object at 0x7fde9eaebdb0>)> 
  WOOT 
  op_target called with args <([None], <frame object at  0x7fde9eaebdb0>)> 

看起來(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 的分支一共有兩種:

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

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

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

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

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

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

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

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

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

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

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

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

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

  # Helper 
  def bytecode_to_string(bytecode): 
    if bytecode.arg is not None: 
      return struct.pack("<Bh", bytecode.opcode, bytecode.arg)  
    return struct.pack("<B", bytecode.opcode) 

  # Dummy class for bytecode_to_string 
  class DummyInstr: 
    def __init__(self, opcode, arg): 
      self.opcode = opcode 
      self.arg = arg 

  def insert_op_debug(code, offset): 
    opcode_jump_rel = ['FOR_ITER', 'JUMP_FORWARD', 'SETUP_LOOP',   'SETUP_WITH', 'SETUP_EXCEPT', 'SETUP_FINALLY'] 
    opcode_jump_abs = ['POP_JUMP_IF_TRUE', 'POP_JUMP_IF_FALSE',   'JUMP_ABSOLUTE'] 
    res_codestring = b"" 
    inserted = False 
    for instr in dis.Bytecode(code): 
      if instr.offset == offset: 
        res_codestring += b"\x00" 
        inserted = True 
      if instr.opname in opcode_jump_rel and not inserted:   #relative jump are always forward 
        if offset < instr.offset + 3 + instr.arg: # inserted   beetwen jump and dest: add 1 to dest (3 for size) 
           #If equal: jump on DEBUG_OP to get info before   exec instr 
          res_codestring +=   bytecode_to_string(DummyInstr(instr.opcode, instr.arg + 1)) 
          continue 
      if instr.opname in opcode_jump_abs: 
        if instr.arg > 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) 
  [] 
  [] 
  [<class 'range'>] 
  [10, <class 'range'>] 
  [range(0, 10)] 
  [<range_iterator object at 0x7f1349afab80>] 
  [0, <range_iterator object at 0x7f1349afab80>] 
  [<range_iterator object at 0x7f1349afab80>] 
  [2, <range_iterator object at 0x7f1349afab80>] 
  [0, 2, <range_iterator object at 0x7f1349afab80>] 
  [False, <range_iterator object at 0x7f1349afab80>] 
  [<range_iterator object at 0x7f1349afab80>] 
  [1, <range_iterator object at 0x7f1349afab80>] 
  [<range_iterator object at 0x7f1349afab80>] 
  [2, <range_iterator object at 0x7f1349afab80>] 
  [1, 2, <range_iterator object at 0x7f1349afab80>] 
  [False, <range_iterator object at 0x7f1349afab80>] 
  [<range_iterator object at 0x7f1349afab80>] 
  [2, <range_iterator object at 0x7f1349afab80>] 
  [<range_iterator object at 0x7f1349afab80>] 
  [2, <range_iterator object at 0x7f1349afab80>] 
  [2, 2, <range_iterator object at 0x7f1349afab80>] 
  [True, <range_iterator object at 0x7f1349afab80>] 
  [<range_iterator object at 0x7f1349afab80>] 
  [] 
  [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ī)制的類(lèi):

  •     改變函數(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 的子類(lèi),在這個(gè)子類(lèi)中增加 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("\n".join(self.report)) 

這里有一些實(shí)現(xiàn)的例子和使用方法。格式有些不方便觀看,畢竟我并不擅長(zhǎng)于搞這種對(duì)用戶(hù)友好的報(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)有趣的。

相關(guān)文章

  • 深入Mysql字符集設(shè)置 圖文版

    深入Mysql字符集設(shè)置 圖文版

    在mysql客戶(hù)端與mysql服務(wù)端之間,存在著一個(gè)字符集轉(zhuǎn)換器
    2012-09-09
  • Mysql事務(wù)索引知識(shí)匯總

    Mysql事務(wù)索引知識(shí)匯總

    這篇文章主要介紹了Mysql事務(wù)索引知識(shí)匯總,mysql事務(wù)是用于處理操作量大、復(fù)雜性高的數(shù)據(jù),索引能加快數(shù)據(jù)庫(kù)的查詢(xún)速度并高效獲取指定的數(shù)據(jù),下文相關(guān)詳細(xì)內(nèi)容,需要的小伙伴可以參考一下
    2022-03-03
  • mysql完整備份時(shí)過(guò)濾掉某些庫(kù)的方法

    mysql完整備份時(shí)過(guò)濾掉某些庫(kù)的方法

    下面小編就為大家?guī)?lái)一篇mysql完整備份時(shí)過(guò)濾掉某些庫(kù)的方法。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧
    2017-03-03
  • MySQL普通表如何轉(zhuǎn)換成分區(qū)表

    MySQL普通表如何轉(zhuǎn)換成分區(qū)表

    分表和表分區(qū)的目的就是減少數(shù)據(jù)庫(kù)的負(fù)擔(dān),提高數(shù)據(jù)庫(kù)的效率,通常點(diǎn)來(lái)講就是提高表的增刪改查效率,下面這篇文章主要給大家介紹了關(guān)于MySQL普通表如何轉(zhuǎn)換成分區(qū)表的相關(guān)資料,需要的朋友可以參考下
    2022-05-05
  • MySQL 5.7臨時(shí)表空間如何玩才能不掉坑里詳解

    MySQL 5.7臨時(shí)表空間如何玩才能不掉坑里詳解

    這篇文章主要給大家介紹了關(guān)于MySQL 5.7臨時(shí)表空間如何玩才能不掉坑里的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用mysql具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起看看吧
    2018-09-09
  • MySQL 游標(biāo)的定義與使用方式

    MySQL 游標(biāo)的定義與使用方式

    這篇文章主要介紹了MySQL 游標(biāo)的定義與使用方式,幫助大家更好的理解和使用MySQL,感興趣的朋友可以了解下
    2021-01-01
  • sql腳本函數(shù)編寫(xiě)postgresql數(shù)據(jù)庫(kù)實(shí)現(xiàn)解析

    sql腳本函數(shù)編寫(xiě)postgresql數(shù)據(jù)庫(kù)實(shí)現(xiàn)解析

    這篇文章主要介紹了sql腳本函數(shù)編寫(xiě)postgresql數(shù)據(jù)庫(kù)實(shí)現(xiàn)解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2019-09-09
  • mysql5.6.zip格式壓縮版安裝圖文教程

    mysql5.6.zip格式壓縮版安裝圖文教程

    這篇文章主要為大家詳細(xì)介紹了mysql5.6.zip格式壓縮版安裝圖文教程,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2018-12-12
  • MySQL示例講解數(shù)據(jù)庫(kù)約束以及表的設(shè)計(jì)

    MySQL示例講解數(shù)據(jù)庫(kù)約束以及表的設(shè)計(jì)

    約束主要完成對(duì)數(shù)據(jù)的檢驗(yàn),保證數(shù)據(jù)庫(kù)數(shù)據(jù)的完整性;如果有相互依賴(lài)數(shù)據(jù),保證該數(shù)據(jù)不被刪除,本篇文章教你如何給表設(shè)置約束及設(shè)計(jì)
    2022-06-06
  • 關(guān)于mysql查詢(xún)字符集不匹配問(wèn)題的解決方法

    關(guān)于mysql查詢(xún)字符集不匹配問(wèn)題的解決方法

    這篇文章主要給大家介紹了關(guān)于mysql查詢(xún)字符集不匹配問(wèn)題的解決方法,文中通過(guò)示例代碼給大家介紹的非常詳細(xì),對(duì)同樣遇到這個(gè)問(wèn)題的朋友們具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面跟著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。
    2017-08-08

最新評(píng)論