使用Python标准库对.pyc进行反编译的全过程
在生产环境中,我们经常只能拿到 Python 的编译文件(.pyc),而没有原始 .py 源码。
本文记录一次完整的从 .pyc 到可读源码的实践过程,并整理成可复用的“知识库条目”,后续你可以对其他 .pyc 复用同样的方法。
环境说明(模拟场景):
- 解释器:CPython 3.x
- 目标文件:module_a.pyc(某业务模块的编译文件)
- 工具:只使用 Python 标准库 dis、marshal(无第三方反编译器依赖)
一、整体思路概览
面对一个 .pyc 文件,我们的目标是:
- 从
.pyc里拿到 Code 对象(Python 的内部字节码表示); - 用
dis把字节码反汇编成可读的文本指令(生成your_module_dis.txt之类的文本); - 基于该文本里的指令列表,手工/半自动还原出逻辑等价的 Python 源码(得到
recovered_module.py这样的还原版源码); - 通过对比运行效果,验证还原代码与原模块行为一致。
本文重点记录第 ①~③ 步的细节和踩坑点,方便后续你作为“知识库”查阅。
二、CPython.pyc文件的基本结构
了解一点 .pyc 结构有助于理解为什么要先 f.read(16):
- 前 16 字节是头部(header),包含:
- 魔数(magic number)
- 标志位(flags)
- 时间戳 / 源文件哈希
- 源码大小等
- 后面才是真正的
code object数据,通过marshal模块进行序列化。
所以大致结构是:
[ 16 字节头部 ] + [ marshal.dump(code_object) 的二进制数据 ]
只要我们跳过前 16 字节,再用 marshal.load 读取,就能拿回一个可以被 dis 反汇编的 code 对象。
三、用dis + marshal导出字节码到文本(示例脚本)
核心导出脚本如下(示例脚本 disassemble_example.py):
import dis, marshal, types
# 示例:从业务模块 module_a.pyc 中读取字节码
with open(r"D:\demo_project\bin\module_a.pyc", "rb") as f:
f.read(16) # 跳过头部
code = marshal.load(f)
with open("your_module_dis.txt", "w", encoding="utf-8") as out:
dis.dis(code, file=out)
关键点说明:
f.read(16):跳过.pyc头 16 字节,只保留真正的字节码部分;marshal.load(f):从剩余二进制流中反序列化出一个code对象;dis.dis(code, file=out):对这个顶层code对象做反汇编,结果写入your_module_dis.txt。
执行完成后,会在同目录下生成一个比较长的 your_module_dis.txt,里面是类似这样的内容(下面是完全虚构的模拟输出片段,用于说明格式与思路):
0 0 RESUME 0
2 2 LOAD_CONST 0 (0)
4 LOAD_CONST 1 (None)
6 IMPORT_NAME 0 (json)
8 STORE_NAME 0 (json)
3 10 LOAD_CONST 0 (0)
12 LOAD_CONST 1 (None)
14 IMPORT_NAME 1 (re)
16 STORE_NAME 1 (re)
...
66 74 LOAD_CONST 6 (<code object extract_main_syms at 0x..., file "/app/demo_project/module_a.py", line 19>)
104 MAKE_FUNCTION 0
106 STORE_NAME 15 (extract_main_syms)
从这种输出格式中,可以看出:
- 模块会先导入若干依赖模块;
- 然后依次把若干
code object绑定为函数。
这些信息就是后续还原源码时用于搭建结构的“骨架”,这里只是虚构的示例格式,不对应任何真实业务代码。
四、分析导出的文本:从字节码骨架还原模块结构
在导出的文本一开始,你一般会看到类似这样的模式(同样是虚构的模拟片段):
11 74 LOAD_CONST 0 (0)
76 LOAD_CONST 2 (('log_info', 'log_error'))
78 IMPORT_NAME 9 (core)
80 IMPORT_FROM 10 (log_info)
82 STORE_NAME 10 (log_info)
84 IMPORT_FROM 11 (log_error)
86 STORE_NAME 11 (log_error)
88 POP_TOP
13 90 LOAD_CONST 3 ('DEMO_CODE')
92 STORE_GLOBAL 12 (code_str)
15 94 LOAD_CONST 4 ('https://api.example.com/i')
96 STORE_NAME 13 (service_url)
16 98 LOAD_CONST 5 (False)
100 STORE_NAME 14 (debug_mode)
结合经验,可以直接还原为类似下面的伪代码形式(示例依然是虚构的,注意其中的配置项和值都只是演示用):
from core import log_error, log_info code_str: str = "DEMO_CODE" ICD_service_url: str = "https://api.example.com" debug_mode: bool = False
还原模块级结构的一般步骤:
- 先关注所有
IMPORT_NAME/IMPORT_FROM:恢复顶层import语句; - 找所有
STORE_NAME+ 常量字符串:通常是模块级常量配置,如code_str/ URL / debug 开关; - 找
LOAD_CONST (<code object ...>) + MAKE_FUNCTION + STORE_NAME:- 可以直接得到函数名:
STORE_NAME 15 (extract_main_syms)→def extract_main_syms(...): - 然后在
Disassembly of <code object extract_main_syms ...>下面继续分析函数内部逻辑。
- 可以直接得到函数名:
到这里为止,我们已经能搭出一个大致的模块轮廓。
五、函数内部:从字节码反推高层逻辑(虚构示例)
拿某个函数(例如 extract_main_syms)举例,在导出文本中它的反汇编开头可能类似(示意片段,完全虚构):
Disassembly of <code object extract_main_syms at 0x..., file "/app/demo_project/module_a.py", line 19>:
19 0 RESUME 0
20 2 BUILD_LIST 0
4 STORE_FAST 2 (result_syms)
21 6 LOAD_FAST 0 (input_data)
8 LOAD_CONST 1 ('record')
10 BINARY_SUBSCR
20 LOAD_CONST 2 ('main_field')
22 BINARY_SUBSCR
32 STORE_FAST 3 (main_text)
22 34 LOAD_FAST 0 (input_data)
36 LOAD_CONST 1 ('record')
38 BINARY_SUBSCR
48 LOAD_CONST 3 ('history_field')
50 BINARY_SUBSCR
60 STORE_FAST 4 (history_text)
根据变量名,可以还原出类似这样的伪代码(同样是虚构示例):
def extract_main_syms(input_data: Dict[str, Any]) -> List[str]:
result_syms: List[str] = []
record = input_data.get("record", {}) or {}
history_text: str = record.get("history_field", "") or ""
...
通用还原技巧:
BUILD_LIST 0 + STORE_FAST (xxx)→xxx = [];LOAD_FAST ... BINARY_SUBSCR多连串 → 多级下标 /dict取值;- 频繁出现的字符串常量(中文问诊要点、病史名称)→ 可以直接在源码里用同样的文本补回去;
CALL_FUNCTION/ `CALL_METHOD`` 结合函数名、参数个数,大多数时候可以明确调用意图。
通过这一套操作,可以逐步把若干核心函数(例如 extract_main_syms 等)都还原为结构清晰、类型标注完善的 Python 源码(在你自己的还原文件中,例如 recovered_module.py)。
六、把反汇编结果固化为“知识库”:还原源码的实现策略
在一次完整的实践中,我们可以在一个还原文件(例如 recovered_module.py)中遵循下面几个原则:
- 保持对外行为一致:函数名、参数、返回结构尽量和原模块保持兼容;
- 逻辑等价优先,而非逐指令对齐:我们关注的是“相同输入 → 相同输出”,不追求一模一样的实现写法;
- 适当增加类型标注和注释:方便后续维护和阅读,特别是复杂的业务流程;
- 抽出可配置项:如
code_str、ICD_service_url、日期限制date_limite等统一放在模块顶部。
例如,顶层配置在还原后被集中到了模块开头(这里用模拟数据举例):
code_str: str = "DEMO_CODE" ICD_service_url: str = "https://api.example.com/icd" debug_mode: bool = False
对于某些主流程函数,可以按照原字节码中调用顺序,清晰划出若干步骤,例如:
- 权限或授权有效期检查;
- 输入参数解析及预处理;
- 关键信息抽取、实体标注或特征工程;
- 业务规则判断、打分与过滤;
- 结果归组、加权及 Top-N 输出。
这些结构在源码层面被“语义化”之后,不再只是 LOAD_CONST / JUMP_FORWARD 这样的指令堆,而是清晰的业务流程。
七、实践经验与踩坑记录
反编译 .pyc 到可维护源码,过程中有一些值得记录的点(下面都以虚构示例来说明思路):
1)优先搞清楚“输入/输出”约定
- 先从接口调用方(如外部服务或其它模块)入手,反向推函数参数含义,比直接看字节码更有效。
2)对复杂 if/for 结构,不要硬记指令,先画流程
- 可以根据
JUMP_IF_FALSE_OR_POP、POP_JUMP_FORWARD_IF_FALSE之类的跳转位置,画一张小流程图,再还原成if/elif/else。
3)保持一个“对照文件”
- 建议始终保留一对文件:例如
your_module_dis.txt作为原始反汇编结果,recovered_module.py是还原后的源码,两边对照非常重要。
4)不要迷信自动反编译工具
- 对于逻辑复杂、包含大量中文业务规则的代码,自动反编译经常给出难以阅读的结果,
手工+半自动(基于dis的文本)更适合做“可维护的重构”。
八、如何复用这套方法处理其他.pyc
当你以后再遇到一个新的 .pyc,可以按下面的模板操作(完全和具体业务无关,只关注技术流程):
准备一个类似前文的 disassemble_example.py,只改文件路径:
import dis, marshal
with open(r"你的目标文件.pyc", "rb") as f:
f.read(16)
code = marshal.load(f)
with open("your_module_dis.txt", "w", encoding="utf-8") as out:
dis.dis(code, file=out)
打开生成的 your_module_dis.txt:
- 先定位所有
IMPORT_NAME/MAKE_FUNCTION + STORE_NAME,搭好模块骨架; - 再逐个函数分析
Disassembly of <code object ...>部分。
在新建的 .py 文件中,还原可读源码:
- 先实现外部接口签名;
- 再根据字节码实现内部逻辑;
- 最后写一些测试用例,对比行为。
把还原心得也整理到类似本篇这样的 markdown 中
- 方便你自己未来查阅,也方便团队里其他同事快速接手。
九、小结
- 使用 Python 标准库的
marshal + dis,可以在不依赖第三方库的前提下,从.pyc中获取字节码并反汇编到文本; - 基于反汇编文本(如
your_module_dis.txt),可以逐步还原出结构清晰的源码文件(例如recovered_module.py),作为长期维护版本; - 将整个流程和经验沉淀成 markdown 知识库(本文)后,可以非常方便地复用于今后所有
.pyc分析与还原工作。
只要掌握了这一套流程,你在面对“只有 .pyc、没有源码”的场景时,就不会再那么无助,而是有一套稳定可复用的反编译+重构方法 论。
以上就是使用Python标准库对.pyc进行反编译的全过程的详细内容,更多关于Python标准库对.pyc反编译的资料请关注脚本之家其它相关文章!
相关文章
Python中使用socket发送HTTP请求数据接收不完整问题解决方法
这篇文章主要介绍了Python中使用socket发送HTTP请求数据接收不完整问题解决方法,本文使用一个循环解决了数据不完整问题,需要的朋友可以参考下2015-02-02


最新评论