3.13 的 pyc 也能还原源码:pycdc 反编译上手指南
【免费下载链接】pycdcC++ python bytecode disassembler and decompiler项目地址: https://gitcode.com/GitHub_Trending/py/pycdc
手里攥着一个 Python 3.13 编译出来的.pyc,大多数反编译工具会直接报"不支持的版本"。pycdc(基于 C++ 的 Python 字节码反汇编/反编译器,项目自称 Decompyle++)是目前能把 3.13 字节码完整读进解析器的工具之一,从 1.0 到 3.13 的主版本它都建了映射表,编译一次,两种产物:pycdc负责还原源码,pycdas负责输出逐条指令的反汇编。
先说清楚边界,免得期望错位:
- 能干什么:把
.pyc还原成可读 Python 源码;对没有文件头的 marshalled code object(即marshal.dumps(compile(...))的输出)也能解析,但要手动指定版本;覆盖 1.0 到 3.13,共 30 多个版本。 - 不能干什么:还原出来的是语法级别的近似,注释、行内格式、部分推导式内部的临时变量名会丢;复杂动态语句可能退化成
pass加注释。把它当"能看懂逻辑"的工具,而不是"能替换原文件"的工具。
这个 pyc 是哪个 Python 版本编译的
.pyc文件头的前 4 个字节是 magic number(解释器用来声明字节码格式的版本指纹)。pycdc 在 pyc_module.cpp 里内置了 38 种 magic 的识别逻辑,文件一进来就按它路由到对应版本的解析器,不需要你手动告诉它。
想知道手上这个文件是什么版本,两个办法:
- 用
xxd file.pyc | head -1看头几字节,对照官方 magic number 表查一下; - 直接跑
pycdas,支持的文件会正常输出,不支持的会明确报"无法识别的版本"。
一个容易踩的坑:marshalled code object 没有文件头,magic 无从谈起。这类输入必须用-c声明它是 raw code object,再用-v 3.13手动指定版本,否则程序会直接拒绝加载。
从零构建 pycdc:两条命令的事
构建走 CMake,Linux/macOS 下两条命令就够(Windows 可以用生成的 MSVC 工程打开):
git clone https://gitcode.com/GitHub_Trending/py/pycdc && cd pycdc cmake -DCMAKE_BUILD_TYPE=Release . && make -j4编完得到两个可执行文件,用法都极简——源码打到标准输出,错误打到标准错误:
./pycdc module.pyc -o out.py # 反编译出源码 ./pycdas module.pyc # 只看逐条指令需要留意的选项就三个:-o指定输出文件,-c声明输入是 raw code object,-v x.y给它指定版本。日常反编译.pyc时一个都不用加。
Python 3.13 的字节码到底难在哪
3.13 的字节码对工具链不友好,原因不在"多了几条指令"这么简单,而是三个变化叠加:
- 宽指令格式。3.11 起大量指令的参数变成 2 字节宽(映射表里以
_A结尾的就是这类,如RESUME_A、LOAD_CONST_A),读错一个字偏移,后面整段代码全错位。 - 指令表持续扩容。pycdc 为 3.13 维护的 python_3_13.cpp 里有 138 条操作码映射,比 3.12 的 129 条多 9 条,其中
BEFORE_ASYNC_WITH、GET_AITER、ENTER_EXECUTOR_A、EXIT_EXECUTOR_A这些和 3.13 新执行器相关的指令,其他工具基本还没适配。 - 控制流指令语义重写。
RESUME从"计次断点"变成了带参数的版本,CLEANUP_THROW的异常栈清理方式和 3.12 之前完全不同,AST 构建逻辑要跟着重写。
| 版本 | 操作码映射数 | 备注 |
|---|---|---|
| 3.10 | — | 窄指令格式的最后一代 |
| 3.12 | 129 | 宽指令格式 |
| 3.13 | 138 | 新增异步执行器相关指令 |
这也是为什么"支持 3.13"不是改个版本号的事——每个版本一份独立映射表,bytes/ 目录下从python_1_0.cpp到python_3_13.cpp一共 30 多个文件,就是这种设计的直接体现。
报错时分两步排查
pycdc 的报错信息会告诉你它卡在哪一步,按顺序查:
- 连文件都加载不了(magic 不识别、头损坏):先确认版本,换
-c -v组合,或者确认这根本不是 code object。 - 能加载、但源码缺块:某个
code块里的指令没实现,pycdc 会跳过并继续。这时用pycdas反汇编,对照输出定位是哪条指令,判断是"工具没实现"还是"文件本身坏了"。
想验证工具本身工作正常,可以跑仓库自带的全量测试:
- 测试流程是:把每个 tests/input/ 里的
.py编译成对应版本的 pyc,反编译后用词法比对预期输出,tests/compiled/ 里已备好 190 个跨版本编译好的样例; - 构建后执行
make check JOBS=4即可,make check FILTER=async可以只跑匹配的子集。
批量处理目录下的所有 pyc,一行 shell 就够:
find . -name "*.pyc" -exec ./pycdc {} -o {}.src.py \;下一步
克隆仓库、编译,然后拿一个你手头最拿不准的 3.13 的.pyc跑一条./pycdas——指令能逐条列出来,就说明版本解析这条路是通的,剩下的事交给pycdc。
【免费下载链接】pycdcC++ python bytecode disassembler and decompiler项目地址: https://gitcode.com/GitHub_Trending/py/pycdc
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考