MediaTea 6小时前
Python 标准库:dis(字节码反汇编与执行机制分析)
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_keji1.html

 

dis 是 Python 标准库中用于分析字节码的模块,可以将编译后的指令转换为便于阅读的形式,帮助观察变量访问、运算、函数调用和控制流程的实现方式。

它适合学习程序执行机制、比较不同写法的编译结果,以及辅助定位异常或分析性能问题。反汇编展示的是解释器层面的指令结构,不能直接代替运行测试和性能测量。

一、导入与常见应用场景

1、导入模块

dis 随 Python 提供,无需额外安装。

import dis

本文以 CPython 为分析对象。字节码属于解释器实现细节,指令名称、参数含义和输出格式可能随解释器版本变化,不应视为 Python 语言固定不变的规则。

2、常见应用场景

• 理解执行机制:观察表达式如何加载数据、执行运算并返回结果。

• 分析控制流程:查看条件分支、循环和短路求值对应的指令。

• 比较代码结构:分析不同写法生成的指令及代码对象信息。

• 观察编译优化:检查常量折叠等优化是否发生。

• 辅助调试:结合异常回溯定位发生错误的指令。

3、与相关模块的分工

• dis:分析编译后的字节码与相关信息。

• ast:分析源代码的语法结构。

• inspect:获取函数、类、参数和源码等对象信息。

• timeit:测量小段代码的执行时间。

• cProfile:分析程序运行过程中各函数的调用与耗时。

dis 主要回答 " 代码被编译成了什么结构 ";执行频率和实际耗时通常需要其他工具测量。

二、核心概念

1、源代码、字节码与机器码

源代码(Source Code)是编写的 Python 文本。

在 CPython 的常规执行流程中,源代码经过解析和编译,形成包含字节码的代码对象,再由解释器执行。

字节码(Bytecode)是供解释器处理的指令表示,不等同于处理器直接执行的机器码(Machine Code)。dis 展示的是字节码层面的反汇编,而不是硬件汇编指令。

2、代码对象

代码对象(Code Object)保存编译后的指令及相关信息,例如常量、变量名称和源码位置。

普通 Python 函数通过 __code__ 属性关联代码对象:

代码对象与函数对象不同。函数还关联全局命名空间、默认参数和闭包等执行所需的信息。

3、指令与操作参数

一条字节码指令通常包含操作码(Opcode),并可能带有操作参数。

操作参数不一定是源代码中的数值,也可能表示常量索引、变量索引、运算类型或跳转信息。

阅读时应结合指令名称与参数解释,不宜单独根据参数数字推断作用。

4、求值栈

CPython 的许多指令通过求值栈(Evaluation Stack)传递中间结果。

例如,计算 a + b 时,可以从概念上理解为:

• 取得 a 和 b 的值。

• 执行加法,将结果作为新的中间值。

• 将结果返回给调用方。

实际输出可能将多个动作组合为一条指令,或采用专门化指令,因此不必强求每个源码动作都对应一条独立指令。

三、使用 dis.dis ( ) 查看字节码

1、反汇编函数

dis.dis ( ) 默认将反汇编结果写入标准输出,函数本身返回 None。

这里传入的是函数对象 add,不会调用该函数。若写成 dis.dis ( add ( 2, 3 ) ) ,则会先计算函数结果,再尝试对结果进行反汇编,通常不符合分析目的。

2、阅读输出中的信息

反汇编输出主要包含:

• 源码位置:指令对应的行号或更详细的位置。

• 逻辑标签:用于标记跳转目标等位置。

• 指令名称:说明该指令执行的操作。

• 操作参数:指令使用的数字参数。

• 参数解释:变量名、常量值、运算符或跳转目标等可读信息。

当前输出通常使用逻辑标签表示跳转目标。需要同时查看指令偏移量和更详细的源码位置时,可以使用:

偏移量用于定位字节码中的位置,不是源代码行号;源码位置与字节码指令也不一定一一对应。

3、理解常见指令类别

阅读输出时,可以先关注指令的作用类别:

• LOAD_CONST:加载常量。

• LOAD_FAST、LOAD_FAST_BORROW 等:访问局部变量,具体指令具有不同的内部语义。

• STORE_FAST:将值保存到局部变量。

• LOAD_GLOBAL:查找全局名称或内置名称。

• BINARY_OP:执行由参数指定的二元或原地操作。

• COMPARE_OP:执行比较操作。

• CALL:执行调用相关操作。

• RETURN_VALUE:返回栈顶结果。

这些名称只是阅读入口。实际代码还可能使用组合指令或特化指令,应以当前解释器的输出与对应定义为准。

4、分析源代码字符串

dis 会先编译字符串,再展示反汇编结果,不会执行其中的赋值和乘法。因此,分析时不要求 price 和 quantity 已经有值。

也可以显式创建代码对象:

eval 模式用于表达式,exec 模式用于语句序列。选择不同模式,生成的代码结构也可能不同。

5、控制嵌套代码的展开

depth=0 只显示当前代码对象,不递归展开其中的嵌套代码对象。

省略 depth 时,dis 会继续分析嵌套代码对象。这里控制的是反汇编展示的递归深度,不是程序运行时的递归限制。

四、结构化读取指令与代码信息

1、遍历 Instruction 对象

需要在程序中分析指令时,使用 get_instructions ( ) 比解析打印文本更合适。

get_instructions ( ) 返回迭代器,每次产生一个指令对象(Instruction)。

常用字段包括:

• offset:指令在字节码中的偏移位置。

• opname:可读的指令名称。

• opcode:操作码数值。

• arg:数字形式的操作参数,没有参数时为 None。

• argval:解析后的参数值。

• argrepr:参数的可读描述。

• is_jump_target:是否为其他指令的跳转目标。

2、查看源码位置

位置对象包含起止行号与列偏移。部分位置数据可能为 None,分析程序应考虑信息缺失的情况。

列偏移按源码的 UTF-8 字节偏移表示。源码包含中文等非 ASCII 字符时,不能直接将它当作 Python 字符串的字符索引切片。

3、查看代码对象摘要

结果通常包含函数名称、文件名、参数数量、局部变量数量、栈空间需求、常量和名称等信息。

也可以直接打印:

code_info ( ) 返回字符串,show_code ( ) 直接输出信息。

其中的栈空间信息描述求值栈所需的槽位,不表示函数调用次数、递归深度或内存字节数。

4、使用 Bytecode 对象

Bytecode 将常用分析功能组织在一个对象中:

• info ( ) :返回代码对象摘要字符串。

• dis ( ) :返回反汇编结果字符串。

• 迭代对象:逐项获取 Instruction。

因此,需要将反汇编结果保存到变量时,可使用 Bytecode.dis ( ) ;不宜将 dis.dis ( ) 的返回值当作结果文本。

五、分析条件、循环与编译优化

1、条件分支

阅读时可以依次寻找:

• score 与常量的加载。

• 比较操作。

• 根据比较结果决定执行路径的跳转。

• 各分支的返回操作。

反汇编列出的是可能执行的指令结构。一次函数调用通常只经过其中一条路径,不会按输出顺序执行所有分支。

2、循环结构

循环中通常可以观察到获取迭代器、取得下一项、更新累计值和返回循环位置等操作。

循环体的指令在反汇编中只出现一份,但运行时可能重复执行很多次。因此,静态指令数量不等于实际执行次数。

3、短路求值

or 会根据第一个操作数的真值决定是否需要取得第二个操作数。

该表达式返回某个操作数本身,不会统一转换为 True 或 False。可以结合跳转和返回位置观察这种选择行为。

4、常量折叠

在常见 CPython 编译结果中,纯常量表达式 2 + 3 * 4 会提前计算为 14;第二个函数中的 3 * 4 也可能提前合并为 12,而 value 相关的加法仍需要运行时执行。

这种编译期处理称为常量折叠(Constant Folding)。

它说明源代码中的每个运算符不一定对应一次运行时运算。具体优化范围由编译器决定,不能据此认为所有看似固定的表达式都会被提前计算。

六、运行时特化与内联缓存

1、观察运行后的指令形式

CPython 可以根据运行时情况,对部分操作采用更有针对性的执行形式,这类机制称为特化(Specialization)。

adaptive=True 用于显示可能经过运行时特化的字节码。

重复调用使解释器有机会观察操作数类型等信息,但不保证一定出现某种指令。特化结果受解释器实现、构建配置、调用历史和输入类型影响。

2、查看内联缓存

内联缓存(Inline Cache)用于保存解释器执行某些操作时可复用的信息,通常与前面的指令关联。

show_caches=True 会显示这些缓存条目。它们不是源代码中的独立语句,也不能简单按普通业务操作累计。

当前的 get_instructions ( ) 不再将缓存条目作为独立 Instruction 返回;相关信息可通过指令对象的 cache_info 字段读取。不要将它与 dis.dis ( ) 的缓存显示方式混为一谈。

3、分析结果的边界

默认反汇编与特化视图展示的层次不同。比较两段代码时,应明确是否启用了 adaptive 和缓存显示。

dis 不会提供完整的处理器机器码视图;运行时优化也可能超出普通字节码输出能够直接呈现的范围。

七、异常定位与命令行使用

1、根据异常回溯定位指令

Bytecode.from_traceback ( ) 根据回溯创建分析对象,并标记引发异常的指令位置。

本例可将错误与除法操作联系起来。它提供的是指令位置,具体原因仍需要结合异常类型、输入数据和调用上下文判断。

2、在终端反汇编文件

将需要分析的代码保存为 example.py,然后运行:

该命令读取并编译源文件,输出反汇编结果,不会执行文件中的顶层业务代码。

常用选项包括:

• -C:显示内联缓存。

• -O:显示指令偏移量。

• -P:显示更详细的源码位置。

例如:

这里的 -O 位于 -m dis 之后,是 dis 的显示选项;python -O -m dis example.py 中的 -O 则是解释器的优化选项,两者作用不同。

3、保存反汇编结果

也可以将 dis.Bytecode ( square ) .dis ( ) 返回的字符串写入文件,便于归档或比较。

八、应用举例:比较两种列表构造方式

以下示例比较普通循环和列表推导式的编译结构,并将代码摘要、反汇编结果和指令名称统计保存为报告。

报告保存在 bytecode_report.txt,再次运行会覆盖同名文件。

阅读报告时,可以关注普通循环中的方法访问与调用,以及列表推导式中可能出现的 LIST_APPEND 等操作,理解两种写法的编译结构。

示例只统计各函数当前代码对象中的指令,不递归累计嵌套代码对象;运行时执行次数也不包含在该统计中。

如果需要比较耗时,可以在上述函数定义之后补充:

这些时间还包含函数调用及计时包装的开销,应在相同环境和相同输入下比较,并结合实际业务规模判断。

指令更少不一定意味着执行更快。一个调用指令可能触发大量底层工作,而数据类型、缓存状态和内存分配也会影响耗时。

九、常用函数与类速览

以下列出常用调用形式,省略部分可选参数。

• dis.dis ( x ) :输出反汇编结果,返回 None。

• dis.dis ( x, file=file ) :将反汇编结果写入指定文本流。

• dis.dis ( x, depth=0 ) :只分析当前层级,不递归展开嵌套代码对象。

• dis.dis ( x, show_offsets=True ) :显示指令偏移量。

• dis.dis ( x, show_positions=True ) :显示更详细的源码位置。

• dis.dis ( x, adaptive=True ) :显示可能经过运行时特化的字节码。

• dis.dis ( x, show_caches=True ) :显示内联缓存。

• dis.get_instructions ( x ) :返回 Instruction 对象的迭代器。

• dis.code_info ( x ) :返回代码对象摘要字符串。

• dis.show_code ( x ) :直接输出代码对象摘要。

• dis.Bytecode ( x ) :创建可迭代的字节码分析对象。

• Bytecode.dis ( ) :返回格式化的反汇编字符串。

• Bytecode.info ( ) :返回代码对象摘要字符串。

• dis.Bytecode.from_traceback ( tb ) :根据异常回溯创建分析对象并标记相关指令。

• dis.stack_effect ( opcode, oparg=None, *, jump=None ) :计算指令对求值栈深度的净影响。

十、补充说明

1、字节码与语言语义

Python 语言规定表达式和语句的行为,CPython 字节码描述某种具体实现。

学习时应先理解代码语义,再用反汇编观察实现,不宜将特定指令组合当作编写 Python 程序必须遵守的固定模式。

2、结构分析与性能判断

dis 展示指令结构,不记录每条指令实际执行了多少次,也不直接测量耗时。

发现可疑结构后,应结合 timeit、cProfile 或实际任务测试验证,再决定是否优化。

3、编译分析与代码执行

反汇编普通函数对象或编译源代码字符串,不会调用被分析的函数或执行业务语句。

但为了取得函数而导入模块时,模块顶层代码仍可能执行;特化示例中的重复调用和性能测量也会实际运行目标函数。

4、反汇编与源码恢复

dis 不能完整恢复原始源码。变量名和部分位置信息可能保留,但注释、排版以及某些源码结构已经在编译过程中丢失或改变。

因此,反汇编结果适合分析执行结构,不能替代源码备份或作为完整的反编译结果。

5、栈变化与执行成本

stack_effect ( ) 返回的是某条指令对求值栈深度的净影响,不是执行时间或内存消耗。

对于存在跳转的指令,跳转与不跳转时的栈变化可能不同。计算整个函数的栈行为时,需要结合控制流程,不能简单将所有指令的结果相加。

小结

dis 将 CPython 字节码转换为可读指令,帮助理解变量访问、控制流程、编译优化和运行时特化。分析时应结合源码语义与解释器环境;涉及性能结论,还需通过计时或性能分析工具验证。

"点赞有美意,赞赏是鼓励"

智客推

智客推

ZAKER 智客推 GEO | AI 时代的品牌认知解决方案

一起剪

一起剪

ZAKER旗下免费视频剪辑工具

相关标签

python 源代码 阅读
相关文章
评论
没有更多评论了
取消

登录后才可以发布评论哦

打开小程序可以发布评论哦

12 我来说两句…
打开 ZAKER 参与讨论