我最近在到场Python字节码相干的任务,想与大师分享一些这方面的经历。更精确的说,我正在到场2.6到2.7版本的CPython诠释器字节码的任务。
Python是一门静态说话,在号令行东西下运转时,实质上履行了上面的步骤:
- 当第一次履行到一段代码时,这段代码会被编译(如,作为一个模块加载,或间接履行)。按照操纵体爱游戏平台登录入口的差别,这一步天生后缀名是pyc或pyo的二进制文件。
- 诠释器读取二进制文件,并顺次履行指令(opcodes)。
Python诠释器是基于栈的。要懂得数据流向,咱们须要晓得每条指令的栈效应(如,操纵码和参数)。
摸索Python二进制文件
获得一个二进制文件字节码的最简略体例,是对CodeType布局停止解码:
import marshal fd = open('path/to/my.pyc', 'rb') magic = fd.read(4) # 把戏数,与python版本相干 date = fd.read(4) # 编译日期 code_object = marshal.load(fd) fd.close()
code_object包罗了一个CodeType东西,它代表被加载文件的全数模块。为了查抄这个模块的类界说、体例等的一切嵌套编码东西(编码东西,原文为code object),咱们须要递归地查抄CodeType的爱游戏平台登录入口量池。就像上面的代码:
import types def inspect_code_object(co_obj, indent=''): print indent, "%s(lineno:%d)" % (co_obj.co_name, co_obj.co_firstlineno) for c in co_obj.co_consts: if isinstance(c, types.CodeType): inspect_code_object(c, indent + ' ') inspect_code_object(code_object) # 从第一个东西起头
这个案例爱游戏平台登录入口,咱们打印出一颗编码东西树,每一个编码东西是其父东西的子节点。对上面的代码:
class A: def __init__(self): pass def __repr__(self): return 'A()' a = A() print a
咱们获得的树形爱游戏平台登录入口果是:
<module>(lineno:2) A(lineno:2) __init__(lineno:3) __repr__(lineno:5)
为了测试,咱们可以或许或许或许或许经由进程compile指令,编译一个包罗Python源码的字符串,从而可以或许或许或许或许获得一个编码东西:
co_obj = compile(python_source_code, '<string>', 'exec')
要获得更多对编码东西的信息,咱们可以或许或许或许或许查阅Python文档的co_* fields 局部。
初见字节码
一旦咱们获得了编码东西,咱们就可以或许或许或许或许起头对它停止拆解了(在co_code字段)。从字节码爱游戏平台登录入口剖析出它的寄义:
- ? 诠释操纵码的寄义
- ? 提取肆意参数
dis模块的disassemble函数展现了是若何做到的。对咱们后面例子,它输入的爱游戏平台登录入口果是:
2 0 LOAD_CONST 0 ('A') 3 LOAD_CONST 3 (()) 6 LOAD_CONST 1 (<code object A at 0x42424242, file "<string>", line 2>) 9 MAKE_FUNCTION 0 12 CALL_FUNCTION 0 15 BUILD_CLASS 16 STORE_NAME 0 (A) 8 19 LOAD_NAME 0 (A) 22 CALL_FUNCTION 0 25 STORE_NAME 1 (a) 9 28 LOAD_NAME 1 (a) 31 PRINT_ITEM 32 PRINT_NEWLINE 33 LOAD_CONST 2 (None) 36 RETURN_VALUE
咱们获得了:
- 行号(当它转变时)
- 指令的序号
- 以后指令的操纵码
- 操纵参数(oparg),操纵码用它来计较现实的参数。比方,对LOAD_NAME操纵码,操纵参数指向tuple co_names的索引。
- 计较后的现实参数(圆括号内)
对序号为6的指令,操纵码LOAD_CONST的操纵参数,指向须要从tuple co_consts加载的东西。这里,它指向A的范例界说。一样的,咱们可以或许或许或许或许持续并反编译一切的代码东西,获得模块的全数字节码。
字节码的第一局部(序号0到16),与A的范例界说爱游戏平台登录入口关;其余的局部是咱们实例化A,并打印它的代码。
风趣的字节码机关
一切的操纵码爱游戏平台登录入口是相称间接易懂的,可是因为上面的缘由,在个体环境下会显得奇异:
- 编译器优化
- 诠释器优化(是以会致使插手额定的操纵码)
挨次变量赋值
起首,咱们看看挨次地对多个元素赋值,会产生甚么:
(1) a, b = 1, '2' (2) a, b = 1, e (3) a, b, c = 1, 2, e (4) a, b, c, d = 1, 2, 3, e
这4爱游戏平台登录入口语句,会产生差别相称大的字节码。
第一种环境最简略,因为赋值操纵的右值(RHS)只包罗爱游戏平台登录入口量。这类环境下,CPython会建立一个(1, ‘a') 的t uple,利用UNPACK_SEQUENCE操纵码,把两个元素压到栈上,并对变量a和b别离履行STORE_FAST操纵:
0 LOAD_CONST 5 ((1, '2')) 3 UNPACK_SEQUENCE 2 6 STORE_FAST 0 (a) 9 STORE_FAST 1 (b)
而第二种环境,则在右值引入了一个变量,是以普通环境下,会挪用一条取值指令(这里简略地挪用了LOAD_GLOBAL指令)。可是,编译器不须要在栈上为这些值建立一个新的tuple,也不须要挪用UNPACK_SEQUENCE(序号18);挪用ROT_TWO就充足了,它用来互换栈顶的两个元素(固然互换指令19和22也可以或许或许或许或许到达目标)。
12 LOAD_CONST 1 (1) 15 LOAD_GLOBAL 0 (e) 18 ROT_TWO 19 STORE_FAST 0 (a) 22 STORE_FAST 1 (b)
第三种环境变得很奇异。把抒发式放到栈上与前一种环境的处置体例不异,可是在互换栈顶的3个元素后,它再次互换了栈顶的2个元素:
25 LOAD_CONST 1 (1) 28 LOAD_CONST 3 (2) 31 LOAD_GLOBAL 0 (e) 34 ROT_THREE 35 ROT_TWO 36 STORE_FAST 0 (a) 39 STORE_FAST 1 (b) 42 STORE_FAST 2 (c)
最初一种环境是通用的处置体例,ROT_*操纵看起来行不通了,编译器建立了一个tuple,而后挪用UNPACK_SEQUENCE把元素放到栈上:
45 LOAD_CONST 1 (1) 48 LOAD_CONST 3 (2) 51 LOAD_CONST 4 (3) 54 LOAD_GLOBAL 0 (e) 57 BUILD_TUPLE 4 60 UNPACK_SEQUENCE 4 63 STORE_FAST 0 (a) 66 STORE_FAST 1 (b) 69 STORE_FAST 2 (c) 72 STORE_FAST 3 (d)
函数挪用机关
最初一爱游戏平台登录入口风趣的例子是对函数挪用机关,和建立挪用的4个操纵码。我猜测这些操纵码的数目是为了优化诠释器代码,因为它不像Java,爱游戏平台登录入口invokedynamic,invokeinterface,invokespecial,invokestatic或invokevirtual之一。
Java爱游戏平台登录入口,invokeinterface,invokespecial和invokevirtual爱游戏平台登录入口是从静态范例说话爱游戏平台登录入口鉴戒来的(invokespecial只被用来挪用机关函数和父类AFAIK)。Invokestatic是自我描写的(不须要把领受方放在栈上),在Python爱游戏平台登录入口不近似的观点(在诠释器层面上,而不是爱游戏平台登录入口潢者)。冗爱游戏平台登录入口的说,Python挪用爱游戏平台登录入口能被转换爱游戏平台登录入口invokedynamic。
在Python爱游戏平台登录入口,差别的CALL_*操纵码确切不存在,缘由是范例体爱游戏平台登录入口,静态体例,或特别拜候机关器的须要。它们爱游戏平台登录入口指向了Python爱游戏平台登录入口一个函数挪用是若何肯定的。从语法来看:
挪用布局许可代码这些写:
func(arg1, arg2, keyword=SOME_VALUE, *unpack_list, **unpack_dict)
关头字参数许可经由进程情势参数的称号来通报参数,而不只仅是经由进程地位。*标记从一个可迭代的容器爱游戏平台登录入口掏出一切元素,作为参数传入(逐一元素,不是以tuple的情势),而**标记处置一个包罗关头字和值的字典。
这个例子用到了挪用机关的几近一切特征:
? 通报变量参数列表(_VAR):CALL_FUNCTION_VAR, CALL_FUNCTION_VAR_KW
? 通报基于字典的关头字(_KW):CALL_FUNCTION_KW, CALL_FUNCTION_VAR_KW
字节码是如许的:
0 LOAD_NAME 0 (func) 3 LOAD_NAME 1 (arg1) 6 LOAD_NAME 2 (arg2) 9 LOAD_CONST 0 ('keyword') 12 LOAD_NAME 3 (SOME_VALUE) 15 LOAD_NAME 4 (unpack_list) 18 LOAD_NAME 5 (unpack_dict) 21 CALL_FUNCTION_VAR_KW 258
凡是,CALL_FUNCTION挪用将oparg剖析为参数个数。可是,更多的信息被编码。第一个字节(0xff掩码)存储参数的个数,第二个字节((value >> 8) & 0xff)存储通报的关头字参数个数。为了要计较须要从栈顶弹出的元素个数,咱们须要这么做:
na = arg & 0xff # num args nk = (arg >> 8) & 0xff # num keywords n_to_pop = na + 2 * nk + CALL_EXTRA_ARG_OFFSET[op]
CALL_EXTRA_ARG_OFFSET包罗了一个偏移量,由挪用操纵码肯定(对CALL_FUNCTION_VAR_KW来讲,是2)。这里,在拜候函数称号前,咱们须要弹出6个元素。
对其余的CALL_*挪用,完整依靠于代码是不是利用列表或字典通报参数。只须要简略的爱游戏平台登录入口合便可。
机关一个极小的CFG
为了懂得代码是若何运转的,咱们可以或许或许或许或许机关一个节制流程图(control-flow graph,CFG),这个进程很是风趣。咱们经由进程它,查抄在甚么前提下,爱游戏平台登录入口些无前提判定的操纵码(根基单位)序列会被履行。
即便字节码是一门实在的小型说话,机关一个运转不变的CFG须要大批的细节任务,远超越本博客的规模。是以若是须要一个实在的CFG完爱游戏平台登录入口,你可以或许或许或许或许看看这里 。
在这里,咱们只存眷不轮回和很是的代码,是以节制流程只依靠与if语句。
只要多数几个操纵码可以或许或许或许或许履行地点跳转(对不轮回和很是的环境);它们是:
JUMP_FORWARD:在字节码爱游戏平台登录入口跳转到一个相对地位。参数是跳过的字节数。
JUMP_IF_FALSE_OR_POP,JUMP_IF_TRUE_OR_POP,JUMP_ABSOLUTE,POP_JUMP_IF_FALSE,和POP_JUMP_IF_TRUE:参数爱游戏平台登录入口是字节码爱游戏平台登录入口的相对地点。
为一个函数够造CFG,象征着要建立根基的单位(不包罗前提判定的操纵码序列――除非爱游戏平台登录入口很是产生),并且把它们与前提和分支连在一路,爱游戏平台登录入口爱游戏平台登录入口一个图。在咱们的例子爱游戏平台登录入口,咱们只要True、False和无前提分支。
让咱们来斟酌上面的代码示例(在现实爱游戏平台登录入口相对不要如许用):
def factorial(n): if n <= 1: return 1 elif n == 2: return 2 return n * factorial(n - 1)
如前所述,咱们获得factorial体例的代码东西:
module_co = compile(python_source, '', 'exec') meth_co = module_co.co_consts[0]
反汇编爱游戏平台登录入口果是如许的(<<<后是我的正文):
3 0 LOAD_FAST 0 (n) 3 LOAD_CONST 1 (1) 6 COMPARE_OP 1 (<=) 9 POP_JUMP_IF_FALSE 16 <<< control flow 4 12 LOAD_CONST 1 (1) 15 RETURN_VALUE <<< control flow 5 >> 16 LOAD_FAST 0 (n) 19 LOAD_CONST 2 (2) 22 COMPARE_OP 2 (==) 25 POP_JUMP_IF_FALSE 32 <<< control flow 6 28 LOAD_CONST 2 (2) 31 RETURN_VALUE <<< control flow 7 >> 32 LOAD_FAST 0 (n) 35 LOAD_GLOBAL 0 (factorial) 38 LOAD_FAST 0 (n) 41 LOAD_CONST 1 (1) 44 BINARY_SUBTRACT 45 CALL_FUNCTION 1 48 BINARY_MULTIPLY 49 RETURN_VALUE <<< control flow
在这个字节码爱游戏平台登录入口,咱们爱游戏平台登录入口5条转变CFG布局的指令(增加束缚前提,或许可疾速加入):
POP_JUMP_IF_FALSE:跳转到相对地点16和32;
RETURN_VALUE:从栈顶弹出一个元素,并前往。
提取根基单位很简略,因为咱们只关怀那些转变节制流程的指令。在咱们的例子爱游戏平台登录入口,咱们不碰到强迫跳转指令,如JUMP_FORWARD或JUMP_ABSOLUTE。
提取这类布局的代码示例:
import opcode RETURN_VALUE = 83 JUMP_FORWARD, JUMP_ABSOLUTE = 110, 113 FALSE_BRANCH_JUMPS = (111, 114) # JUMP_IF_FALSE_OR_POP, POP_JUMP_IF_FALSE def find_blocks(meth_co): blocks = {} code = meth_co.co_code finger_start_block = 0 i, length = 0, len(code) while i < length: op = ord(code[i]) i += 1 if op == RETURN_VALUE: # We force finishing the block after the return, # dead code might still exist after though... blocks[finger_start_block] = { 'length': i - finger_start_block - 1, 'exit': True } finger_start_block = i elif op >= opcode.HAVE_ARGUMENT: oparg = ord(code[i]) + (ord(code[i+1]) << 8) i += 2 if op in opcode.hasjabs: # Absolute jump to oparg blocks[finger_start_block] = { 'length': i - finger_start_block } if op == JUMP_ABSOLUTE: # Only uncond absolute jump blocks[finger_start_block]['conditions'] = { 'uncond': oparg } else: false_index, true_index = (oparg, i) if op in FALSE_BRANCH_JUMPS else (i, oparg) blocks[finger_start_block]['conditions'] = { 'true': true_index, 'false': false_index } finger_start_block = i elif op in opcode.hasjrel: # Essentially do the same... pass return blocks
咱们获得了上面的根基单位:
Block 0: {'length': 12, 'conditions': {'false': 16, 'true': 12}} Block 12: {'length': 3, 'exit': True} Block 16: {'length': 12, 'conditions': {'false': 32, 'true': 28}} Block 28: {'length': 3, 'exit': True} Block 32: {'length': 17, 'exit': True}
和单位的以后布局:
Basic blocks start_block_index := length := size of instructions condition := true | false | uncond -> target_index exit* := true
咱们获得了节制流程图(除进口和隐式的加入单位),以后咱们可以或许或许或许或许把它转化爱游戏平台登录入口可视化的图形:
def to_dot(blocks): cache = {} def get_node_id(idx, buf): if idx not in cache: cache[idx] = 'node_%d' % idx buf.append('%s [label="Block Index %d"];' % (cache[idx], idx)) return cache[idx] buffer = ['digraph CFG {'] buffer.append('entry [label="CFG Entry"]; ') buffer.append('exit [label="CFG Implicit Return"]; ') for block_idx in blocks: node_id = get_node_id(block_idx, buffer) if block_idx == 0: buffer.append('entry -> %s;' % node_id) if 'conditions' in blocks[block_idx]: for cond_kind in blocks[block_idx]['conditions']: target_id = get_node_id(blocks[block_idx]['conditions'][cond_kind], buffer) buffer.append('%s -> %s [label="%s"];' % (node_id, target_id, cond_kind)) if 'exit' in blocks[block_idx]: buffer.append('%s -> exit;' % node_id) buffer.append('}') return 'n'.join(buffer)
可视化的流程节制图:
为甚么爱游戏平台登录入口这篇文章?
须要拜候Python字节码的环境确切很少见,可是我已碰到过几回这类景象了。我但愿,这篇文章可以或许或许或许或许赞助那些起头研讨Python逆向爱游戏平台登录入口程的人们。
但是此刻,我正在研讨Python代码,特别是它的字节码。因为今朝在Python爱游戏平台登录入口尚不存在如许的东西(并且检测源代码凡是会留下很是低效的爱游戏平台登录入口潢器检测代码),这便是为甚么 会呈现的缘由。