91

我想了解如何使用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_POPetc. 是字节码指令(虽然有趣的是,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 
4

2 回答 2

109

您正在尝试反汇编包含源代码的字符串,但dis.disPython 2 不支持这种方式。使用字符串参数时,它会将字符串视为包含字节码(请参阅参考资料disassemble_stringdis.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,带有 oparg 0000;然后(在偏移量 3 处)6a是操作码LOAD_ATTR,带有0100oparg,依此类推。请注意,opargs 是 little-endian 顺序的,所以它0100是数字 1。未记录的opcode模块包含表格opname,为您提供每个操作码的名称,并opmap为您提供每个名称的操作码:

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

oparg 的含义取决于操作码,完整的故事你需要阅读 CPython 虚拟机ceval.c. ForLOAD_NAMELOAD_ATTRoparg 是co_names代码对象属性的索引:

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

因为LOAD_CONST它是co_consts代码对象属性的索引:

>>> co.co_consts
(3,)

对于CALL_FUNCTION,它是传递给函数的参数数量,以 16 位编码,低字节为普通参数的数量,高字节为关键字参数的数量。

于 2012-10-01T12:24:17.137 回答
83

我正在重新发布我对另一个问题的回答,以确保在谷歌搜索时找到它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. 指令参数的人性化解释
于 2017-11-28T10:32:16.040 回答