Pythonプログラムの実行手順を下から簡単に分析する

24884 ワード

最近Pythonの運行モデルを勉強しています.私はPythonのいくつかの内部メカニズムに興味を持っています.例えば、PythonはどのようにYIELDVALUE、YIELDFROMのような操作コードを実現したのか.プッシュ構造リスト(List Comprehensions)、ジェネレータ式(generator expressions)、および他の興味深いPython特性がどのようにコンパイルされているかについて.バイトコードの面から見ると、異常が投げ出されたときに何が起こったのか.CPythonのコードをめくることはもちろんこれらの問題を解くのに役立ちますが、このような方法でやるとバイトコードの実行やスタックの変化を理解するのに何か足りないと思います.GDBは良い選択ですが、私は怠け者で、比較的高いレベルのインタフェースを使ってPythonコードを書いてこのことを完成したいだけです.
だから、私の目標はsysのようなバイトコードレベルの追跡APIを作成することです.setraceが提供するように、相対的により良い粒度があります.これは私がPythonで実現したCコードの符号化能力を十分に鍛えた.この記事で使用するPythonバージョンは3.5です.
  • 新しいCpythonインタプリタ操作コード
  • オペレーティングコードをPythonバイトコードに注入する方法
  • .
  • オペレーティングコードを処理するためのPythonコード
  • 新しい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が提供するインタフェースはどのようになりますか.他のコードを呼び出すことができる新しいオペレーティングコードを持つのはかなりクールですが、いったいどのコードを呼び出すのでしょうか.この操作コードはどのようにコールバック関数のつまみを見つけますか?フレームのグローバル領域に関数名を書き込む最も簡単な方法を選択しました.では問題は、辞書から固定的なC文字列を見つけるにはどうすればいいのでしょうか.この質問に答えるためにPythonのmain loopで使用されるコンテキスト管理に関連する識別子enterとexitを見てみましょう.
    この2つの識別子がオペレーティングコード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)
    
    

    ええと、注釈部分はもうはっきり説明されています.検索してみると、辞書から固定文字列を探すのに使える関数が見つかりました.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の時は失敗した.2008年以降、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, 
        /** ... **/ 
    
    

    これで終わりです.私たちは今、仕事ができる新しい操作コードを持っています.唯一の問題は、この品物は存在するが、呼び出されたことがないことだ.次はDEBUG_OPは関数のバイトコードに注入される.Pythonバイトコードに操作コードDEBUG_を注入OP
    Pythonバイトコードに新しいオペレーティングコードを注入する方法はたくさんあります.
  • peephole optimizerを使用して、Quarkslabはこのようにしています
  • バイトコードを生成するコードに手足を動かす
  • 実行時に関数のバイトコードを直接変更する
  • 新しいオペレーティングコードを作るためには、上のCコードの山があれば十分です.原点に戻り、奇妙で不思議なPythonを理解し始めましょう.
    私たちがしなければならないことは、
  • 関数を追跡したいcode object
  • を得る
  • は、DEBUG_を注入するために、セクションコードを再書き込みします.OP
  • 新しく生成されたcode objectを
  • に置き換える
    code objectに関するチラシ
    code objectを聞いたことがない場合は、簡単な紹介ネットワークにもいくつかの関連ドキュメントがあります.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__ 
       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 
    
    

    , 。

    •     dis
    •     dis.BytecodePython 3.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 
       
      >>> m.co_consts 
      [None, 2] 
      >>> m.co_consts[1] = '3' 
      >>> m.co_name = 'truc' 
      >>> m.get_code() 
      ", line 1> 
    
    

    DEBUG_OP , 。 :

    
      from new_code import MutableCodeObject 
    
      def op_target(*args): 
        print("WOOT") 
        print("op_target called with args ".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 )> 
    
    

    ! new_nop_code.co_stacksize += 3

    •     co_stacksize code object
    •     DEBUG_OP ,

    Python !

    , Pyhton so easy。 , , ( )。 , 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 "\x00", ! DEBUG_OP , insert_op_debug 。

    Python :

       (1) : Instruction_Pointer = argument(instruction)

        (2) : Instruction_Pointer += argument(instruction)

                  

    , 。 :

       (1)

            , 1

            , 1 DEBUG_OP

            ,

       (2) code object

            , 1

            , ,

            ,

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

    ! Python 。 。 。
    Python

    , 。 op_target ( , )。

    , :

    •     f_code code object
    •     f_lasti (code object )

    DEBUG_OP , 。

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

    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("
    ".join(self.report))

    。 , 。

    •     1
    •     2

    (List Comprehensions) 。

    •     3
    •     4

    Python , main loop,Python C 、Python 。 Python , 、 。

    。 , 。 , 。