搜尋
首頁後端開發Python教學深入Python解释器理解Python中的字节码

我最近在参与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实现,你可以看看这里equip。

在这里,我们只关注没有循环和异常的代码,因此控制流程只依赖与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)

可视化的流程控制图:

201541101827020.jpg (552×517)

为什么有这篇文章?

需要访问Python字节码的情况确实很少见,但是我已经遇到过几次这种情形了。我希望,这篇文章能够帮助那些开始研究Python逆向工程的人们。

然而现在,我正在研究Python代码,尤其是它的字节码。由于目前在Python中尚不存在这样的工具(并且检测源代码通常会留下非常低效的装饰器检测代码),这就是为什么equip会出现的原因。

陳述
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
Python的主要目的:靈活性和易用性Python的主要目的:靈活性和易用性Apr 17, 2025 am 12:14 AM

Python的靈活性體現在多範式支持和動態類型系統,易用性則源於語法簡潔和豐富的標準庫。 1.靈活性:支持面向對象、函數式和過程式編程,動態類型系統提高開發效率。 2.易用性:語法接近自然語言,標準庫涵蓋廣泛功能,簡化開發過程。

Python:多功能編程的力量Python:多功能編程的力量Apr 17, 2025 am 12:09 AM

Python因其簡潔與強大而備受青睞,適用於從初學者到高級開發者的各種需求。其多功能性體現在:1)易學易用,語法簡單;2)豐富的庫和框架,如NumPy、Pandas等;3)跨平台支持,可在多種操作系統上運行;4)適合腳本和自動化任務,提升工作效率。

每天2小時學習Python:實用指南每天2小時學習Python:實用指南Apr 17, 2025 am 12:05 AM

可以,在每天花費兩個小時的時間內學會Python。 1.制定合理的學習計劃,2.選擇合適的學習資源,3.通過實踐鞏固所學知識,這些步驟能幫助你在短時間內掌握Python。

Python與C:開發人員的利弊Python與C:開發人員的利弊Apr 17, 2025 am 12:04 AM

Python適合快速開發和數據處理,而C 適合高性能和底層控制。 1)Python易用,語法簡潔,適用於數據科學和Web開發。 2)C 性能高,控制精確,常用於遊戲和系統編程。

Python:時間投入和學習步伐Python:時間投入和學習步伐Apr 17, 2025 am 12:03 AM

學習Python所需時間因人而異,主要受之前的編程經驗、學習動機、學習資源和方法及學習節奏的影響。設定現實的學習目標並通過實踐項目學習效果最佳。

Python:自動化,腳本和任務管理Python:自動化,腳本和任務管理Apr 16, 2025 am 12:14 AM

Python在自動化、腳本編寫和任務管理中表現出色。 1)自動化:通過標準庫如os、shutil實現文件備份。 2)腳本編寫:使用psutil庫監控系統資源。 3)任務管理:利用schedule庫調度任務。 Python的易用性和豐富庫支持使其在這些領域中成為首選工具。

Python和時間:充分利用您的學習時間Python和時間:充分利用您的學習時間Apr 14, 2025 am 12:02 AM

要在有限的時間內最大化學習Python的效率,可以使用Python的datetime、time和schedule模塊。 1.datetime模塊用於記錄和規劃學習時間。 2.time模塊幫助設置學習和休息時間。 3.schedule模塊自動化安排每週學習任務。

Python:遊戲,Guis等Python:遊戲,Guis等Apr 13, 2025 am 12:14 AM

Python在遊戲和GUI開發中表現出色。 1)遊戲開發使用Pygame,提供繪圖、音頻等功能,適合創建2D遊戲。 2)GUI開發可選擇Tkinter或PyQt,Tkinter簡單易用,PyQt功能豐富,適合專業開發。

See all articles

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱門文章

R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
1 個月前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳圖形設置
1 個月前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您聽不到任何人,如何修復音頻
1 個月前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.聊天命令以及如何使用它們
1 個月前By尊渡假赌尊渡假赌尊渡假赌

熱工具

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

Safe Exam Browser

Safe Exam Browser

Safe Exam Browser是一個安全的瀏覽器環境,安全地進行線上考試。該軟體將任何電腦變成一個安全的工作站。它控制對任何實用工具的訪問,並防止學生使用未經授權的資源。

WebStorm Mac版

WebStorm Mac版

好用的JavaScript開發工具

mPDF

mPDF

mPDF是一個PHP庫,可以從UTF-8編碼的HTML產生PDF檔案。原作者Ian Back編寫mPDF以從他的網站上「即時」輸出PDF文件,並處理不同的語言。與原始腳本如HTML2FPDF相比,它的速度較慢,並且在使用Unicode字體時產生的檔案較大,但支援CSS樣式等,並進行了大量增強。支援幾乎所有語言,包括RTL(阿拉伯語和希伯來語)和CJK(中日韓)。支援嵌套的區塊級元素(如P、DIV),