您的位置:68399皇家赌场 > 域名注册 > 68399皇家赌场:从最底层简析Python程序的实施进程

68399皇家赌场:从最底层简析Python程序的实施进程

发布时间:2019-05-22 09:21编辑:域名注册浏览(141)

    再有一件事情须要小心的是在那篇小说所指的情形中 code object 是不可变的:

    从尾巴部分简析Python程序的实践进度,底层简析python程序

    近年笔者在上学 Python 的运作模型。笔者对 Python 的一些里头机制卓殊惊叹,比如Python 是怎么落到实处类似 YIELDVALUE、YIELDFROM 那样的操作码的;对于 递推式构造列表(List Comprehensions)、生成器说明式(generator expressions)以及此外部分风趣的 Python 性情是怎么编写翻译的;从字节码的层面来看,当极度抛出的时候都发生了什么业务。翻阅 CPython 的代码对于解答那些难点自然是很有帮扶的,但笔者依旧感到以如此的方法来做的话对于掌握字节码的举行和库房的变型还是不够点什么。GDB 是个好选取,但是作者懒,而且只想接纳一些比较高阶的接口写点 Python 代码来造成那件事。

    所以啊,笔者的对象便是成立一个字节码级其他跟踪 API,类似 sys.setrace 所提供的那么,但针锋相对来讲会有越来越好的粒度。那丰盛操练了自身编写 Python 完结的 C 代码的编码技巧。大家所供给的有如下几项,在那篇文章中所用的 Python 版本为 三.5。

    •     3个新的 Cpython 解释器操作码
    •     一种将操作码注入到 Python 字节码的方法
    •     一些用来拍卖操作码的 Python 代码

    1个新的 Cpython 操作码
    新操作码:DEBUG_OP

    以此新的操作码 DEBUG_OP 是本身首先次尝试写 CPython 完毕的 C 代码,我将尽或者的让它保持简单。 我们想要实现的目标是,当大家的操作码被实行的时候作者能有壹种格局来调用一些 Python 代码。同时,咱们也想能够追踪一些与执行上下文有关的多寡。我们的操作码会把那么些新闻当作参数字传送递给大家的回调函数。通过操作码能鉴定区别出的有用音信如下:

    •     客栈的内容
    •     执行 DEBUG_OP 的帧对象消息

    于是啊,我们的操作码必要做的业务是:

    •     找到回调函数
    •     创制三个包涵货仓内容的列表
    •     调用回调函数,并将涵盖旅馆内容的列表和脚下帧作为参数字传送递给它

    听上去挺简单的,今后上马初始吧!注解:下边全体的解释表明和代码是透过了大气段错误调节和测试之后计算猎取的下结论。首先要做的是给操作码定义一个名字和呼应的值,因而我们需求在 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 
    

    那部分办事就完了了,以往大家去编写操作码真正行事的代码。
    实现 DEBUG_OP

    在设想怎么样兑现DEBUG_OP在此以前大家需求驾驭的是 DEBUG_OP 提供的接口将长什么。 具备1个得以调用别的代码的新操作码是非常酷眩的,可是究竟它将调用哪些代码捏?这几个操作码如何找到回调函数的捏?我选择了一种最简易的方法:在帧的大局区域写死函数名。那么难点就改成了,作者该怎么从字典中找到3个永远的 C 字符串?为了回应这些主题素材大家来探望在 Python 的 main loop 中运用到的和上下文管理有关的标志符 enter 和 exit。

    大家得以看看那两标记符被选择在操作码 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; 
    

    最近,看一眼宏 _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)
    

    啊,注释部分已经认证得很明亮了。通过1番寻找,大家发掘了能够用来从字典找稳固字符串的函数 _PyDict_GetItemId,所以大家操作码的探索部分的代码便是长这么滴。

       /** 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(); 
      } 
    

    为了有利于清楚,对那壹段代码做一些验证:

    •     f 是近期的帧,f->f_globals 是它的全局区域
    •     假如我们从没找到 op_target,大家将会检查这几个极度是或不是 KeyError
    •     goto error; 是一种在 main loop 中抛出极度的主意
    •     PyErr_Clear() 抑制了最近特别的抛出,而 DISPATCH() 触发了下二个操作码的实践

    下一步正是收罗我们想要的库房消息。

      /** 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); 
      } 
    

    末尾一步正是调用大家的回调函数!我们用 call_function 来化解那件事,大家经过钻研操作码 CALL_FUNCTION 的落实来学习怎么使用 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(); 
      } 
    

    有了上边这一个信息,大家总算得以鼓捣出二个操作码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(); 
      }
    

    在编辑 CPython 完成的 C 代码方面自己真的未有何经验,有不小可能率本身漏掉了些细节。即使您有怎样建议还请你勘误,作者希望你的反映。

    编译它,成了!

    万事看起来很顺遂,可是当大家尝试去行使我们定义的操作码 DEBUG_OP 的时候却难倒了。自从 二〇一〇 年之后,Python 使用预先写好的 goto(你也得以从 这里收获更加的多的消息)。故,大家须求更新下 goto jump table,大家在 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, 
        /** ... **/ 
    

    那就完事了,我们后天就有了1个方可干活的新操作码。唯1的难点就是那货纵然存在,然而从未被人调用过。接下来,大家将DEBUG_OP注入到函数的字节码中。
    在 Python 字节码中流入操作码 DEBUG_OP

    有诸多艺术得以在 Python 字节码中注入新的操作码:

    •     使用 peephole optimizer, Quarkslab就是那样干的
    •     在生成字节码的代码中动些小动作
    •     在运营时一贯退换函数的字节码(那正是大家就要干的事情)

    为了创设出1个新操作码,有了上边的那一批 C 代码就够了。现在让大家回到原点,开端精通奇异以致美妙的 Python!

    咱俩就要做的事儿有:

    •     获得我们想要追踪函数的 code object
    •     重写字节码来注入 DEBUG_OP
    •     将新生成的 code object 替换回去

    和 code object 有关的小贴士

    假设您从未听闻过 code object,这里有2个简短的介绍网路上也可能有一部分有关的文书档案可供查阅,能够一贯Ctrl F 查找 code object

    还有一件事情须要留意的是在那篇文章所指的景况中 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 
    

    但是并非顾虑,大家将会找到办法绕过那么些题指标。
    选用的工具

    为了修改字节码大家需求有的工具:

    •     dis模块用来反编写翻译和分析字节码
    •     dis.BytecodePython 三.4猛增的三个特点,对于反编写翻译和剖判字节码特别有用
    •     一个力所能致轻易修改 code object 的主意

    用 dis.Bytecode 反编写翻译 code object 能告诉大家一些关于操作码、参数和上下文的音信。

      # 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,作者定义了二个比极小的类用来复制 code object,同时能够按我们的供给修改相应的值,然后再一次生成二个新的 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) 
    

    那一个类用起来很有利,解决了地点提到的 code object 不可变的标题。

      >>> 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> 
    

    测试大家的新操作码

    我们今日具有了注入 DEBUG_OP 的有所工具,让大家来证实下大家的兑现是不是可用。大家将我们的操作码注入到三个最轻巧易行的函数中:

      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 
      [email protected] ~/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>)> 
    

    看起来它成功了!有1行代码供给说澳优(Ausnutria Hyproca)下 new_nop_code.co_stacksize = 3

    •     co_stacksize 代表 code object 所急需的货仓的深浅
    •     操作码DEBUG_OP往货仓中加进了叁项,所以大家需求为那些充实的项预留些空间

    现今大家能够将大家的操作码注入到每三个 Python 函数中了!
    重写字节码

    正如笔者辈在地点的例证中所看到的那样,重写 Pyhton 的字节码仿佛 so easy。为了在每2个操作码之间注入我们的操作码,大家须要得到每3个操作码的偏移量,然后将大家的操作码注入到这个地点上(把我们操作码注入到参数上是有弊端大大滴)。这一个偏移量也很轻易得到,使用 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? 
    

    基于上边的事例,有人恐怕会想大家的 insert_op_debug 会在钦点的偏移量扩大1个"x00",那尼玛是个坑啊!大家率先个 DEBUG_OP 注入的例子中被注入的函数是尚未其余的道岔的,为了能够达成周全贰个函数注入函数 insert_op_debug 大家需求思量到存在分支操作码的情事。

    Python 的支行一共有二种:

       (一) 相对分支:看起来是近乎那样子的 Instruction_Pointer = argument(instruction)

        (二)相对分支:看起来是类似那样子的 Instruction_Pointer = argument(instruction)

                   相对分支总是向前的

    咱俩意在那么些分支在我们插入操作码之后如故能够健康办事,为此我们必要修改部分指令参数。以下是其逻辑流程:

       (一) 对于每种在插入偏移量之前的相对分支来讲

            假使目的地址是严格大于我们的插入偏移量的话,将下令参数扩展 一

            若是相等,则无需追加 1就能够在跳转操作和目的地址之间进行我们的操作码DEBUG_OP

            假诺低于,插入我们的操作码的话并不会潜移默化到跳转操作和对象地点之间的相距

       (贰) 对于 code object 中的每四个纯属分支来说

            若是指标地方是从严大于大家的插入偏移量的话,将指令参数扩展 一

            就算相等,那么无需此外改动,理由和相对分支部分是一模2样的

            要是低于,插入大家的操作码的话并不会影响到跳转操作和指标地址之间的距离

    下边是促成:

      # 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] 
    

    甚好!今后大家驾驭了怎么获取仓库音信和 Python 中每八个操作对应的帧消息。上边结果所显示的结果近期来讲并不是很实用。在最后一片段中让我们对流入做进一步的卷入。
    增加 Python 封装

    正如你所旁观标,全部的平底接口都以好用的。大家最后要做的一件事是让 op_target 尤其方便使用(那1部分相对来说相比较空虚一些,终归以笔者之见那不是1体项目中最棒玩的一部分)。

    首先大家来看一下帧的参数所能提供的新闻,如下所示:

    •     f_code当前帧将实行的 code object
    •     f_lasti当前的操作(code object 中的字节码字符串的目录)

    通过大家的拍卖大家得以摸清 DEBUG_OP 之后要被实践的操作码,这对我们聚合数据并展示是1对壹有效的。

    新建1个用来追踪函数内部机制的类:

    •     改动函数本人的 co_code
    •     设置回调函数作为 op_debug 的靶子函数

    只要我们领略下二个操作,我们就能够分析它并修改它的参数。譬世尊讲我们得以追加三个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) 
    

    到现在大家完结2个 Trace 的子类,在这么些子类中追加 callback 和 doreport 那五个章程。callback 方法将要每一个操作之后被调用。doreport 方法将我们采访到的消息打字与印刷出来。

    那是贰个伪函数追踪器实现:

      

     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)) 
    

    那边有局地贯彻的例子和利用方式。格式某个不便于探望,究竟本人并不擅长于搞这种对用户本身的告诉的事儿。

    •     例一自动追踪仓库消息和曾经实践的授命
    •     例2上下文管理

    递推式构造列表(List Comprehensions)的寻踪示例。

    •     例三伪追踪器的出口
    •     例4出口搜集的库房音信

    总结

    其一小品种是八个打听 Python 底层的美丽门路,包蕴解释器的 main loop,Python 完成的 C 代码编制程序、Python 字节码。通过这些小工具大家能够旁观 Python 一些风趣构造函数的字节码行为,比方生成器、上下文管理和递推式构造列表。

    这里是那些小项指标完整代码。更进一步的,大家还足以做的是修改大家所追踪的函数的商旅。我就算不鲜明这些是否有用,不过足以一定是那1进程是1对1风趣的。

    近期本人在念书 Python 的运营模型。小编对 Python 的某些里头机制非常惊讶,比如Python 是怎...

    • dis模块用来反编写翻译和解析字节码
    • dis.BytecodePython 3.四猛增的3个特点,对于反编写翻译和分析字节码特别有用
    • 一个力所能致轻易修改 code object 的办法

      

    为了方便清楚,对那壹段代码做一些表达:

    下一步正是采访我们想要的商旅新闻。

        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)  
    

    据他们说下面的例证,有人或者会想大家的 insert_op_debug 会在钦定的偏移量扩充一个"x00",这尼玛是个坑啊!咱们第3个 DEBUG_OP 注入的例证中被注入的函数是尚未此外的分层的,为了能够完毕周详3个函数注入函数 insert_op_debug 大家必要考虑到存在分支操作码的意况。

    二个新的 Cpython 操作码

    递推式构造列表(List Comprehensions)的寻踪示例。

    那边有局地贯彻的事例和利用办法。格式有个别不便于探望,终归自个儿并不擅长于搞这种对用户自个儿的报告的事儿。

    有了位置那些音信,我们终于可以鼓捣出一个操作码DEBUG_OP的文稿了:

    大家今后具有了注入DEBUG_OP的有所工具,让大家来表明下大家的兑现是不是可用。我们将大家的操作码注入到二个最简便易行的函数中:

    通过大家的管理大家得以摸清 DEBUG_OP 之后要被推行的操作码,那对大家聚合数据并出示是壹对一有效的。

    设若大家知道下一个操作,大家就足以深入分析它并修改它的参数。比方来讲我们得以追加1个auto-follow-called-functions的特性。

    让大家看一下意义如何:

    新操作码:DEBUG_OP

    在编排 CPython 完结的 C 代码方面自个儿真的并未有啥样经验,有非常大希望自身漏掉了些细节。假若您有怎样提出还请你校正,笔者梦想您的报告。

    近期,看一眼宏_Py_IDENTIFIER的定义

    为了修改字节码我们必要部分工具:

    以此新的操作码 DEBUG_OP 是笔者先是次尝试写 CPython 完成的 C 代码,小编将尽大概的让它保持简单。 我们想要实现的指标是,当大家的操作码被实践的时候笔者能有1种格局来调用一些 Python 代码。同时,大家也想能够追踪一些与实施上下文有关的多少。我们的操作码会把那几个音讯当作参数字传送递给大家的回调函数。通过操作码能识别出的有用消息如下:

    在怀恋怎么着贯彻DEBUG_OP此前大家必要明白的是 DEBUG_OP 提供的接口将长什么。 具有一个得以调用其余代码的新操作码是至极酷眩的,不过究竟它将调用哪些代码捏?那一个操作码怎么样找到回调函数的捏?作者选择了壹种最简易的法子:在帧的全局区域写死函数名。那么难点就变成了,笔者该怎么从字典中找到二个一定的 C 字符串?为了应对那些标题大家来探望在 Python 的 main loop 中运用到的和上下文管理有关的标记符 enter 和 exit。

    用dis.Bytecode反编写翻译 code bject 能告诉我们有的关于操作码、参数和上下文的消息。

    •     找到回调函数
    •     创立3个富含客栈内容的列表
    •     调用回调函数,并将富含货仓内容的列表和当下帧作为参数字传送递给它

    Python 的分支一共有三种:

       (壹) 相对分支:看起来是看似这样子的 Instruction_Pointer = argument(instruction)

    正文笔者系OneAPM程序猿编写翻译整理。OneAPM是中华基础软件领域的新生领军集团。专注于提供下一代应用质量管理软件和劳动,辅助企业用户和开拓者轻便完结:缓慢的程序代码和SQL语句的实时抓取。想阅读更加多工夫作品,请访问OneAPM官方技艺博客。

      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? 
    

    正如您所看到的,全数的最底层接口都是好用的。大家最终要做的1件事是让 op_target 特别方便使用(那有的相对来说相比较空虚一些,究竟在笔者眼里那不是整套项目中最风趣的部分)。

    可是不用忧郁,我们将会找到办法绕过这么些标题标。
    动用的工具

    为了修改字节码大家需求有个别工具:

    那一部分办事就成功了,未来大家去编写操作码真正行事的代码。
    实现 DEBUG_OP

        /** 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);  
        }  
    

    本条类用起来很方便,消除了上边提到的 code object 不可变的标题。

    • co_stacksize 表示 code object 所急需的库房的轻重
    • 操作码DEBUG_OP往货仓中追加了三项,所以我们须要为那个充实的项预留些空间

    当今大家落实三个 Trace 的子类,在这几个子类中追加 callback 和 doreport 那多个主意。callback 方法将在每3个操作之后被调用。doreport 方法将大家搜罗到的音讯打字与印刷出来。

    递推式构造列表(List Comprehensions)的追踪示例 。

    你也许感兴趣的作品:

    • 总括的多少个Python函数方法设计基准
    • Python使用ftplib完成简易FTP客户端的办法
    • Python中的深拷贝和浅拷贝详解

    测试大家的新操作码

    •     f 是现阶段的帧,f->f_globals 是它的大局区域
    •     即使我们并未有找到 op_target,我们将会检讨这几个非常是否 KeyError
    •     goto error; 是一种在 main loop 中抛出万分的艺术
    •     PyErr_Clear() 抑制了近些日子分外的抛出,而 DISPATCH() 触发了下叁个操作码的举办
    • 使用 peephole optimizer, Quarkslab就是那样干的
    • 在生成字节码的代码中动些小动作
    • 在运作时一贯更换函数的字节码(那正是大家将要干的事务)

    最后一步就是调用大家的回调函数!大家用 call_function 来解决那件事,大家通过钻研操作码 CALL_FUNCTION 的贯彻来上学怎么利用 call_function 。

    方方面面看起来很顺畅,不过当大家品尝去选择大家定义的操作码DEBUG_OP的时候却难倒了。自从 二零零六 年之后,Python 使用预先写好的 goto(你也得以从 这里获得更加多的音信)。故,大家须要立异下 goto jump table,大家在 Python/opcode_targets.h 中做如下修改。

    这里是以此小项指标完全代码。更进一步的,我们还足以做的是修改大家所追踪的函数的库房。作者尽管不鲜明这一个是还是不是有用,但是足以料定是这壹经过是十分有意思的。

        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)  
    

    本条小品种是三个打听 Python 底层的好好路子,包罗解释器的 main loop,Python 实现的 C 代码编制程序、Python 字节码。通过那么些小工具大家可以见见 Python 一些有趣构造函数的字节码行为,举个例子生成器、上下文管理和递推式构造列表。

    这里是以此小品种的1体化代码。更进一步的,大家还是能做的是修改我们所追踪的函数的酒馆。我就算不明确这么些是否有用,可是足以一定是那一进度是一定风趣的。

      /** 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(); 
      } 
    
        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  
    
    •     f_code当前帧将推行的 code object
    •     f_lasti当前的操作(code object 中的字节码字符串的目录)

    听上去挺轻巧的,以后起来入手吧!声明:上面全数的阐述阐明和代码是透过了汪洋段错误调节和测试之后总计获得的结论。首先要做的是给操作码定义三个名字和对应的值,因而大家需求在 Include/opcode.h中增多代码。

    大家将在做的事儿有:

    总结

    为了便利明白,对那1段代码做一些验证:

    听说上边包车型大巴事例,有人只怕会想大家的insert_op_debug会在钦命的偏移量扩展三个"x00",那尼玛是个坑啊!我们率先个DEBUG_OP注入的事例中被注入的函数是尚未任何的分层的,为了能够实现宏观二个函数注入函数insert_op_debug大家供给思索到存在分支操作码的情状。

      /** 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); 
      } 
    
        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>)>  
    

    测试大家的新操作码

    • 例3伪追踪器的出口
    • 例4出口搜集的饭馆新闻
      # 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) 
    

    甚好!今后大家知道了怎样赢得仓库新闻和 Python 中每1个操作对应的帧音信。下面结果所出示的结果日前来说并不是很实用。在最终1有个别中让大家对流入做越来越的包装。
    增加 Python 封装

            若是相等,则无需增添 1就能够在跳转操作和对象地址之间实行大家的操作码DEBUG_OP

    那部分做事就达成了,未来我们去编写操作码真正行事的代码。
    实现 DEBUG_OP

    这是1个伪函数追踪器实现:

    于是啊,大家的操作码须要做的业务是:

      /** 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 
    

    正如笔者辈在上边的事例中所看到的那么,重写 Pyhton 的字节码就像 so easy。为了在每三个操作码之间注入我们的操作码,我们要求获得每3个操作码的偏移量,然后将我们的操作码注入到那个岗位上(把我们操作码注入到参数上是有坏处大大滴)。这几个偏移量也很轻松得到,使用dis.Bytecode ,就好像这么 。

    本文由68399皇家赌场发布于域名注册,转载请注明出处:68399皇家赌场:从最底层简析Python程序的实施进程

    关键词: 68399皇家赌场 Python

上一篇:MySQL Semisynchronous Replication介绍

下一篇:没有了