19.4. dis — 反汇编 Python 字节码
目的:将代码对象转换为字节码的人类可读表示形式以供分析。
dis
模块包括用于反编译 Python 字节码将其拆解为更易于阅读的形式的函数。 查看解释器正在执行的字节码是手动调整紧密循环和执行其他类型优化的好方法。 它对于在多线程应用程序中查找竞争条件也很有用,因为它可用于估计代码中线程控制可能切换的点。
警告
字节码的使用是 CPython 解释器的特定版本的实现细节。 请参阅源代码中的
Include/opcode.h
,了解用于当前查找字节码规范列表的解释器版本。
基本反编译
函数 dis()
打印 Python 代码源(模块、类、方法、函数或代码对象)的反汇编形式。 可以通过从命令行运行 dis
来反汇编诸如 dis_simple.py
之类的模块。
dis_simple.py
#!/usr/bin/env python3
# encoding: utf-8
my_dict = {'a': 1}
输出被组织成列,包含原始源行号,代码对象中的指令地址、操作码名称以及传递给操作码的任何参数。
$ python3 -m dis dis_simple.py
4 0 LOAD_CONST 0 ('a')
2 LOAD_CONST 1 (1)
4 BUILD_MAP 1
6 STORE_NAME 0 (my_dict)
8 LOAD_CONST 2 (None)
10 RETURN_VALUE
在这个例子中,源转换为四种不同的操作来创建和填充字典,然后将结果保存到局部变量。 由于 Python 解释器是基于堆栈的,因此第一步是使用 LOAD_CONST
以正确的顺序将常量放入堆栈,然后使用 BUILD_MAP
弹出新的键和值以添加到字典中。 生成的 dict
对象绑定到变量名 my_dict
并带有 STORE_NAME
。
函数反编译
不幸的是,反编译整个模块并不会自动递归到函数中。
dis_function.py
#!/usr/bin/env python3
# encoding: utf-8
def f(*args):
nargs = len(args)
print(nargs, args)
if __name__ == '__main__':
import dis
dis.dis(f)
反编译 dis_function.py
的结果显示了将函数的代码对象加载到堆栈然后将其转换为函数( LOAD_CONST
, MAKE_FUNCTION
)的操作,但不是函数的主体。
$ python3 -m dis dis_function.py
5 0 LOAD_CONST 0 (<code object f at
0x1044fcf60, file "dis_function.py", line 5>)
2 LOAD_CONST 1 ('f')
4 MAKE_FUNCTION 0
6 STORE_NAME 0 (f)
10 8 LOAD_NAME 1 (__name__)
10 LOAD_CONST 2 ('__main__')
12 COMPARE_OP 2 (==)
14 POP_JUMP_IF_FALSE 34
11 16 LOAD_CONST 3 (0)
18 LOAD_CONST 4 (None)
20 IMPORT_NAME 2 (dis)
22 STORE_NAME 2 (dis)
12 24 LOAD_NAME 2 (dis)
26 LOAD_ATTR 2 (dis)
28 LOAD_NAME 0 (f)
30 CALL_FUNCTION 1
32 POP_TOP
>> 34 LOAD_CONST 4 (None)
36 RETURN_VALUE
要查看函数内部,函数本身必须传递给 dis()
。
$ python3 dis_function.py
6 0 LOAD_GLOBAL 0 (len)
2 LOAD_FAST 0 (args)
4 CALL_FUNCTION 1
6 STORE_FAST 1 (nargs)
7 8 LOAD_GLOBAL 1 (print)
10 LOAD_FAST 1 (nargs)
12 LOAD_FAST 0 (args)
14 CALL_FUNCTION 2
16 POP_TOP
18 LOAD_CONST 0 (None)
20 RETURN_VALUE
要打印函数的摘要,包括有关它使用的参数和名称的信息,请调用 show_code()
,将函数作为第一个参数传递。
#!/usr/bin/env python3
# encoding: utf-8
def f(*args):
nargs = len(args)
print(nargs, args)
if __name__ == '__main__':
import dis
dis.show_code(f)
show_code()
的参数传递给 code_info()
,它返回一个格式正确的函数、方法、代码字符串或其他代码对象的摘要,准备好打印。
$ python3 dis_show_code.py
Name: f
Filename: dis_show_code.py
Argument count: 0
Kw-only arguments: 0
Number of locals: 2
Stack size: 3
Flags: OPTIMIZED, NEWLOCALS, VARARGS, NOFREE
Constants:
0: None
Names:
0: len
1: print
Variable names:
0: args
1: nargs
类
类可以传递给 dis()
,在这种情况下,所有的方法都会被反编译。
dis_class.py
#!/usr/bin/env python3
# encoding: utf-8
import dis
class MyObject:
"""Example for dis."""
CLASS_ATTRIBUTE = 'some value'
def __str__(self):
return 'MyObject({})'.format(self.name)
def __init__(self, name):
self.name = name
dis.dis(MyObject)
这些方法按字母顺序列出,而不是它们在文件中出现的顺序。
$ python3 dis_class.py
Disassembly of __init__:
16 0 LOAD_FAST 1 (name)
2 LOAD_FAST 0 (self)
4 STORE_ATTR 0 (name)
6 LOAD_CONST 0 (None)
8 RETURN_VALUE
Disassembly of __str__:
13 0 LOAD_CONST 1 ('MyObject({})')
2 LOAD_ATTR 0 (format)
4 LOAD_FAST 0 (self)
6 LOAD_ATTR 1 (name)
8 CALL_FUNCTION 1
10 RETURN_VALUE
源代码
使用程序的源代码通常比使用代码对象本身更方便。 dis
中的函数接受包含源代码的字符串参数,并在生成反编译或其他输出之前将它们转换为代码对象。
dis_string.py
import dis
code = """
my_dict = {'a': 1}
"""
print('Disassembly:\n')
dis.dis(code)
print('\nCode details:\n')
dis.show_code(code)
传递字符串可以保存编译代码的步骤并自己保存对结果的引用,这在检查函数外部的语句时更为方便。
$ python3 dis_string.py
Disassembly:
2 0 LOAD_CONST 0 ('a')
2 LOAD_CONST 1 (1)
4 BUILD_MAP 1
6 STORE_NAME 0 (my_dict)
8 LOAD_CONST 2 (None)
10 RETURN_VALUE
Code details:
Name: <module>
Filename: <disassembly>
Argument count: 0
Kw-only arguments: 0
Number of locals: 0
Stack size: 2
Flags: NOFREE
Constants:
0: 'a'
1: 1
2: None
Names:
0: my_dict
使用反编译调试
有时在调试异常时,查看哪个字节码导致问题会很有用。 有几种方法可以围绕错误反编译代码。 第一种是通过在交互式解释器中使用 dis()
来报告最后一个异常。 如果没有参数传递给 dis()
,那么它会查找异常并显示导致错误的堆栈顶部的反编译。
$ python3
Python 3.5.1 (v3.5.1:37a07cee5969, Dec 5 2015, 21:12:44)
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import dis
>>> j = 4
>>> i = i + 4
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'i' is not defined
>>> dis.dis()
1 --> 0 LOAD_NAME 0 (i)
3 LOAD_CONST 0 (4)
6 BINARY_ADD
7 STORE_NAME 0 (i)
10 LOAD_CONST 1 (None)
13 RETURN_VALUE
>>>
行号后面的 -->
表示导致错误的操作码。 没有定义 i
变量,因此与名称关联的值无法加载到堆栈中。
程序还可以直接将其传递给 distb()
来打印有关活动回溯的信息。 在这个例子中,有一个 DivideByZero
异常,但由于公式有两个分区,因此可能不清楚哪个部分为零。
dis_traceback.py
#!/usr/bin/env python3
# encoding: utf-8
i = 1
j = 0
k = 3
try:
result = k * (i / j) + (i / k)
except Exception:
import dis
import sys
exc_type, exc_value, exc_tb = sys.exc_info()
dis.distb(exc_tb)
在反编译版本中将错误加载到堆栈中时,很容易发现该错误。 使用 -->
突出显示错误操作,其中前一行将 j
的值压入堆栈。
$ python3 dis_traceback.py
4 0 LOAD_CONST 0 (1)
2 STORE_NAME 0 (i)
5 4 LOAD_CONST 1 (0)
6 STORE_NAME 1 (j)
6 8 LOAD_CONST 2 (3)
10 STORE_NAME 2 (k)
8 12 SETUP_EXCEPT 24 (to 38)
9 14 LOAD_NAME 2 (k)
16 LOAD_NAME 0 (i)
18 LOAD_NAME 1 (j)
--> 20 BINARY_TRUE_DIVIDE
22 BINARY_MULTIPLY
24 LOAD_NAME 0 (i)
26 LOAD_NAME 2 (k)
28 BINARY_TRUE_DIVIDE
30 BINARY_ADD
32 STORE_NAME 3 (result)
...trimmed...
循环性能分析
除了调试错误,dis
还可以帮助识别性能问题。 检查反编译代码对于紧密循环特别有用,其中的 Python 指令数量很少但它们被转换为低效的字节码集。 通过检查类的一些不同实现,可以看到反编译的有用性, Dictionary
读取单词列表并按照第一个字母对它们进行分组。
dis_test_loop.py
import dis
import sys
import textwrap
import timeit
module_name = sys.argv[1]
module = __import__(module_name)
Dictionary = module.Dictionary
dis.dis(Dictionary.load_data)
print()
t = timeit.Timer(
'd = Dictionary(words)',
textwrap.dedent("""
from {module_name} import Dictionary
words = [
l.strip()
for l in open('/usr/share/dict/words', 'rt')
]
""").format(module_name=module_name)
)
iterations = 10
print('TIME: {:0.4f}'.format(t.timeit(iterations) / iterations))
测试驱动程序应用程序 dis_test_loop.py
可用于运行 Dictionary
类的每个实体,从一个简单但缓慢的实现开始。
dis_slow_loop.py
#!/usr/bin/env python3
# encoding: utf-8
class Dictionary:
def __init__(self, words):
self.by_letter = {}
self.load_data(words)
def load_data(self, words):
for word in words:
try:
self.by_letter[word[0]].append(word)
except KeyError:
self.by_letter[word[0]] = [word]
使用此版本运行测试程序会显示反编译的程序以及运行所需的时间。
$ python3 dis_test_loop.py dis_slow_loop
12 0 SETUP_LOOP 83 (to 86)
3 LOAD_FAST 1 (words)
6 GET_ITER
>> 7 FOR_ITER 75 (to 85)
10 STORE_FAST 2 (word)
13 13 SETUP_EXCEPT 28 (to 44)
14 16 LOAD_FAST 0 (self)
19 LOAD_ATTR 0 (by_letter)
22 LOAD_FAST 2 (word)
25 LOAD_CONST 1 (0)
28 BINARY_SUBSCR
29 BINARY_SUBSCR
30 LOAD_ATTR 1 (append)
33 LOAD_FAST 2 (word)
36 CALL_FUNCTION 1 (1 positional, 0
keyword pair)
39 POP_TOP
40 POP_BLOCK
41 JUMP_ABSOLUTE 7
15 >> 44 DUP_TOP
45 LOAD_GLOBAL 2 (KeyError)
48 COMPARE_OP 10 (exception match)
51 POP_JUMP_IF_FALSE 81
54 POP_TOP
55 POP_TOP
56 POP_TOP
16 57 LOAD_FAST 2 (word)
60 BUILD_LIST 1
63 LOAD_FAST 0 (self)
66 LOAD_ATTR 0 (by_letter)
69 LOAD_FAST 2 (word)
72 LOAD_CONST 1 (0)
75 BINARY_SUBSCR
76 STORE_SUBSCR
77 POP_EXCEPT
78 JUMP_ABSOLUTE 7
>> 81 END_FINALLY
82 JUMP_ABSOLUTE 7
>> 85 POP_BLOCK
>> 86 LOAD_CONST 0 (None)
89 RETURN_VALUE
TIME: 0.0568
上一个输出显示 dis_slow_loop.py
需要0.0568秒才能在 OS X 上的/usr/share/dict/words
副本中加载 235886 个单词。这不是太糟糕,但随附的反编译表明循环在做更多它不需要的工作。 当它进入操作码 13 中的循环时,它会建立一个异常上下文( SETUP_EXCEPT
)。 然后,在将 word
附加到列表之前,需要六个操作码才能找到 self.by_letter[word[0]]
。 如果因为 word[0]
不在字典中而有异常,则异常处理程序执行所有相同的工作以确定 word [0]
(三个操作码)并设置 self.by_letter[word[0]]
到包含该单词的新列表。
消除异常设置的一种技术是预先填充 self.by_letter
,其中每个字母的字母都有一个列表。 这意味着应始终能找到新单词的列表,并且可以在查找后保存该值。
dis_faster_loop.py
#!/usr/bin/env python3
# encoding: utf-8
import string
class Dictionary:
def __init__(self, words):
self.by_letter = {
letter: []
for letter in string.ascii_letters
}
self.load_data(words)
def load_data(self, words):
for word in words:
self.by_letter[word[0]].append(word)
此更改将操作码数量减少了一半,但只将时间减少到 0.0567 秒。 显然,异常处理有一些开销,但不是很大。
$ python3 dis_test_loop.py dis_faster_loop
17 0 SETUP_LOOP 38 (to 41)
3 LOAD_FAST 1 (words)
6 GET_ITER
>> 7 FOR_ITER 30 (to 40)
10 STORE_FAST 2 (word)
18 13 LOAD_FAST 0 (self)
16 LOAD_ATTR 0 (by_letter)
19 LOAD_FAST 2 (word)
22 LOAD_CONST 1 (0)
25 BINARY_SUBSCR
26 BINARY_SUBSCR
27 LOAD_ATTR 1 (append)
30 LOAD_FAST 2 (word)
33 CALL_FUNCTION 1 (1 positional, 0
keyword pair)
36 POP_TOP
37 JUMP_ABSOLUTE 7
>> 40 POP_BLOCK
>> 41 LOAD_CONST 0 (None)
44 RETURN_VALUE
TIME: 0.0567
通过在循环之外移动 self.by_letter
的查找,可以进一步提高性能(毕竟值不会改变)。
dis_fastest_loop.py
#!/usr/bin/env python3
# encoding: utf-8
import collections
class Dictionary:
def __init__(self, words):
self.by_letter = collections.defaultdict(list)
self.load_data(words)
def load_data(self, words):
by_letter = self.by_letter
for word in words:
by_letter[word[0]].append(word)
操作码 0-6 现在找到 self.by_letter
的值并将其保存为局部变量 by_letter
。 使用局部变量只需要一个操作码,而不是两个操作码(语句 22 使用 LOAD_FAST
将字典放入堆栈中)。此更改后,运行时间将减少到0.0473秒。
$ python3 dis_test_loop.py dis_fastest_loop
14 0 LOAD_FAST 0 (self)
3 LOAD_ATTR 0 (by_letter)
6 STORE_FAST 2 (by_letter)
15 9 SETUP_LOOP 35 (to 47)
12 LOAD_FAST 1 (words)
15 GET_ITER
>> 16 FOR_ITER 27 (to 46)
19 STORE_FAST 3 (word)
16 22 LOAD_FAST 2 (by_letter)
25 LOAD_FAST 3 (word)
28 LOAD_CONST 1 (0)
31 BINARY_SUBSCR
32 BINARY_SUBSCR
33 LOAD_ATTR 1 (append)
36 LOAD_FAST 3 (word)
39 CALL_FUNCTION 1 (1 positional, 0
keyword pair)
42 POP_TOP
43 JUMP_ABSOLUTE 16
>> 46 POP_BLOCK
>> 47 LOAD_CONST 0 (None)
50 RETURN_VALUE
TIME: 0.0473
Brandon Rhodes 建议的进一步优化是完全消除 for
循环的 Python 版本。 如果 itertools.groupby()
用于排列输入,则迭代将移至 C 中。这是安全的,因为已知输入已排序。 如果不是这样,程序将需要先对它们进行排序。
dis_eliminate_loop.py
#!/usr/bin/env python3
# encoding: utf-8
import operator
import itertools
class Dictionary:
def __init__(self, words):
self.by_letter = {}
self.load_data(words)
def load_data(self, words):
# 按字母表排列
grouped = itertools.groupby(
words,
key=operator.itemgetter(0),
)
# 保存已排序的单词集
self.by_letter = {
group[0][0]: group
for group in grouped
}
itertools
版本只需要0.0332秒运行,大约是原始运行时间的60%。
$ python3 dis_test_loop.py dis_eliminate_loop
16 0 LOAD_GLOBAL 0 (itertools)
3 LOAD_ATTR 1 (groupby)
17 6 LOAD_FAST 1 (words)
9 LOAD_CONST 1 ('key')
18 12 LOAD_GLOBAL 2 (operator)
15 LOAD_ATTR 3 (itemgetter)
18 LOAD_CONST 2 (0)
21 CALL_FUNCTION 1 (1 positional, 0
keyword pair)
24 CALL_FUNCTION 257 (1 positional, 1
keyword pair)
27 STORE_FAST 2 (grouped)
21 30 LOAD_CONST 3 (<code object
<dictcomp> at 0x101517930, file ".../dis_eliminate_loop.py",
line 21>)
33 LOAD_CONST 4
('Dictionary.load_data.<locals>.<dictcomp>')
36 MAKE_FUNCTION 0
23 39 LOAD_FAST 2 (grouped)
42 GET_ITER
43 CALL_FUNCTION 1 (1 positional, 0
keyword pair)
46 LOAD_FAST 0 (self)
49 STORE_ATTR 4 (by_letter)
52 LOAD_CONST 0 (None)
55 RETURN_VALUE
TIME: 0.0332
编译器优化
反编译编译源还会暴露编译器所做的一些优化。 例如,在可能的情况下,在编译期间折叠文字表达式。
dis_constant_folding.py
#!/usr/bin/env python3
# encoding: utf-8
# Folded
i = 1 + 2
f = 3.4 * 5.6
s = 'Hello,' + ' World!'
# Not folded
I = i * 3 * 4
F = f / 2 / 3
S = s + '\n' + 'Fantastic!'
第 5-7 行的表达式中的值都不能改变执行操作的方式,因此表达式的结果可以在编译时计算并折叠成单个 LOAD_CONST
指令。 第 10-12 行不是这样。 因为变量涉及这些表达式,并且变量可能引用了重载所涉及的运算符的对象,所以执行必须延迟到运行时。
$ python3 -m dis dis_constant_folding.py
5 0 LOAD_CONST 11 (3)
2 STORE_NAME 0 (i)
6 4 LOAD_CONST 12 (19.04)
6 STORE_NAME 1 (f)
7 8 LOAD_CONST 13 ('Hello, World!')
10 STORE_NAME 2 (s)
10 12 LOAD_NAME 0 (i)
14 LOAD_CONST 6 (3)
16 BINARY_MULTIPLY
18 LOAD_CONST 7 (4)
20 BINARY_MULTIPLY
22 STORE_NAME 3 (I)
11 24 LOAD_NAME 1 (f)
26 LOAD_CONST 1 (2)
28 BINARY_TRUE_DIVIDE
30 LOAD_CONST 6 (3)
32 BINARY_TRUE_DIVIDE
34 STORE_NAME 4 (F)
12 36 LOAD_NAME 2 (s)
38 LOAD_CONST 8 ('\n')
40 BINARY_ADD
42 LOAD_CONST 9 ('Fantastic!')
44 BINARY_ADD
46 STORE_NAME 5 (S)
48 LOAD_CONST 10 (None)
50 RETURN_VALUE
另请参阅
- dis 标准库文档 -- 包括 字节码介绍 的列表。
Include/opcode.h
-- CPython 解释器的源代码定义了opcode.h
中的字节代码。- Python Essential Reference, 第四版, David M. Beazley -- www.informit.com/store/product.aspx...
- thomas.apestaart.org "Python Disassembly" -- 简要讨论在 Python 2.5 和2.6 之间存储字典中值的区别。
- Why is looping over range() in Python fast... -- StackOverflow 上关于通过反汇编的字节码比较 2 个循环示例的讨论。
- Decorator for binding constants at compile... -- Raymond Hettinger 和 Skip Montanaro 的 Python Cookbook 参考,说明了一个函数装饰器,它重写函数的字节码以插入全局常量,以避免运行时名称查找。
本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。