0x1.前言
Python 代码先被编译为字节码后,再由Python虚拟机来执行字节码, Python的字节码是一种类似汇编指令的中间语言, 一个Python语句会对应若干字节码指令,虚拟机一条一条执行字节码指令, 从而完成程序执行。
Python dis 模块支持对Python代码进行反汇编, 生成字节码指令。
dis.dis()
将CPython字节码转为可读的伪代码(类似于汇编代码)。结构如下:
1 | 7 0 LOAD_CONST 1 (0) |
其实就是这样的结构:
1 | 源码行号 | 指令在函数中的偏移 | 指令符号 | 指令参数 | 实际参数值 |
0x2.变量
1.const
LOAD_CONST
加载const
变量,比如数值、字符串等等,一般用于传给函数的参数
1 | 55 12 LOAD_GLOBAL 1 (test) |
转为python代码就是:
1 | test(2, 'output') |
2.局部变量
LOAD_FAST
一般加载局部变量的值,也就是读取值,用于计算或者函数调用传参等。STORE_FAST
一般用于保存值到局部变量。
1 | 61 77 LOAD_FAST 0 (n) |
这段bytecode转为python就是:
1 | n = n / p |
函数的形参也是局部变量,如何区分出是函数形参还是其他局部变量呢?
形参没有初始化,也就是从函数开始到LOAD_FAST
该变量的位置,如果没有看到STORE_FAST
,那么该变量就是函数形参。
而其他局部变量在使用之前肯定会使用STORE_FAST
进行初始化。
具体看下面的实例:
1 | 4 0 LOAD_CONST 1 (0) |
对应的python代码如下,对比一下就一目了然。
1 | def test(arg1): |
3.全局变量
LOAD_GLOBAL
用来加载全局变量,包括指定函数名,类名,模块名等全局符号。
STORE_GLOBAL
用来给全局变量赋值。
1 | 8 6 LOAD_CONST 2 (101) |
对应的python代码
1 | def test(): |
0x3.常用数据类型
1.list
BUILD_LIST
用于创建一个list结构。
1 | 13 0 LOAD_CONST 1 (1) |
对应python代码是:
1 | k = [1, 2] |
另外再看看一种常见的创建list的方式如下:
1 | [x for x in xlist if x!=0 ] |
一个实例bytecode如下:
1 | 22 235 BUILD_LIST 0 //创建list,为赋值给某变量,这种时候一般都是语法糖结构了 |
转为python代码是:
1 | [for x in sieve if x != 0] |
2.dict
BUILD_MAP
用于创建一个空的dict。STORE_MAP
用于初始化dict的内容。
1 | 13 0 BUILD_MAP 1 |
对应的python代码是:
1 | k = {'a': 1} |
再看看修改dict的bytecode:
1 | 14 13 LOAD_CONST 3 (2) |
对应的python代码是:
1 | k['b'] = 2 |
3.slice
BUILD_SLICE
用于创建slice。对于list、元组、字符串都可以使用slice的方式进行访问。
但是要注意BUILD_SLICE
用于[x:y:z]这种类型的slice,结合BINARY_SUBSCR
读取slice的值,结合STORE_SUBSCR
用于修改slice的值。
另外SLICE+n
用于[a:b]类型的访问,STORE_SLICE+n
用于[a:b]类型的修改,其中n
表示如下:
1 | SLICE+0() |
下面看具体实例:
1 | 13 0 LOAD_CONST 1 (1) |
0x4.循环
SETUP_LOOP
用于开始一个循环。SETUP_LOOP 26 (to 35)
中35
表示循环退出点。
while循环
1 | 23 0 LOAD_CONST 1 (0) |
对应python代码是:
1 | i = 0 |
for in结构
1 | 238 LOAD_FAST 3 (sieve)#sieve是个list |
这是典型的for+in结构,转为python代码就是:
1 | for x in sieve: |
0x5.if
POP_JUMP_IF_FALSE
和JUMP_FORWARD
一般用于分支判断跳转。POP_JUMP_IF_FALSE
表示条件结果为FALSE
就跳转到目标偏移指令。JUMP_FORWARD
直接跳转到目标偏移指令。
1 | 23 0 LOAD_CONST 1 (0) |
转为python代码是:
1 | i = 0 |
0x6.分辨函数
1.函数范围
前面介绍第二列表示指令在函数中的偏移地址,所以看到0就是函数开始,下一个0前一条指令就是函数结束位置,当然也可以通过RETURN_VALUE
来确定函数结尾
1 | 54 0 LOAD_FAST 1 (plist) //函数开始 |
2.函数调用
函数调用类似于push+call
的汇编结构,压栈参数从左到右依次压入(当然不是push
,而是读取指令LOAD_xxxx
来指定参数)。
函数名一般通过LOAD_GLOBAL
指令指定,如果是模块函数或者类成员函数通过LOAD_GLOBAL
+LOAD_ATTR
来指定。
先指定要调用的函数,然后压参数,最后通过CALL_FUNCTION
调用。
CALL_FUNCTION
后面的值表示有几个参数。
支持嵌套调用:
1 | 6 0 LOAD_GLOBAL 0 (int) //int函数 |
这段bytecode
转换成python
代码就是
1 | nroot = int(math.sqrt(n)) //其中n是一个局部变量或者函数参数,具体看上下文 |
0x7.其他指令
其他常见指令,一看就明白,就不具体分析了,更多详细内容请看官方文档。
1 | INPLACE_POWER() |
基础运算还有一套对应的BINARY_xxxx
指令,两者区别很简单。
1 | i += 1 //使用INPLACE_xxx |