python

关注公众号 jb51net

关闭
首页 > 脚本专栏 > python > Python标准库对.pyc反编译

使用Python标准库对.pyc进行反编译的全过程

作者:feifeiyechuan

在生产环境中,我们经常只能拿到 Python 的编译文件(.pyc),而没有原始 .py 源码,本文记录一次完整的从 .pyc 到可读源码的实践过程,需要的朋友可以参考下

在生产环境中,我们经常只能拿到 Python 的编译文件(.pyc),而没有原始 .py 源码。
本文记录一次完整的.pyc 到可读源码的实践过程,并整理成可复用的“知识库条目”,后续你可以对其他 .pyc 复用同样的方法。

环境说明(模拟场景):

  • 解释器:CPython 3.x
  • 目标文件:module_a.pyc(某业务模块的编译文件)
  • 工具:只使用 Python 标准库 dis、marshal(无第三方反编译器依赖)

一、整体思路概览

面对一个 .pyc 文件,我们的目标是:

  1. .pyc 里拿到 Code 对象(Python 的内部字节码表示);
  2. dis 把字节码反汇编成可读的文本指令(生成 your_module_dis.txt 之类的文本);
  3. 基于该文本里的指令列表,手工/半自动还原出逻辑等价的 Python 源码(得到 recovered_module.py 这样的还原版源码);
  4. 通过对比运行效果,验证还原代码与原模块行为一致

本文重点记录第 ①~③ 步的细节和踩坑点,方便后续你作为“知识库”查阅。

二、CPython.pyc文件的基本结构

了解一点 .pyc 结构有助于理解为什么要先 f.read(16)

所以大致结构是:

[ 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)

关键点说明:

执行完成后,会在同目录下生成一个比较长的 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)

从这种输出格式中,可以看出:

这些信息就是后续还原源码时用于搭建结构的“骨架”,这里只是虚构的示例格式,不对应任何真实业务代码。

四、分析导出的文本:从字节码骨架还原模块结构

在导出的文本一开始,你一般会看到类似这样的模式(同样是虚构的模拟片段):

  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

还原模块级结构的一般步骤:

  1. 先关注所有 IMPORT_NAME / IMPORT_FROM:恢复顶层 import 语句;
  2. 找所有 STORE_NAME + 常量字符串:通常是模块级常量配置,如 code_str / URL / debug 开关;
  3. 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 ""
    ...

通用还原技巧:

通过这一套操作,可以逐步把若干核心函数(例如 extract_main_syms 等)都还原为结构清晰、类型标注完善的 Python 源码(在你自己的还原文件中,例如 recovered_module.py)。

六、把反汇编结果固化为“知识库”:还原源码的实现策略

在一次完整的实践中,我们可以在一个还原文件(例如 recovered_module.py)中遵循下面几个原则:

例如,顶层配置在还原后被集中到了模块开头(这里用模拟数据举例):

code_str: str = "DEMO_CODE"
ICD_service_url: str = "https://api.example.com/icd"
debug_mode: bool = False

对于某些主流程函数,可以按照原字节码中调用顺序,清晰划出若干步骤,例如:

这些结构在源码层面被“语义化”之后,不再只是 LOAD_CONST / JUMP_FORWARD 这样的指令堆,而是清晰的业务流程。

七、实践经验与踩坑记录

反编译 .pyc 到可维护源码,过程中有一些值得记录的点(下面都以虚构示例来说明思路):

1)优先搞清楚“输入/输出”约定

2)对复杂 if/for 结构,不要硬记指令,先画流程

3)保持一个“对照文件”

4)不要迷信自动反编译工具

八、如何复用这套方法处理其他.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

在新建的 .py 文件中,还原可读源码

把还原心得也整理到类似本篇这样的 markdown 中

九、小结

只要掌握了这一套流程,你在面对“只有 .pyc、没有源码”的场景时,就不会再那么无助,而是有一套稳定可复用的反编译+重构方法 论。

以上就是使用Python标准库对.pyc进行反编译的全过程的详细内容,更多关于Python标准库对.pyc反编译的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文