当前位置: 首页 > 科技观察

字节码:分析Python执行的终极利器

时间:2023-03-17 00:17:29 科技观察

本文转载自微信公众号《Python中文社区》,作者龚庆奎。转载本文请联系Python中文社区公众号。一、什么是代码对象CodeObject(代码对象)封装了Python虚拟机的字节码和虚拟机执行相关的信息。字节码在Python虚拟机上可以称为汇编语言。学习代码对象有什么用?从它的定义来看,字节码是经过编译的Python代码。学习代码对象有助于我们了解Python虚拟机、编译过程、执行过程,更深入地了解Python语言的特点和难点。.在解决一些疑难杂症时,查看代码对象的字节码,往往有事半功倍的效果。2.探索代码对象Python程序是由代码块组成的。代码块可以是模块、函数或类、脚本文件,也可以是python-c'string'和exec('string')、eval('string')中的字符串内容。两种方法:fun.__code__获取函数的代码对象funbodycompile('sourcecode','','exec')获取代码块sourcecode的代码对象3.一个函数及其代码对象我们定义一个函数fun(a,b),执行简单的加法运算。deffun(a,b):returna+b查看其代码对象属性,我们重点关注co_开头的属性。forattrindir(fun.fun.__code__):ifattr.startswith('co_'):print("{attr}:\t{attrs}".format(attrs=getattr(attr=attr,fun.fun.__code__,attr)))输出为:co_argcount:2co_cellvars:()co_code:b'|\x00|\x01\x17\x00S\x00'co_consts:(None,)co_filename:G:\pythonCodeStudy\manuscript\0bytecode\fun.pyco_firstlineno:1co_flags:67co_freevars:()co_kwonlyargcount:0co_lnotab:b'\x00\x01'co_name:funco_names:()co_nlocals:2co_posonlyargcount:0co_stacksize:2co_varnames:('a','b')代码对象的属性含义fun函数体如下:co_argcount函数的形参个数,该属性只存在于函数类型代码块的代码对象中,其他类型代码块没有该属性。co_code字节码指令序列,字节码是由操作码opcode和参数opatg组成的序列。co_const常量列表,该列表包括以下内容:None函数的返回值,系统自带。从前到后计算所有文字常量:数字和字符串。内联函数代码对象代码对象。内部函数的qual_name常量,例如:outer..inner。co_name此函数的名称。co_varnames本函数的局部变量,不包括引用的自由变量,包括形式参数和内置函数名。co_names该函数使用的非局部变量,即全局变量和系统内置变量。co_nlocals该函数的局部变量个数。co_cellvars单元格变量。co_freevars自由变量。co_flag代码对象的类型,如协程、生成器等,其含义定义在include/code.h中。co_lnotab计算由字节码偏移量表示的源代码行号的字节序列。两个字节序列是一个单位,表示由两条源代码指令编译出的字节码偏移了多少。co_stacksize在执行字节码指令时,计算栈上的最大项数,这与函数参数个数有关。Python虚拟机是一个基于栈的机器,函数调用的每一步都会产生一个栈帧(stackframe)。每个栈帧包含一个计算栈和一个块栈。将所有参数压入计算栈,调用时出栈,计算完出结果,结束栈帧。在上面的例子中,我们没有详细解释co_cellvars和co_freevars,下面详细解释。3.1co_cellvars和co_freevarsco_cellvars和co_freevars是一个相对的概念。我们写一个嵌套函数来解释这两个概念:在函数outer中,定义了一个变量e,被inner嵌套函数inner引用;变量e在内部函数内部使用,但未在块中定义。defouter(o1,o2='o2'):e='enclose'definner(i1,i2='i2'):print(e)returnreturninnerprint(outer.__code__.co_cellvars)print(outer('i1').__code__.co_freevars)结果。('e',)('e',)所以,我们知道:co_cellvars是内部嵌套块(函数)引用的变量元组。外部函数创建一个特殊的单元格对象来存储这个变量,单元格对象比定义它的外部函数长寿。这句话的理解是:外部变量执行后,场景被清理,它的变量消失了,但是cell对象并没有消失,依然存在。这就是所谓的*cellvariable*co_freevars在block中使用,但没有在block中定义(不包括全局变量,内置变量)。即由当前块(函数)引用的外部单元格变量组成的元组,与前面的co_cellvars是一个相对的概念。这两个变量是一个有两个边的变量。在外部变量的外部作用域中,创建了变量e和变量inner(函数)。因为嵌套函数inner使用了外部变量e,所以在outer函数中,e作为cell变量,绑定了一个特殊的cell对象。这个特殊的cell对象绑定到inner上,这样当outer作用域消失时,inner通过cell对象访问变量e,这是inner函数的一个自由变量(来自cell对象)。掌握自由变量的概念是编写有状态函数和装饰器的基础。4.字节码细节字节码看起来像乱码,比如上面fun函数的字节码:co_code:b'|\x00|\x01\x17\x00S\x00'。综上所述,一个字节码由一个位的操作码和一个位的参数序列组成。我们来具体分析一下。co_code[0]表示第一个opcode|,也就是ASCII码124表示的字符。在include/opcode.h中可以看到124是LOAD_FASTopcode,是对局部变量列表的加载操作。其他类似:例如,LOAD_CONST对文字常量列表进行操作,而LOAD_GLOBAL对全局变量进行操作。如果操作码不带参数,则可以省略参数。这里第一个操作码的参数是co_code[1]as0x00。因此,这个完整的字节码操作就是将局部变量列表co_varnames的0x00索引内容a压入计算栈顶。字节序列好像很费力,我们用dis.dis(fun)来反汇编一下代码,字节码如下。20LOAD_FAST0(a)2LOAD_FAST1(b)4BINARY_ADD6RETURN_VALUE这个看起来很简单。第一列2代表源码的第二行,即returna+b。第二列0表示操作码相对于字节码开头的偏移量,LOAD_FAST表示操作码,0表示参数,(a)由dis生成,即局部变量tuple的第0个元素a。此条目将变量a压入计算堆栈。类推第二个,局部变量b被压入计算栈。第三个BINARY_ADD没有参数,是求和,弹出a和b,求和,将结果压入计算栈顶。第四个RETURN_VALUE弹出结果并结束栈帧。Python编译生成pyc文件:之前在本地目录生成Python3,然后在pycache目录下生成。我们打开上面例子生成的pyc文件,以16进制查看。很明显编译后的字节码直接在PYC文件中。5.其他代码块在上面的代码对象中,我们对函数进行了反汇编,使用了dis.dis(fun)指令,其中fun是一个函数。第二部分说了获取code对象的方式有两种。当我们编译其他代码对象时,我们实际上反编译了模块。如果此时代码块中有函数,只会生成代码对象,不会生成真正的函数对象。例如下面的语句:print(dis.dis(compile('deffun(a,b):returna+b','','exec'))),输出结果如下。可以看出,此时的函数只是一个代码对象,作为常量加载,在MAKE_FUNCTION之后赋值给fun局部变量。10LOAD_CONST0()2LOAD_CONST1('fun')4MAKE_FUNCTION06STORE_NAME0(fun)8LOAD_CONST2(None)10RETURN_VALUE6.总结代码对象封装了Python虚拟机的字节码和字节编译相关的其他信息。代码被称为Python虚拟机上的汇编语言,我们分析了自由变量和cell变量,掌握了自由变量的概念,自由变量是编写带状态的装饰器和函数的基础,字节码由一个操作码和一个parameter了解其细节形成的序列,有助于我们理解Python的特性。在分析变量作用域和闭包时,可以作为一个有力的工具。作者:龚庆奎,大奎,对计算机和电子信息工程感兴趣。gongqingkuiat126.com