Decompyle++(pycdc)使用指南:用 C++ 将任意版本 Python 字节码还原为源码
Decompyle++(pycdc)使用指南:用 C++ 将任意版本 Python 字节码还原为源码
Decompyle++ 是当前仓库 README.markdown 所介绍的 C++ 实现的 Python 字节码反汇编与反编译工具集,目标是把编译产物 .pyc 重新翻译回有效且人类可读的 Python 源码。它同时提供 pycdas(反汇编器)与 pycdc(反编译器)两个命令行工具,并覆盖从 Python 1.0 到 3.13 的全部字节码格式。读完本文,你将掌握 Decompyle++ 的完整构建流程、调试选项、测试机制,以及两个工具的全部命令行用法,包括普通 .pyc 文件与 marshal 序列化 code object 的反编译实战。
项目定位:从字节码回到源码
Decompyle++(名称取自 Decompyle 与 C++ 的合写)的目标非常明确:把编译后的 Python 字节码翻译回有效且人类可读的 Python 源码。与许多只针对某几个 Python 版本的反编译项目不同,Decompyle++ 的独特之处在于它寻求支持任何版本的 Python 字节码——从上古的 Python 1.0 到当前主流的 3.x 系列,均在其设计覆盖范围之内。
这一"全版本"承诺并非空谈,仓库中有多处实现层面的支撑:
- pyc_module.h 定义了从
MAGIC_1_0到MAGIC_3_13的全部.pyc魔数(magic number)常量,这是识别.pyc文件由哪个 Python 版本编译的判据; - CMakeLists.txt 将
bytes/目录下的python_1_0.cpp、python_1_3.cpp……python_3_13.cpp等 28 个版本专属实现全部编入pycxx静态库; - bytecode_ops.inl 中每个操作码都以注释标注了其存活版本区间,例如
UNARY_NOT标注Python 1.0 ->(至今仍存在),而BINARY_DIVIDE标注Python 1.0 - 2.7(Python 3 中已移除),说明操作码表是严格按各版本差异维护的。
从工具架构看,Decompyle++ 包含两套互补的产物:pycdas 负责将字节码展开为结构化的反汇编文本(含 code object 元信息、常量表、异常表等),pycdc 则进一步在控制流与 AST 层面完成语义还原,直接输出可读的 Python 源码。两者的入口实现分别位于 pycdas.cpp 与 pycdc.cpp。
构建 Decompyle++
环境与依赖
Decompyle++ 本身是纯 C++ 项目,构建系统采用 CMake](http://www.cmake.org),要求 CMake 版本不低于 3.12(见 [CMakeLists.txt),C++ 标准为 C++11(CMAKE_CXX_STANDARD 11)。构建过程不需要额外的第三方库,全部依赖均来自仓库自身源码。若需要运行测试套件,则还需环境中存在 Python 3.6 及以上版本的解释器(CMakeLists.txt 通过 find_package(Python3 3.6 COMPONENTS Interpreter) 查找)。
CMake 配置
在仓库根目录下新建构建目录并执行 CMake 生成工程或 Makefile:
mkdir build
cd build
cmake ..
README 特别说明了三个可用于控制调试功能的 CMake 选项,其作用与源码中的实现对应如下:
| CMake 选项 | 说明 | 对应实现 |
|---|---|---|
-DCMAKE_BUILD_TYPE=Debug |
产生调试符号(Debug 构建) | 标准 CMake 构建类型 |
-DENABLE_BLOCK_DEBUG=ON |
启用控制流块(block)调试输出 | 定义 -DBLOCK_DEBUG 宏(CMakeLists.txt) |
-DENABLE_STACK_DEBUG=ON |
启用栈模拟调试输出 | 定义 -DSTACK_DEBUG 宏(CMakeLists.txt) |
例如需要完整调试输出时:
cmake -DCMAKE_BUILD_TYPE=Debug -DENABLE_BLOCK_DEBUG=ON -DENABLE_STACK_DEBUG=ON ..
两个开关默认均为 OFF(CMakeLists.txt),仅在排查反编译算法问题时才需要打开。此外,针对 GCC/Clang 编译器,项目默认启用了 -Wall -Wextra -Werror 严格告警策略,MSVC 下则对应 /WX。
编译与安装
生成工程后即可编译。对于 MSVC 等 IDE 工程,直接打开生成的项目文件构建;对于 Makefile,执行:
make
构建产物为两个可执行文件 pycdas 与 pycdc,它们都链接自 pycxx 静态库(其中 pycdc 额外链接 ASTree.cpp 与 ASTNode.cpp,因为反编译器需要 AST 相关代码,见 CMakeLists.txt)。CMake 还配置了 install 目标,可将两个工具安装到 bin 目录(CMakeLists.txt):
make install
运行测试套件
在 *nix 或 MSYS 环境下,可用 make check 一键运行测试。README 给出的完整命令为:
make check JOBS=4
JOBS 参数控制并行测试的进程数。make check 目标实际调用的是 tests/run_tests.py(由 CMakeLists.txt 定义),因此也可绕过 CMake 直接运行:
python3 tests/run_tests.py --jobs 4
若只想运行部分测试,README 提供了 FILTER 参数:
make check JOBS=4 FILTER=test_functions
FILTER 同样可换为脚本的 --filter 参数,它按测试名做通配匹配(如 *test_functions*.txt)。
测试机制本身值得说明:仓库在 tests/input 中存放各语法特性的 .py 源文件(覆盖函数、类、循环、异常、推导式、f-string、yield from、协程等 80 余个用例),在 tests/compiled 中存放对应多版本编译出的 .pyc,而 tests/tokenized 则保存了期望输出的 token 序列。测试时,run_tests.py 用 pycdc 反编译 .pyc,再用 scripts/token_dump 将反编译结果与期望源码一并分词(忽略空白与注释差异,仅比较 token 流),逐字节比对是否一致(tests/run_tests.py)。此外,tests/xfail 目录存放已知可能失败的用例,用于统计 XFAIL 数量而不计入硬性失败。
使用 pycdas 反汇编字节码
基本用法
pycdas(PYC Disassembler)用于查看 .pyc 文件内部的原始结构。README 给出的调用方式:
./pycdas [PATH TO PYC FILE]
字节码反汇编结果打印到 stdout。这是分析 .pyc 结构、理解 Python 编译器输出最直接的途径。
命令行选项
从 pycdas.cpp 的参数解析代码可知,pycdas 支持以下选项:
| 选项 | 作用 |
|---|---|
-o <filename> |
将输出写入指定文件(默认 stdout) |
-c |
以编译后的 code object(marshalled)模式加载输入,需配合 -v |
-v <x.y> |
指定 code object 的 Python 版本(格式 x.y) |
--pycode-extra |
在 PyCode 对象转储中显示额外字段(如 3.11+ 的局部变量类型表、行号表) |
--show-caches |
不抑制 Python 3.11+ 反汇编中的 CACHE 指令 |
--help / -h |
显示帮助并退出 |
示例:
./pycdas example.pyc
./pycdas -o example.disasm.txt example.pyc
./pycdas --pycode-extra example.pyc
./pycdas --show-caches example_py312.pyc
其中 --show-caches 针对 Python 3.11 引入的 CACHE 指令——该类指令仅用于解释器内部缓存、不承载语义,默认被过滤,需要观察原始指令流时可显式开启(对应 DISASM_SHOW_CACHES 标志,见 bytecode.h)。
反汇编输出结构
pycdas 的输出由 pycdas.cpp 中的 output_object 递归生成,形如:
example.pyc (Python 3.8)
[Code]
File Name: example.py
Object Name: <module>
Arg Count: 0
Pos Only Arg Count: 0
KW Only Arg Count: 0
Locals: 1
Stack Size: 1
Flags: 0x00000000
[Names]
...
[Var Names]
...
[Constants]
...
[Disassembly]
...
具体字段按 Python 版本条件输出(从源码可直接确认):Qualified Name 仅 3.11+ 输出(pycdas.cpp)、Pos Only Arg Count 仅 3.8+(pycdas.cpp)、Free Vars/Cell Vars 仅 2.1 至 3.10 区间(pycdas.cpp)、<a href="https://link.gitcode.com/i/05504b0b3552588f6454742bc613d507" target="_blank">Exception Table] 仅 3.11+([pycdas.cpp)。字符串、元组、列表、字典、集合、数值等常量对象也会递归展开。这恰好体现了 Decompyle++ 对版本差异的细粒度处理。
使用 pycdc 反编译源码
基本用法
pycdc(PYC Decompiler)是核心的反编译工具。README 给出的调用方式:
./pycdc [PATH TO PYC FILE]
反编译得到的 Python 源码打印到 stdout,任何错误信息打印到 stderr——这一约定让反编译结果与错误信息可以清晰分离,便于脚本自动化处理:
./pycdc example.pyc > restored.py
./pycdc -o restored.py example.pyc # 等价写法
其命令行选项与 pycdas 基本一致(-o、-c、-v、--help,见 pycdc.cpp)。值得注意的是 pycdc 不支持 --pycode-extra 与 --show-caches——那是反汇编器特有的观察性选项。
输出与错误处理
pycdc 的成功输出会带两行由 pycdc.cpp 生成的头部注释:
# Source Generated with Decompyle++
# File: example.pyc (Python 3.8)
<反编译出的源码>
其中 (Python 3.8) 会显示从魔数识别出的实际版本;若为 Python 2 且启用了 Unicode 模式,还会追加 Unicode 字样。加载失败、版本解析失败、反编译异常等情形均以非零退出码结束并向 stderr 输出 Error ... 信息。
反编译的语义还原能力可从 AST 节点类型窥见一斑:ASTNode.h 定义了函数(NODE_FUNCTION)、类(NODE_CLASS)、调用(NODE_CALL)、导入(NODE_IMPORT)、推导式(NODE_COMPREHENSION)、格式化字符串(NODE_FORMATTEDVALUE、NODE_JOINEDSTR)、三元表达式(NODE_TERNARY)、注解变量(NODE_ANNOTATED_VAR)、链式赋值(NODE_CHAINSTORE)等三十余种节点类型,覆盖了现代 Python 语法的大多数构造。
处理 Marshalled Code Objects
除标准 .pyc 文件外,两个工具都支持marshalled code object——即通过 Python 标准库 marshal 直接序列化的 code 对象,例如 marshal.dumps(compile(...)) 的产物。这类输入不包含 .pyc 的魔数头,因此无法自动识别 Python 版本。
README 明确指出,使用该特性需要在命令行同时指定 -c 与 -v <version>:
# 先用 Python 生成一个 marshalled code object
python3 -c "import marshal; marshal.dump(compile('print(\"hello\")', '<string>', 'exec'), open('code.bin', 'wb'))"
# 反编译 / 反汇编该对象,必须显式指定版本
./pycdc -c -v 3.10 code.bin
./pycdas -c -v 3.10 code.bin
版本字符串必须采用 x.y 格式。从两个工具的源码可见(pycdc.cpp、pycdas.cpp):-v 参数会被解析为 major 与 minor 两个整数,若缺少小数点或小数点位于末尾(如 3.),会直接报错 Unable to parse version string (use the format x.y);若只给了 -c 而未给 -v,则会提示 Opening raw code objects requires a version to be specified。
内部实现原理
模块加载与魔数识别
入口 PycModule(pyc_module.h)负责加载工作:普通模式下 loadFromFile 读取 .pyc 头部的魔数,通过 pyc_module.h 中定义的 MAGIC_1_0 ~ MAGIC_3_13 常量表映射出版本号;marshalled 模式下 loadFromMarshalledFile 直接使用 -v 指定的版本。isValid() 用于确认版本信息已成功解析。
字节码定义与操作码映射
每个 Python 版本的操作码集由 bytecode_ops.inl 统一维护,并通过 bytecode.h 的宏展开生成 Opcode 枚举;ByteToOpcode 负责把各版本原始操作码数值映射到统一枚举,bc_disasm 则负责具体指令的反汇编输出。Python 3.11 后新增的异常表解析由 bc_exceptiontable 承担(bytecode.h)。
从字节码到 AST
pycdc 的还原路径比反汇编更深一层:ASTree.h 声明了核心入口 BuildFromCode——它把 PycCode 中的指令流转换为中间表示的 AST 节点树(实现于 ASTree.cpp),再由 decompyle 调用 print_src 将 AST 树美化打印为源码文本。控制流结构(if/else、try/except/finally、while/for、with)在 ASTNode.h 中以 ASTBlock 及其子类表达,条件块、迭代块、容器块分别承担对应结构的还原。这一"字节码 → 栈/控制流分析 → AST → 源码"的三层流水线,正是 Decompyle++ 区别于简单指令打印器的关键。
作者与许可
Decompyle++ 由 Michael Hansen 与 Darryl Pogue 创作,并接受了 charlietang98、Kunal Parmar、Olivier Iffrig、Zlodiy 等人的额外贡献。项目以 GNU General Public License version 3(GPL v3)条款发布,完整许可文本见 LICENSE 文件。
需要提醒的是:反编译能力是把双刃剑,请仅对你有权分析的字节码使用该工具,并遵守目标代码的许可协议。