Python中的源代码反编译成字节码及其解析
前⾔
我之前的⼀篇博⽂给⼤家提及了Python中的pyc⽂件的诞⽣和它的作⽤,其实是为了提升Python解释器的效率,将py⽂件编译成了字节码,并保存到了pyc⽂件中。其中Python实际上是将源代码编译为虚拟机的⼀组指令(字节码,也叫pycodeobject),Python解释器就是该虚拟机的实现。
Python虚拟机
Cpython使⽤基于堆栈的虚拟机,也就是说,它完全围绕堆栈数据结构(你可以将项⽬“推”到结构的“顶部”,或者将项⽬“弹
出”到“顶部”)去运⾏。
CPython使⽤三种类型的栈
① 调⽤堆栈。这是运⾏中的Python程序的主要结构。对于每个当前活动的函数调⽤,它都有⼀个项⽬ – “帧(Frame)”,堆栈的底部是程序的⼊⼝点。每次函数调⽤都会将新的帧推到调⽤堆栈上,每次函数调⽤返回时,它的帧(可以理解为⽤于传输数据的结构)都会弹出。
② 在每⼀帧中,都有⼀个评估堆栈(也称为数据堆栈)。这个堆栈是执⾏ Python 函数的地⽅,执⾏Python代码主要包括将数据推到这个堆栈上,操纵它们,然后将它们弹出。
③ 同样在每⼀帧中,都有⼀个块堆栈。Python使⽤它来跟踪某些类型的控制结构:循环、try /except块,以及 with 块都会导致条⽬被推送到块堆栈上,每当退出这些结构之⼀时,块堆栈就会弹出。这有助于Python知道在任何给定时刻哪些块是活动的,例如,continue或break语句可以影响正确的块。
⼤多数 Python 字节码指令操作的是当前调⽤栈帧的计算栈,虽然,还有⼀些指令可以做其它的事情(⽐如跳转到指定指令,或者操作块栈)。
为了更好的理解,假设我们有⼀些调⽤函数的代码,⽐如下⾯这个:
my_func(my_var, 6)
上述Python代码在执⾏时,Python解释器会将其转换为⼀系列的字节码指令:
⼀个 LOAD_NAME 指令,⽤于查函数对象 my_func ,并将其推送到计算栈的顶部;
另⼀个 LOAD_NAME 指令去查变量 my_var,并将其推送到计算栈的顶部;
⼀个 LOAD_CONST 指令将⼀个整数 6 推送到计算栈的顶部;
⼀个 CALL_FUNCTION 指令。
CALL_FUNCTION 指令有1个参数,它表⽰ Python 需要在堆栈顶部弹出1个位置参数;然后函数将在它上⾯进⾏调⽤,并且它也同时被弹出(关键字参数的函数,使⽤指令 CALL_FUNCTION_KW 类似的操作,并配合使⽤第三条指令 CALL_FUNCTION_EX ,它适⽤于函数调⽤涉及到参数使⽤ * 或 ** 操作符的情况)
⼀旦 Python 具备了这些,它将在调⽤堆栈上分配⼀个新的帧,填充到函数调⽤的本地变量,然后运⾏该帧内的 my_func 的字节码。⼀旦运⾏完成,帧将从调⽤堆栈中弹出,在原始帧中,my_func 的返回值将被推⼊到计算栈的顶部。
我们知道了这个,也知道字节码⽂件了,但是如何去使⽤字节码呢?
不知道也没关系,接下来的时间我们所有的话题都将围绕字节码,在python有⼀个模块可以通过反编译Python代码来⽣成字节码,这个模块就是今天要说的 dis 模块。
使⽤ dis 模块反汇编Python源代码
上图中,通过 dis 模块我们可以很快将 hello() 函数中的Python源代码反汇编成字节码:
1、LOAD_GLOBAL 0:告诉 Python 通过 co_names (它是 print 函数)的索引 0 上的名字去查它指向的全局对象,然后将它推⼊到计算栈;
2、LOAD_CONST 1:带⼊ co_consts 在索引 1 上的字⾯值,并将它推⼊(索引 0 上的字⾯值是 None,它表⽰在 co_consts 中,因为 Python 函数调⽤有⼀个隐式的返回值 None,如果函数没有显式的返回表达式,就返回这个隐式的值 );
3、CALL_FUNCTION 1:告诉 Python 去调⽤⼀个函数;它需要从栈中弹出⼀个位置参数,然后,新的栈顶将被函数调⽤。
代码对象在函数中可以以属性 __code__ 来访问,并且携带了⼀些重要的属性:
co_consts 是存在于函数体内的任意实数的元组;
co_varnames 是函数体内使⽤的包含任意本地变量名字的元组;
co_names 是在函数体内引⽤的任意⾮本地名字的元组;
co_code 表⽰函数的字节码指令序列。
许多字节码指令,尤其是那些推⼊到栈中的加载值,或者在变量和属性中的存储值,都是以在这些元组中的索引作为它们参数。
其中 co_code 的字节码指令序列我们可以打印出来:
对照dis输出的字节码指令, 以[116,0]序列为例。116表⽰在Python字节码定义中的索引,在python代码中,可以通过
dis.opname[116] 查看,即为 LOAD_GLOBAL 。⽽后的1个字节表⽰指令的参数。⽽使⽤ dis 输出的字节码指令中,第⼆列的字节码索引则是指当前指令在 co_code 序列中所在的位置。
再来⼀个反汇编Python代码的案例
python新手代码及作用以第⼀条指令为例:
第⼀列的数字 2 表⽰对应python源代码的⾏号;
第⼆列的数字是字节码的索引(该指令在 co_code 中的索引),字节码指令 LOAD_CONST 在 0 位置,即 co_code 列表中的第⼀个元素;
第三列是指令本⾝对应的⼈类可读的名字(⽽⾮字节码,我们上⾯打印出来的 co_code 是 bytes 类型);
第四列表⽰执⾏指令需要的参数在 co_varnames 和 co_consts 元组中的索引;
第五列则是实际计算传⼊的参数;
另外,其中的 >> 表⽰跳转的⽬标, 第6⾏的 16 表明了跳转到索引为 16 的指令,这个指令由 POP_JUMP_IF_FALSE 触发(这⾏指令后⾯的 16 即表⽰跳转到字节码指令索引为 16 的指令去执⾏)。
Python代码在编译过程中会⽣成 CodeObject ,CodeObject 是在虚拟机中的抽象表⽰, 在Python 的 C源码中表⽰为 PyCodeObject ,⽽⽣成的 .pyc ⽂件则是字节码在磁盘中的存储的⽂件。
当然对于简单的代码我们可以通过命令⾏的形式完成 .py ⽂件中代码的反汇编:
参考⽂献
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论