登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  python教程

Python pickletools.dis 怎样查看 pickle 字节码结构

来源:17golang原创

时间:2026-11-01 08:59:11 148浏览 收藏

排查一个 .pkl 文件时,我更愿意先看它的协议操作码,再决定是否需要恢复对象。Python 的 pickletools.dis() 正好负责这一步:它把 pickle 字节流展开成带位置、操作码和参数的符号化文本,不要求先执行反序列化逻辑。

如果目标是“看 pickle 里面大概写了什么、用了哪个协议、哪些位置引用了 memo”,用 pickletools.dis 或 python -m pickletools;如果目标是得到 Python 对象,才涉及 pickle.load 或 pickle.loads。来源不可信时不要跨过这个安全边界。

官方文档:https://docs.python.org/3/library/pickletools.html

先把“查看结构”和“恢复对象”分开

pickle 是面向 Python 对象的二进制序列化协议,操作码里可能包含类型、字符串、容器构造和全局名称等信息。pickletools 的职责是分析这些操作码;它不等于把字节流恢复成原来的对象。

命令行最省事的写法如下。这里的 # 是 shell 注释,只说明命令用途,不是输入文件的一部分:

# 只展开 pickle 操作码,不调用 pickle.load 或 pickle.loads
python -m pickletools sample.pkl

我在处理来源不明的文件时会先保留这份文本输出。这样能先观察协议版本、字符串和容器相关操作码,再决定是否需要交给隔离环境做进一步处理。不要为了“确认内容”直接把文件交给 pickle.load。

最小配方:用 pickletools.dis 输出可读结构

dis 接受 pickle 字节串或文件对象,默认把符号化反汇编写到标准输出。下面的示例只用本地构造的可信数据生成演示字节流;它的重点是查看协议结构,而不是展示反序列化。

import io
import pickle
import pickletools

# 只为演示生成可信的本地 pickle 字节流,不要对不可信输入调用 loads。
payload = pickle.dumps(
    {"name": "demo", "items": [1, 2]},
    protocol=4,
)

# 把 bytes 包成二进制文件对象,dis 会把操作码写到标准输出。
pickletools.dis(io.BytesIO(payload), annotate=1)

输出通常会包含偏移位置、操作码名称、参数和栈级别信息。annotate=1 会在输出中追加简短的操作码说明,便于第一次阅读;如果你只想保留紧凑结果,可以省略它。

pickle 字节流经过 io.BytesIO 交给 pickletools.dis 后展开为 OpcodeInfo、memo 与标准输出的静态结构图
图1:操作示意图。输入边界中的 pickle 字节流和 BytesIO 与 dis 的关系,以及反汇编结果如何落到操作码、memo 和文本输出;这是静态说明图,不是运行截图。

文件输入和输出位置怎么控制

分析现成文件时,不需要先把文件读成一个巨大的 bytes。直接把以二进制模式打开的文件对象传给 dis 即可。输出也可以从标准输出改到 io.StringIO,便于保存、截取或交给日志系统。

import io
import pickletools

# 以 rb 打开文件,避免文本编码层改变原始 pickle 字节。
with open("sample.pkl", "rb") as source:
    report = io.StringIO()

    # 把反汇编文本写入内存缓冲区,indentlevel 控制嵌套层级缩进。
    pickletools.dis(
        source,
        out=report,
        indentlevel=2,
        annotate=1,
    )

# 这里只读取分析文本,不会把 pickle 恢复成 Python 对象。
print(report.getvalue())

这里有三个容易混淆的参数:

  • out 是文本输出对象,不是新的 pickle 文件;
  • indentlevel 影响嵌套结构的显示缩进,不改变原始字节流;
  • annotate 是注释列位置提示,非零值会让操作码旁边多出简短说明。

需要筛选记录时改用 pickletools.genops

dis 适合人眼通读;如果我想找出某个位置、某类操作码或某个参数,就会改用 genops。它逐项返回 (opcode, arg, pos),其中 opcode 是 OpcodeInfo 对象,arg 是已解码的参数,pos 是该操作码在字节流中的偏移。

import pickletools

# 逐项读取操作码记录,只做结构分析,不恢复对象。
with open("sample.pkl", "rb") as source:
    for opcode, arg, pos in pickletools.genops(source):
        # 只打印稳定的名称、参数和偏移,方便后续筛选。
        print(f"pos={pos:04d} name={opcode.name} arg={arg!r}")

例如,排查协议版本时可以关注 PROTO;分析引用关系时可以关注 PUT、GET、BINPUT 或 BINGET。不要把这些名字当作 Python 业务字段,它们是 pickle 协议层的操作码。

pickle 文件通过二进制文件对象进入 pickletools.genops 并拆成 OpcodeInfo、arg、pos 与 memo 结构记录的静态图
图2:结果示意图。文件输入和 genops 的关系,以及每条结构记录中的 OpcodeInfo、arg、pos 与 memo;这是静态说明图,不是运行证据。

连续分析和 optimize 该怎么选

如果多个 pickle 片段来自同一个 pickler,dis 的 memo 参数可以复用同一个字典,让连续反汇编共享引用上下文。它适合分析一组有关联的片段,不代表线程安全,也不会把多个文件自动合并。

import io
import pickletools

# 同一分析会话复用 memo,用来保留连续 pickle 片段的引用上下文。
memo = {}
first = io.BytesIO(b"...")
second = io.BytesIO(b"...")

# 示例中的字节流必须替换为来自同一 pickler 的可信数据。
pickletools.dis(first, memo=memo, out=io.StringIO())
pickletools.dis(second, memo=memo, out=io.StringIO())

如果你的目标是得到语义等价但更紧凑的 pickle 字节串,可以了解 pickletools.optimize()。它会返回一个新的等价字节串并移除未使用的 PUT 操作码;这属于整理序列化结果,不是查看报告的必要步骤,也不应拿来处理未经信任的输入后再继续反序列化。

这几个边界最容易踩坑

目标适合的入口需要注意
查看协议和操作码python -m pickletools 或 pickletools.dis输出是结构说明,不是恢复后的对象
筛选偏移和参数pickletools.genopsarg 已被解析成 Python 值,但仍处于分析上下文
保留连续引用信息dis(..., memo=同一个字典)只对具有关联的连续片段有意义
恢复 Python 对象pickle.load 或 pickle.loads只处理明确可信且完整的输入

特别要注意,pickletools 的“更安全”只表示它专注于反汇编、不执行 pickle 字节码;它不是对输入来源的认证。文件是否被篡改、内容是否符合业务预期,仍需要来源控制、签名或隔离环境来解决。

最后的使用判断

把任务拆成“先看结构、再决定动作”后,选择就很清楚:人眼阅读用 dis,程序筛选用 genops,连续片段才考虑 memo,只有在信任边界已经明确时才考虑恢复对象。对大文件或不熟悉来源的文件,我会先保存反汇编文本和偏移位置,避免把一次排查变成一次不可逆的执行。

常见延伸问题

  • 为什么 pickletools.dis 看不到原始对象的完整业务结构? 因为它展示的是协议操作码和参数,不负责执行对象构造,也不保证还原出业务层字段关系。
  • genops 返回的 pos 有什么用? 它对应操作码在 pickle 字节流中的位置,适合把异常或可疑结构定位回原始文件偏移。
  • 看完反汇编后能直接调用 pickle.loads 吗? 只有输入来源、完整性和运行环境都可信时才考虑;反汇编本身不能替代信任判断。
声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>