我应该如何理解dis.dis的输出?
- 2025-04-16 08:57:00
- admin 原创
- 19
问题描述:
我想了解如何使用dis (Python 字节码反汇编器)dis.dis
。具体来说,应该如何解释(或)的输出dis.disassemble
?
。
这是一个非常具体的例子(在 Python 2.7.3 中):
dis.dis("heapq.nsmallest(d,3)")
0 BUILD_SET 24933
3 JUMP_IF_TRUE_OR_POP 11889
6 JUMP_FORWARD 28019 (to 28028)
9 STORE_GLOBAL 27756 (27756)
12 LOAD_NAME 29811 (29811)
15 STORE_SLICE+0
16 LOAD_CONST 13100 (13100)
19 STORE_SLICE+1
我看到JUMP_IF_TRUE_OR_POP
等是字节码指令(虽然有趣的是,BUILD_SET
它并没有出现在这个列表中,尽管我预计它会像 一样工作BUILD_TUPLE
)。我认为右边的数字是内存分配,左边的数字是goto指令……我注意到它们几乎每次都增加 3(但不完全是)。
如果我包装dis.dis("heapq.nsmallest(d,3)")
在一个函数中:
def f_heapq_nsmallest(d,n):
return heapq.nsmallest(d,n)
dis.dis("f_heapq(d,3)")
0 BUILD_TUPLE 26719
3 LOAD_NAME 28769 (28769)
6 JUMP_ABSOLUTE 25640
9 <44> # what is <44> ?
10 DELETE_SLICE+1
11 STORE_SLICE+1
解决方案 1:
您正在尝试反汇编一个包含源代码的字符串,但 Python 2 不支持此功能。dis.dis
如果使用字符串参数,它会将该字符串视为包含字节码(请参阅disassemble_string
中的dis.py
函数)。因此,您看到的输出毫无意义,因为将源代码误解为字节码。
Python 3 中的情况有所不同,它会在反汇编dis.dis
字符串参数之前对其进行编译:
Python 3.2.3 (default, Aug 13 2012, 22:28:10)
>>> import dis
>>> dis.dis('heapq.nlargest(d,3)')
1 0 LOAD_NAME 0 (heapq)
3 LOAD_ATTR 1 (nlargest)
6 LOAD_NAME 2 (d)
9 LOAD_CONST 0 (3)
12 CALL_FUNCTION 2
15 RETURN_VALUE
在 Python 2 中,您需要先自行编译代码,然后再将其传递给dis.dis
:
Python 2.7.3 (default, Aug 13 2012, 18:25:43)
>>> import dis
>>> dis.dis(compile('heapq.nlargest(d,3)', '<none>', 'eval'))
1 0 LOAD_NAME 0 (heapq)
3 LOAD_ATTR 1 (nlargest)
6 LOAD_NAME 2 (d)
9 LOAD_CONST 0 (3)
12 CALL_FUNCTION 2
15 RETURN_VALUE
这些数字是什么意思?最左边的数字1
是编译此字节码的源代码中的行号。左侧列中的数字是字节码中指令的偏移量,右侧的数字是opargs。让我们看一下实际的字节码:
>>> co = compile('heapq.nlargest(d,3)', '<none>', 'eval')
>>> co.co_code.encode('hex')
'6500006a010065020064000083020053'
在字节码的偏移量 0 处,我们找到了65
的操作码LOAD_NAME
,以及相应的操作参数0000
;然后(在偏移量 3 处)6a
是操作码LOAD_ATTR
,以及0100
相应的操作参数,依此类推。注意,操作参数是小端序的,所以0100
数字是 1。未公开的opcode
模块包含一些表格,opname
列出了每个操作码的名称,以及opmap
每个名称对应的操作码:
>>> opcode.opname[0x65]
'LOAD_NAME'
oparg 的含义取决于操作码,要了解完整的内容,您需要阅读 CPython 虚拟机的实现。ceval.c
对于LOAD_NAME
和 , oparg 是代码对象属性LOAD_ATTR
的索引:co_names
>>> co.co_names
('heapq', 'nlargest', 'd')
因为它是代码对象属性的LOAD_CONST
索引:co_consts
>>> co.co_consts
(3,)
对于CALL_FUNCTION
,它是传递给函数的参数的数量,以 16 位编码,低字节为普通参数的数量,高字节为关键字参数的数量。
解决方案 2:
我正在重新发布对另一个问题的回答,以确保在谷歌搜索时能找到它dis.dis()
。
为了完成伟大的Gareth Rees 的回答,这里只是一个逐列的小摘要,以解释反汇编字节码的输出。
例如,给定此函数:
def f(num):
if num == 42:
return True
return False
这可以拆解成(Python 3.6):
(1)|(2)|(3)|(4)| (5) |(6)| (7)
---|---|---|---|----------------------|---|-------
2| | | 0|LOAD_FAST | 0|(num)
|-->| | 2|LOAD_CONST | 1|(42)
| | | 4|COMPARE_OP | 2|(==)
| | | 6|POP_JUMP_IF_FALSE | 12|
| | | | | |
3| | | 8|LOAD_CONST | 2|(True)
| | | 10|RETURN_VALUE | |
| | | | | |
4| |>> | 12|LOAD_CONST | 3|(False)
| | | 14|RETURN_VALUE | |
每列都有特定用途:
源代码中对应的行号
可选地指示当前执行的指令(例如,当字节码来自框架对象时)
JUMP
表示从之前的指令到这个指令的可能的标签字节码中与字节索引对应的地址(这些是 2 的倍数,因为 Python 3.6 对每个指令使用 2 个字节,而在以前的版本中可能会有所不同)
指令名称(也称为opname ),每个指令在模块
dis
中都有简要说明,其实现可以在ceval.c
(CPython 的核心循环)中找到指令的参数(如果有)由 Python 内部使用,用于获取一些常量或变量、管理堆栈、跳转到特定指令等。
指令论证的人性化解释
扫码咨询,免费领取项目管理大礼包!