• 浮生半日:探究Python字节码


     好吧!“人生苦短,请用Python”,作为python爱好者以及安全从业者,而且最近也碰到了一些这方面的问题,懂点python字节码还是很有必要的。

     Python是一门解释性语言,它的具体工作流程如下:

        1:编译,形成.pyc或.pyo后缀的语言

        2:放入解释器,解释器执行字节流(opecode)

     和java字节码一样,他们都是基于栈进行解释的。首先,先来看对pyc文件进行一个直观的理解:

    一:直面pyc文件

      pyc文件的生成一般用于加快Python的解释速度,运行时,如果pyc的编译时间晚于py的修改时间,会直接运行pyc文件。所以一般需要以module形式加载时,才会直接生成,生成pyc文件脚本如下:

    import imp
    import sys
    def generate_pyc(name):
        fp, pathname, description = imp.find_module(name)
        try:
            imp.load_module(name, fp, pathname, description)    
        finally:
            if fp:
                fp.close()
    if __name__ == '__main__':
        t = raw_input()
        generate_pyc(t)

      通过load_module形式进行加载,来间接获取pyc文件。

    二:分析pyc文件

      pyc文件的格式分为两种

      3.4以前前四个字节为magic,这个和python版本相关,然后四个字节为编译时间,后面为code_type(PycodeObject)。而在3.4及以后则在编译时间之后添加一个filesize。下面是CPython中对code_type数据结构的描述

    
    
    #define OFF(x) offsetof(PyCodeObject, x)
    static PyMemberDef code_memberlist[] = {
        {"co_argcount",     T_INT,          OFF(co_argcount),       READONLY},
        {"co_nlocals",      T_INT,          OFF(co_nlocals),        READONLY},
        {"co_stacksize",T_INT,              OFF(co_stacksize),      READONLY},
        {"co_flags",        T_INT,          OFF(co_flags),          READONLY},
        {"co_code",         T_OBJECT,       OFF(co_code),           READONLY},
        {"co_consts",       T_OBJECT,       OFF(co_consts),         READONLY},
        {"co_names",        T_OBJECT,       OFF(co_names),          READONLY},
        {"co_varnames",     T_OBJECT,       OFF(co_varnames),       READONLY},
        {"co_freevars",     T_OBJECT,       OFF(co_freevars),       READONLY},
        {"co_cellvars",     T_OBJECT,       OFF(co_cellvars),       READONLY},
        {"co_filename",     T_OBJECT,       OFF(co_filename),       READONLY},
        {"co_name",         T_OBJECT,       OFF(co_name),           READONLY},
        {"co_firstlineno", T_INT,           OFF(co_firstlineno),    READONLY},
        {"co_lnotab",       T_OBJECT,       OFF(co_lnotab),         READONLY},
        {NULL}      /* Sentinel */
    };

      下面是一个基于python2.7的pyc文件例子的部分截图

                          

      解析pyc文件可以得到,如下:

    magic 03f30d0a
    moddate aa813e59 (Mon Jun 12 19:57:30 2017)
    code
       argcount 0
       nlocals 0
       stacksize 1
       flags 0040
       code 640000474864010053      
       consts
          'hello world!'
          None
       names ()
       varnames ()
       freevars ()
       cellvars ()
       filename 'C:\Users\Administrator\Desktop\test3.py'
       name '<module>'
       firstlineno 1
       lnotab 

      本文着重讲解的是co_code,也就是opcode。

    三:解读opcode

      opcode代码在理解上还是很简单的,想要得到某个函数或者module的话可以直接使用dis.dis()或者dis.disassemble()函数,这里先使用dis.dis()函数,直接观察函数的opcode。下面是一个简单的python脚本:

    import dis
    def test1():
        a = "hello"
        b = " "
        c = "world"
        d = a +b+c
        print d
        
    print dis.dis(test1)

      可以得到test1函数的opcode代码

      3           0 LOAD_CONST               1 ('hello')      //'hello'压栈
                  3 STORE_FAST               0 (a)             //'hell0'出栈,同时local['a'] = 'hello'
    
      4           6 LOAD_CONST               2 (' ')
                  9 STORE_FAST               1 (b)
    
      5          12 LOAD_CONST               3 ('world')
                 15 STORE_FAST               2 (c)
    
      6          18 LOAD_FAST                0 (a)           //将local['a']压栈
                 21 LOAD_FAST                1 (b)           //将local['b']压栈
                 24 BINARY_ADD                               //栈中a,b相加,结果压栈
                 25 LOAD_FAST                2 (c)           
                 28 BINARY_ADD          
                 29 STORE_FAST               3 (d)
    
      7          32 LOAD_FAST                3 (d)
                 35 PRINT_ITEM          
                 36 PRINT_NEWLINE       
                 37 LOAD_CONST               0 (None)
                 40 RETURN_VALUE        
    None

      源码和opcode对照并结合注释,理解起来还是很方便的。第一列是源代码的行数,第二列是字节相对于第一个字节的偏移,第三个则是命令,第四个是命令参数。opcode的格式如下:

                                    

       想要彻底理解上面的代码,必须先理解python基于栈的运行机制,python的运行是单纯模拟cpu运行的机制,看一下它的堆结构

    typedef struct _frame {
        PyObject_VAR_HEAD
        struct _frame *f_back;    /* 调用者的帧 */
        PyCodeObject *f_code;     /* 帧对应的字节码对象 */
        PyObject *f_builtins;     /* 内置名字空间 */
        PyObject *f_globals;      /* 全局名字空间 */
        PyObject *f_locals;       /* 本地名字空间 */
        PyObject **f_valuestack;  /* 运行时栈底 */
        PyObject **f_stacktop;    /* 运行时栈顶 */
        …….

      可以利用sys.getFrame来得到运行时的堆栈状态

    {'a': 'hello', 'c': 'world', 'frame': <frame object at 0x0000000002DEA3A8>, 'b': ' ', 'd': 'hello '}
    {'test1': <function test1 at 0x00000000032B9908>, '__builtins__': <module '__builtin__' (built-in)>, '__file__': 'C:\Users\Administrator\Desktop\test3.py', '__package__': None, 'sys': <module 'sys' (built-in)>, '__name__': '__main__', '__doc__': None, 'dis': <module 'dis' from 'C:Python27libdis.pyc'>}
    {'test1': <function test1 at 0x00000000032B9908>, '__builtins__': <module '__builtin__' (built-in)>, '__file__': 'C:\Users\Administrator\Desktop\test3.py', '__package__': None, 'sys':

    四:常见语句以及对应opcode

      1.判断语句

    def test2(t):if t > 3:
            print "OK!"

      对应

      6           0 LOAD_FAST                0 (t)
                  3 LOAD_CONST               1 (3)
                  6 COMPARE_OP               4 (>)
                  9 POP_JUMP_IF_FALSE       20
    
      7          12 LOAD_CONST               2 ('OK!')
                 15 PRINT_ITEM          
                 16 PRINT_NEWLINE       
                 17 JUMP_FORWARD             0 (to 20)
            >>   20 LOAD_CONST               0 (None)
                 23 RETURN_VALUE        
    None

       2.循环语句

    def test3():
        i = 0
        while t < 10:
            t += i
            i+=1

      对应

      5           0 LOAD_CONST               1 (0)
                  3 STORE_FAST               0 (i)
    
      6           6 SETUP_LOOP              36 (to 45)
            >>    9 LOAD_FAST                1 (t)
                 12 LOAD_CONST               2 (10)
                 15 COMPARE_OP               0 (<)
                 18 POP_JUMP_IF_FALSE       44
    
      7          21 LOAD_FAST                1 (t)
                 24 LOAD_FAST                0 (i)
                 27 INPLACE_ADD         
                 28 STORE_FAST               1 (t)
    
      8          31 LOAD_FAST                0 (i)
                 34 LOAD_CONST               3 (1)
                 37 INPLACE_ADD         
                 38 STORE_FAST               0 (i)
                 41 JUMP_ABSOLUTE            9
            >>   44 POP_BLOCK           
            >>   45 LOAD_CONST               0 (None)
                 48 RETURN_VALUE     

      3.调用操作

    def test4(a,b):
        print a+b
    
    def test3():
        test4(3,4)

      对应

     7           0 LOAD_GLOBAL              0 (test4)
                  3 LOAD_CONST               1 (3)
                  6 LOAD_CONST               2 (4)
                  9 CALL_FUNCTION            2
                 12 POP_TOP             
                 13 LOAD_CONST               0 (None)
                 16 RETURN_VALUE    
  • 相关阅读:
    关于重载和重写的区别
    UML的关联(Association), 聚合(Aggregation), 组合(Composition)区别
    解析CSS加密技术之“障眼法”
    ASP.NET中MEMCACHED
    新建项目的无法应用已有项目
    C# 判断两张图片是否一致的快速方法
    压力测试中需要掌握的几个基本概念
    软件测试Web数据分析工具HttpWatch安装
    在SQL Server实现最短路径的搜索
    从算法入手讲解如何在SQL Server中实现最优最简
  • 原文地址:https://www.cnblogs.com/0xJDchen/p/6995204.html
Copyright © 2020-2023  润新知