我应该如何理解dis.dis的输出?

104

我希望了解如何使用Python字节码反汇编工具dis。具体来说,如何解释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
请参见:https://dev59.com/3nA75IYBdhLWcg3wbofM - Andy Hayden
2个回答

121
您正在尝试反汇编包含源代码的字符串,但是Python 2中的dis.dis不支持此操作。使用字符串参数时,它会将字符串视为包含字节码的内容(请参见dis.py中的函数disassemble_string)。因此,您将看到基于将源代码误解为字节码而产生的无意义输出。
在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操作码,带有oparg0000;然后(在偏移量为3处)6aLOAD_ATTR操作码,带有oparg0100,以此类推。请注意,opargs按照小端序排列,因此0100表示数字1。未经记录的opcode模块包含表格opname,给出每个操作码的名称,以及opmap表格,给出每个名称的操作码:

>>> opcode.opname[0x65]
'LOAD_NAME'

oparg的含义取决于操作码,要了解完整的情况,您需要阅读CPython虚拟机的实现ceval.c。对于LOAD_NAMELOAD_ATTR,oparg是代码对象的co_names属性中的索引:

>>> co.co_names
('heapq', 'nlargest', 'd')

对于LOAD_CONST指令,它是代码对象的co_consts属性中的索引值:
>>> co.co_consts
(3,)

对于CALL_FUNCTION来说,这个数字是要传递给函数的参数数量,在16位中编码,低字节表示普通参数的数量,高字节表示关键字参数的数量。

5
太棒了,有没有包含所有这些底层细节的参考资料/教程/书籍?我想深入了解。 - DevC
4
@DevC: 代码对象可以使用 inspect 模块进行文档化。字节码指令可以使用 dis 模块进行文档化。想要了解CPython虚拟机的实现细节,您需要阅读 ceval.c 的源代码。 - Gareth Rees
那么这个链接显示它是小端字节序,并且oparg长度为两个字节? - schemacs
是的,还有ceval.c文件中的NEXTARGPEEKARG宏。 - Gareth Rees
@GarethRees 如果你需要关于 ceval.c 的文档,可以参考执行模型 https://docs.python.org/3.3/reference/executionmodel.html - KeatsKelleher
显示剩余5条评论

110

我正在重新发布我的回答以回答另一个问题,目的是为了在搜索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          |   |

每一列都有特定的用途:

  1. 源代码中的相应行号
  2. 可选地指示正在运行的当前指令(当字节码来自帧对象时)
  3. 标签,表示从先前的指令可能会JUMP到此处
  4. 与字节码中对应的地址,该地址对应于字节索引(这些是2的倍数,因为Python 3.6使用2个字节作为每个指令,而在以前的版本中可能会有所不同)
  5. 指令名称(也称为opname),每个指令在dis模块中都有简要解释,它们的实现可以在ceval.c(CPython的核心循环)中找到
  6. 指令的参数(如果有的话),Python内部用它来获取一些常量或变量,管理堆栈,跳转到特定指令等
  7. 指令参数的易于理解的解释

7
这就是我要找的!但我在官方文档中没有找到,谢谢。 - Tarjintor
那个漂亮的输出表是手动创建的还是可以从dis中获取?我在文档中看到了这个https://docs.python.org/3/library/dis.html#dis.disco条目,建议我应该看到那些箭头和列,但在我的repl中我没有看到... - d8aninja
1
@d8aninja 我手动创建了表格以便于解释。 :) 你不应该期望在 dis 输出中看到这些列。 (2) 和 (3) 上的箭头是可能的,但这取决于你正在反汇编的代码。 - Delgan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接