pycdecompiler
把 Python 的 .pyc 反编译回源码,内核基于 Decompyle++ / pycdc,
用 C++ 实现反编译内核,通过 C ABI 动态库 暴露,再由 Python(ctypes)封装成可调用的函数。
内核源码已经内置在本仓库的
native/目录(以前是外挂的pycdc-master/), 克隆下来直接python build_native.py就能用,不需要额外下载任何东西。
支持 Python 1.0 ~ 3.15 的字节码;本项目重点补全了 3.12 / 3.13 的实现, 并验证了 3.1 / 3.11 等版本。
| 版本 | 0.1.2.2 |
| 作者 | hyy hyy@hyysn.cn |
| 仓库 | https://github.com/hyy-aaa/pycdecompile |
| PyPI | https://pypi.org/project/pycdecompile/ |
| 许可 | GPL-3.0-only(内核 Decompyle++ 为 GPLv3,详见「许可与致谢」) |
pip install pycdecompile
import pycdecompiler as pc
print(pc.decompile_file("app.pyc")) # 磁盘 pyc -> 源码
print(pc.decompile(open("app.pyc","rb").read()))
print(pc.decompile_code_object(compile("a = 1 + 2", "<s>", "exec")))
pycdecompile app.pyc -o app.py # 命令行(装了包之后可用)
python -m pycdecompiler app.pyc # 等价写法
1. 目录结构
pycdecompile/
├── native/ Decompyle++ 内核源码(内置,含本项目新增/修改)
│ ├── pycdecompile_api.h/.cpp ★ 新增:C ABI 导出层
│ ├── pycdecompile_log.h/.cpp ★ 新增:内核诊断信息出口
│ ├── ASTree.cpp ★ 修改:补全 3.12 ~ 3.15 指令
│ ├── ASTNode.h ★ 修改:新增 setter
│ ├── data.cpp ★ 修改:std::exit → 抛异常、UTF-8 路径
│ ├── pyc_module.h/.cpp ★ 修改:支持内存加载 / 放行 3.13 ~ 3.15
│ ├── bytes/python_*.cpp 各版本 opcode 映射表(1.0 ~ 3.15)
│ ├── CMakeLists.txt 可选的 CMake 构建(等价于 build_native.py)
│ └── README.md 内核目录说明 + 相对上游的改动清单
├── tests_data/ 上游回归语料(input/compiled/tokenized/xfail)
├── pycdecompiler/ ★ Python 包(对外 API)
│ ├── __init__.py 导出全部公开函数
│ ├── core.py 对外 Python 函数实现
│ ├── _native.py ctypes 绑定
│ └── cli.py / __main__.py 命令行入口
├── build_native.py ★ 一键构建动态库(增量、可并行)
├── setup.py / pyproject.toml ★ 打包(PyPI: sdist / wheel)
├── MANIFEST.in sdist 要带上的文件(含内核源码)
├── LICENSE GPLv3 全文(与 native/LICENSE 一致)
├── example.py ★ 使用示例(当前解释器 / 3.1 / 3.12)
├── tests_pycdecompiler.py ★ pytest 单元测试
└── tools/ ★ 分析与回归工具
├── official_test.py 按上游 tokenized 口径回归
├── corpus_test.py AST 级别比对
├── gap_analysis.py 查 ASTree 缺哪些 opcode
├── needed_opcodes.py 逐文件定位缺失 opcode
├── diff_one.py 单个文件差异对比
├── dump_dis.py 导出各语法结构的反汇编参考
├── show_syntax.py 反编译标准库并展示 ast.parse 失败处上下文
├── stress_stdlib.py 标准库压力测试(子进程隔离,统计 ok/warn/syntax/error/CRASH)
├── stress_multi.py 指定解释器(3.13/3.14/3.15…)编译标准库后压力测试
├── gen_opcode_map.py 从目标解释器导出 bytes/python_X_Y.cpp 并检查缺口
└── corpus/ 针对 3.11 语法结构的回归语料
├── unpack_star_311.py 星号解包 / ** 字典展开 / 星号显示
├── try_patterns_311.py try/except 各种写法
├── loops_311.py while / break / continue / while-else
└── none_test_311.py is None 系列条件
2. 架构
┌──────────────────────────────────────────┐
│ Python (pycdecompiler 包) │
│ decompile_file / decompile_code_object │
└───────────────┬──────────────────────────┘
│ ctypes
┌───────────────▼──────────────────────────┐
│ pycdecompile.dll (C ABI,extern "C") │
│ pycdc_decompile_file / _data / ... │
└───────────────┬──────────────────────────┘
│ C++
┌───────────────▼──────────────────────────┐
│ pycxx 内核 │
│ PycModule(pyc 头 + marshal 解析) │
│ PycCode / PycObject …(对象模型) │
│ ASTree.cpp(字节码 → AST → 源码) │
└──────────────────────────────────────────┘
三层职责分明:
| 层 | 文件 | 说明 |
|---|---|---|
| 内核 | native/*.cpp |
解析 .pyc、反汇编、构建 AST、打印源码 |
| 导出层 | pycdecompile_api.cpp |
把内核包成稳定 C 接口,消化所有异常 |
| 调用层 | pycdecompiler/*.py |
参数校验、编码转换、异常映射、结果缓存 |
3. 安装与构建
3.1 从 PyPI 安装
pip install pycdecompile
包里的 Python 代码是纯 Python,真正干活的是 native/ 编译出来的动态库,所以:
-
平台 wheel(如
pycdecompile-0.1.2.2-py3-none-win_amd64.whl)里已经带了 对应平台的动态库,装完即可用; -
**源码包(sdist)**里带的是内核 C++ 源码,装完还需要编译一次:
pip download --no-binary :all: pycdecompile # 或直接 git clone python build_native.py # 需要 g++ / clang++ / cl.exe
动态库是延迟加载的:即使还没编译好,import pycdecompile、pycdecompile --help
都能正常工作,只有真正调用反编译时才报错,并给出「先跑 python build_native.py」的提示。
3.2 从源码构建
内核源码随仓库分发在 native/,不需要单独下载 pycdc。
需要任意 C++ 编译器即可(本项目在 MinGW-w64 g++ 14.2 上验证通过),CMake 是可选的。
构建是增量的:只重编源文件/头文件有变化的那些目标文件。
脚本为什么叫
build_native.py而不是build.py? 因为build.py会遮蔽 PyPA 的build包(仓库根目录在sys.path最前面), 导致在这个目录里执行标准的python -m build时,实际跑到我们的脚本上, 报出unrecognized arguments这种莫名其妙的错。请不要改回build.py。
python build_native.py # 自动探测编译器,产物 build/pycdecompile.dll
python build_native.py --jobs 8 # 并行编译(默认 = CPU 核心数)
python build_native.py --debug # -O0 -g
python build_native.py --cxx clang++ # 指定编译器
python build_native.py --clean
pycdecompiler 会依次在以下位置查找动态库:
- 环境变量
PYCDC_LIBRARY_PATH(也可以是库文件所在目录) pycdecompiler/_lib/pycdecompiler/<项目根>/build/- 当前工作目录的
build/
装了 CMake 的话(产物同样落在 <项目根>/build/):
cmake -S native -B build/cmake
cmake --build build/cmake --config Release
3.3 发布到 PyPI
python build_native.py # 先确保 build/ 里有本平台的动态库
# 标准做法(推荐,等价于 PEP 517 的构建流程)
pip install build
python -m build # 产物在 dist/:sdist + 本平台 wheel
# 老写法也能用(会有一行 setuptools 弃用提示,但不影响结果)
python setup.py sdist bdist_wheel
python -m twine check dist/* # 检查元数据与 README 渲染
python -m twine upload dist/* # 需要 PyPI API Token
几点约定:
- wheel 会按本机平台打标签(Windows 上是
win_amd64)。带原生库的包绝不能打成py3-none-any,否则 Linux/macOS 的 pip 也会装这个 Windows 包。想给其它平台发包, 就在对应平台上各构建一次(推荐用 CI)。 - Linux 上
setup.py bdist_wheel得到的是linux_x86_64,PyPI 不收这个标签, 需要用auditwheel(manylinux)或auditwheel repair处理后再上传; 实在不方便就只发 sdist。 - 没有编译器时
build_py只告警不中断,但这种 wheel 是空壳,别传上去; 加PYCDC_SKIP_NATIVE_BUILD=1可以显式跳过原生构建。 - sdist 里包含完整的
native/内核源码——这既是构建需要,也是 GPLv3 的要求。
3.4 GitHub Actions
仓库里带了两个工作流(.github/workflows/):
| 工作流 | 触发 | 做什么 |
|---|---|---|
ci.yml |
push 到 main、PR、手动 | 在 ubuntu / windows / macos × Python 3.11 / 3.13 上编译内核,跑单元测试、冒烟测试与上游语料回归 |
release.yml |
打 v* 标签、手动 |
构建四份产物(sdist、Windows wheel、macOS wheel、manylinux wheel),标签触发时发布到 PyPI |
几个约定:
- Windows 用 runner 自带的 MSVC(
ilammy/msvc-dev-cmd),build_native.py会自动识别cl.exe; Linux / macOS 用系统自带的 g++ / clang++。 - manylinux wheel 在
quay.io/pypa/manylinux_2_28_x86_64容器里编译, 用auditwheel repair打上manylinux_2_28标签——PyPI 只接受这个系列的 Linux 标签。 为此build_native.py在 Linux 上只静态链 libstdc++/libgcc(不能用-static), 产出的.so只依赖 libc。 - 上游语料回归只在 Linux + Python 3.11 上设门槛(
--min-pass 143,即当前基线): 这个比对依赖宿主 Python 的tokenize,3.12+ 的 f-string 分词行为变了(PEP 701), 在别的版本上只记录不判定。 - 发布到 PyPI 二选一:在 PyPI 上配置 Trusted Publisher(推荐,不需要任何密钥),
或者在仓库 Secrets 里加
PYPI_API_TOKEN并取消release.yml里password那行的注释。 - 日常发版流程:改
pycdecompiler/_version.py→ 提交 → 打标签(git tag v0.1.2.2 && git push --tags)。publish任务会先校验标签与包版本一致,不一致直接失败,避免把错版本推上 PyPI。
4. Python 对外接口
4.1 反编译
| 函数 | 说明 |
|---|---|
decompile_file(path, *, include_header=True) |
反编译磁盘上的 .pyc |
decompile(data, *, include_header=True, display_name=None) |
反编译内存中的 pyc 字节 |
decompile_code_object(code, *, include_header=True) |
反编译运行中的 types.CodeType |
decompile_marshalled(data, major, minor, ...) |
反编译纯 marshal 的 code object |
decompile_to_file(source, output, ...) |
反编译并写入文件 |
4.2 反汇编(等价 pycdas)
disassemble_file(path, *, verbose=False, show_caches=False)、
disassemble(data, ...)、disassemble_code_object(code, ...)
4.3 版本信息
| 函数 | 说明 |
|---|---|
get_pyc_version(source) |
探测 pyc 版本,返回 PycVersion(3, 13) |
get_magic_version(magic) |
magic number → 版本 |
read_pyc_magic(source) |
读取前 4 字节 magic |
supported_versions() |
内核支持的版本列表 |
is_supported_version(major, minor) |
版本是否受支持 |
backend_version() |
内核版本字符串 |
last_report() |
最近一次反编译产生的诊断信息 |
4.4 异常
try:
src = decompile_file("broken.pyc")
except pc.PycDecompileError as exc:
print(exc.status_name) # PYCDC_ERR_BAD_MAGIC / PYCDC_ERR_MARSHAL / ...
print(exc) # [PYCDC_ERR_BAD_MAGIC] 反编译 xxx 失败: ...
状态码见 pycdecompile_api.h 的 PycdcStatus。
5. 命令行
装好包之后可以用 pycdecompile 命令,也可以始终用 python -m pycdecompiler:
pycdecompile demo.pyc # 打印到 stdout
pycdecompile demo.pyc -o demo.py # 写入文件
pycdecompile -d demo.pyc # 反汇编(等价 pycdas)
pycdecompile --version-info demo.pyc # 只打印 pyc 版本
pycdecompile --list-versions # 列出内核支持的所有版本
pycdecompile --version # 打印本包版本
退出码:0 成功;1 反编译/写入失败;2 参数或文件问题;
3 找不到原生核心(尚未 python build_native.py)。
6. 测试
python tools/official_test.py # 用本机 Python 编译 input/*.py 后比对
python tools/official_test.py --compiled # 跑仓库自带 pyc(覆盖 3.1/3.12 等)
python tools/official_test.py --compiled --min-pass 143 # 当成门槛:低于 143 即失败
python tools/corpus_test.py # AST 级别比对
python tools/needed_opcodes.py # 逐文件列出缺失的 opcode
python tools/diff_one.py f-string.py # 单文件差异
python tools/show_syntax.py argparse.py # 看标准库反编译结果哪里语法不合法
python tools/stress_stdlib.py # 标准库压力测试(崩溃/语法分类统计)
python -m pytest tests_pycdecompiler.py # 单元测试(需要 pytest)
python run_tests.py # 零依赖的单元测试运行器
python smoke_test.py # 冒烟:能不能加载库 + 反编译几个样例
python -m twine check dist/* # 打包前后的元数据自检
标准库压力测试
每一轮修改后建议跑一次 python tools/stress_stdlib.py:它把 32 个标准库模块
用本机 Python 编译后逐个反编译(每个都在独立子进程里,任何一个把进程搞崩都
会被记成 CRASH 而不是拖垮整轮测试),并按结果分类:
| 分类 | 含义 |
|---|---|
ok |
反编译成功且内核无诊断 |
warn |
成功但有诊断(Unsupported opcode、块栈未清空等) |
syntax |
反编译出结果,但 ast.parse 失败(语法不合法) |
error |
抛了 PycDecompileError |
CRASH |
子进程被信号/异常终止 |
当前实测结果
tools/official_test.py --compiled(仓库自带 191 个 pyc,与上游 tests/tokenized
期望输出逐 token 比对,即上游官方测试口径):
| 版本 | 一致率 |
|---|---|
| Python 3.1 | 4/4 (100%) |
| Python 3.11 | 8/8 (100%) |
| Python 3.12 | 4/6 (67%) |
| Python 3.13 | 本机自编译语料 17/54 (31.5%) |
| 1.0 ~ 3.13 全量 | 143/191 (75%) |
tools/stress_multi.py --python <解释器> 用真实解释器编译标准库再反编译,
覆盖 3.13 / 3.14 / 3.15(本机实测版本:3.13.15 / 3.14.3 / 3.15.0rc1):
| 目标版本 | error / CRASH | syntax | warn | ok |
|---|---|---|---|---|
| 3.13 | 0 | 24 | 3 | 1 |
| 3.14 | 0 | 24 | 3 | 0 |
| 3.15 | 0 | 24 | 3 | 0 |
三个版本都能完整解析
.pyc(magic / marshal / opcode)并产出源码,没有崩溃或异常; 语法/语义层面的差异见「已知限制」。
标准库压力测试(tools/stress_stdlib.py,32 个模块,本机 Python 3.11):
| 阶段 | error(崩溃/异常) | syntax | warn | ok |
|---|---|---|---|---|
| 修复前 | 17 | 12 | 2 | 1 |
| 修复后 | 0 | 27 | 3 | 2 |
崩溃类(17 个模块直接失败)已经清零:现在 32 个模块都能跑完并产出源码, 其中 5 个与原始源码完全一致、27 个仍有语法/语义差异(见「已知限制」)。
3.13 一致率偏低的原因见下方「已知限制」——绝大多数用例能产出语法合法的 Python 代码,但与原始源码存在语义差异(上游 fork 对 3.11+ 的若干结构本身就不完整)。
7. 本项目对内核做的改动
7.1 新增
| 文件 | 内容 |
|---|---|
pycdecompile_api.h/.cpp |
C ABI:pycdc_decompile_file/_data/_marshalled_*、pycdc_disassemble_*、pycdc_probe_*、pycdc_magic_to_version、pycdc_last_error/report、pycdc_free |
pycdecompile_log.h/.cpp |
统一的诊断出口:既写 stderr(命令行行为不变),又留一份给 pycdc_last_report(),供其它语言宿主读取 |
7.2 修改
| 文件 | 改动 |
|---|---|
data.cpp |
PycFile/PycBuffer 的 std::exit(1) 改为抛 std::runtime_error——作为库被嵌入时不能再杀宿主进程 |
pyc_module.h/.cpp |
新增 loadFromData / loadFromMarshalledData(内存加载)、magicToVersion、maxSupportedMinor;isSupportedVersion 放行 3.13 |
pyc_module.cpp / pycdas.cpp / pycdc.cpp |
加载失败改为异常路径,命令行工具已加 try/catch |
ASTNode.h |
ASTFunction::setDefArgs/setKwDefArgs、ASTFormattedValue::setFormatSpec |
ASTree.cpp |
见下 |
7.3 ASTree.cpp 补全的 Python 3.12 / 3.13 指令
函数构造
MAKE_FUNCTION(3.13 无参形式)SET_FUNCTION_ATTRIBUTE(默认值 / 关键字默认值,展开 tuple、dict 后复用原有打印逻辑)
f-string
CONVERT_VALUE、FORMAT_SIMPLE、FORMAT_WITH_SPEC
调用约定
- 3.13 的栈布局由
[NULL, callable, args]变为[callable, self, args], 相应修正CALL、CALL_FUNCTION_EX、LOAD_ATTR、LOAD_BUILD_CLASS的识别 - 新增
CALL_KW(含类定义class A(B, metaclass=M))、CALL_FUNCTION_EX、DICT_MERGE、DICT_UPDATE LOAD_SUPER_ATTR(super().m())、CALL_INTRINSIC_1/2
条件与循环
POP_JUMP_IF_NONE/POP_JUMP_IF_NOT_NONE(if x is None:)TO_BOOL、RETURN_CONST(含if分支收尾)、RETURN_GENERATOR
闭包与帧
MAKE_CELL、COPY_FREE_VARS、LOAD_FAST_CHECK、LOAD_FAST_AND_CLEAR、DELETE_DEREF、LOAD_FROM_DICT_OR_DEREF/_GLOBALS、EXIT_INIT_CHECK
微融合指令
STORE_FAST_LOAD_FAST、STORE_FAST_STORE_FAST
推导式与容器
MAP_ADD、SET_ADD
await / async
SEND、END_SEND、CLEANUP_THROW、END_ASYNC_FOR、BEFORE_ASYNC_WITH; 用inAwait标记区分 await 协议内部的YIELD_VALUE与真正的yieldYIELD_VALUE_A、GET_AWAITABLE_A(3.12+ 起这两条指令带 oparg)
其它
GET_LEN、LOAD_ASSERTION_ERROR、ENTER_EXECUTOR- 各种
INSTRUMENTED_*变体 decompyle()结尾的隐式return清理改为循环剥离(3.12 起可能出现连续两条)- 内核里 31 处
fprintf(stderr, ...)改走pycdc_report()
7.4 本次针对 Python 3.11+ 的修复
稳定性(进程不再被杀)
| 问题 | 原因 | 修复 |
|---|---|---|
反编译标准库时 PycBuffer::getByte(): Unexpected end of stream |
RETURN_VALUE 分支无条件多读一条指令「跳过分支多余的指令」,最后一条指令恰好是 RETURN_VALUE 时就跨过字节码末尾 |
读之前判断 source.atEof()(RETURN_CONST 早已有此守卫,RETURN_VALUE 漏了) |
| 进程被 abort / 访问越界杀死(17/32 个标准库模块) | std::stack::pop()/top() 作用在空栈上是 UB:stack_hist.pop()、blocks.top() 在块栈/历史栈提前耗尽时踩空 |
新增空栈安全的 stackhist_t / BlockStack(FastStack.h),空栈时 pop 退化为空操作、top 返回占位块;JUMP_BACKWARD 的块栈循环加空栈守卫 |
| 坏掉的异常表让整次反编译失败 | exceptionTableEntries() 直接 getByte() 越界抛异常 |
改成有界解析(_try_parse_varint),截断的表只告警不抛异常 |
| 宿主进程被内核带走(GUI 窗口成孤儿) | 调用方在自己的进程里直接调 ctypes 内核 | 内核侧加了 VEH + longjmp 护栏:访问越界转成带 opcode 定位的 PycDecompileError;调用方仍应把不可信 pyc 放在子进程里跑(见 tools/stress_stdlib.py 的隔离写法) |
功能补全
| 指令 / 结构 | 说明 |
|---|---|
UNPACK_EX |
星号解包 a, *b, c = seq;新增 ASTStarred 节点,按 oparg 的 before/after 定位星号目标 |
POP_JUMP_FORWARD/BACKWARD_IF_NONE、_IF_NOT_NONE、POP_JUMP_BACKWARD_IF_FALSE/TRUE |
3.11 的条件跳转;is None 的条件按「不跳转即进入分支体」取反 |
3.8+ 的 while 循环重建 |
3.8 起没有 SETUP_LOOP,改为识别回边:if cond: body; if cond: goto body → while cond: body |
DICT_UPDATE |
{**a, **b}:字典条目允许「无键」表示 **mapping |
LIST_EXTEND / SET_UPDATE / LIST_TO_TUPLE |
(*a, *b) / [*a, *b] / {*a, *b} 的星号显示;修掉原来遇到非常量就丢弃左值导致的栈失衡 |
| 3.11+ try/except(零开销异常表) | 用「处理器的代码形态」区分 except / finally(PUSH_EXC_INFO 后有没有 CHECK_EXC_MATCH),支持裸 except:、except X as e:(含隐式 del e 清理的省略)、循环内的 try;没有配对处理器的 try 退化为普通语句块,不再产出悬空 try: |
PRINT_EXPR / ASYNC_GEN_WRAP |
源码层面不可见,按无操作处理 |
MAKE_FUNCTION 的位置默认值 |
3.6 起默认值在栈上是「一个 tuple 常量」(def f(a, b=1) → LOAD_CONST (1,)),原先直接当成单个默认值,输出成 b = (1,);现在按 3.6+ 且 oparg==1 展开成逐个默认值 |
另外去掉了 RETURN_VALUE 里那条「多读一条指令」的逻辑——它会吞掉紧跟其后的
elif 条件(LOAD_FAST),是 if/elif 链产出 None is not None 之类错乱的根因。
7.5 Python 3.13 / 3.14 / 3.15 支持
| 版本 | magic | 支持程度 | 说明 |
|---|---|---|---|
| 3.13 | 0x0A0D0DF3 |
完整(沿用既有实现 + 本轮补的 RETURN_VALUE/块栈/异常表修复) |
opcode 映射由 3.13.15 解释器导出,138 条 |
| 3.14 | 0x0A0D0E2B |
简要支持 | 新增 LOAD_FAST_BORROW、LOAD_SMALL_INT、LOAD_COMMON_CONSTANT、POP_ITER、NOT_TAKEN、BUILD_TEMPLATE/BUILD_INTERPOLATION(t-string)、LOAD_SPECIAL(with 语句)、无参 CALL_FUNCTION_EX;marshal 新增的 slice 类型(0x3A) 也已解析 |
| 3.15 | 0x0A0D0E52 |
简要支持 | 在 3.14 基础上:GET_ITER 带参、TRACE_RECORD、LOAD_COMMON_CONSTANT 表扩充、IMPORT_NAME 的 namei = oparg >> 2、类体新增的 __classdict__/__classdictcell__ 内部赋值在输出时剔除 |
opcode 映射表由脚本从真实解释器导出,避免手抄出错:
python tools/gen_opcode_map.py /path/to/python3.14 --write # 生成 bytes/python_3_14.cpp
python tools/gen_opcode_map.py /path/to/python3.15 # 只检查缺口
脚本会解析 bytecode_ops.inl 里已有的枚举符号(NAME = 无参、NAME_A = 带参),
把新版本的 opcode 名字映射过去,并列出「未声明」「arg 属性冲突」两类需要人工处理的项。
新版本若给某条指令加了参数(例如 3.15 的 GET_ITER),需要先在
bytecode_ops.inl 里补一个 OPCODE_A(GET_ITER)(无参的 OPCODE(GET_ITER) 保留给旧版本)。
7.6 语义重建(操作数栈失同步的修复)
「能跑完」和「跑对」之间差的是操作数栈语义。本轮针对 3.12+ 的
SWAP/COPY/STORE_FAST_STORE_FAST 与推导式做了重建:
| 问题 | 现象 | 修复 |
|---|---|---|
SWAP 被当成 3.11 的多赋值 |
self.attr += v、x[i] *= v 整条语句消失;a == b == c 变成 == a, b or a, b == c 这种非法代码 |
3.12 起 SWAP 只是纯栈重排(多赋值改用 STORE_FAST_STORE_FAST/LOAD_FAST_LOAD_FAST),只有 3.11 及更早才走多赋值启发式 |
STORE_FAST_STORE_FAST 拆成两条 store |
a, b = b, a 输出成 b = a; a = b(语义错误,交换失效) |
合并成一次元组赋值 (a, b) = (b, a);CPython 语义是 TOS → 高半字节、TOS1 → 低半字节,源码顺序的第一个目标在低半字节 |
| 推导式 / 生成器表达式的 code object 被当成 lambda | sum(v*2 for v in items) → sum((lambda .0: for v in .0: v*2.0)()) |
识别 <listcomp>/<setcomp>/<dictcomp>/<genexpr>:打印时不加 (lambda ...) 包装、剥掉结尾的 return、CALL 到推导式函数对象时直接折叠成推导式节点,新增 COMP_GENEXPR 输出 (x for x in y) |
链式比较被判成 or |
if x == y == z: → if x == y or y == z:(语义错误) |
新增判据:两次条件跳转极性一致 = and、相反 = or。链式比较的两次 POP_JUMP_IF_FALSE 目标并不相同(一次落在清理分支上),只比较位置会误判 |
| 形参打印顺序违反源码语法 | def f(*args, x=1) → def f(*, x=1, *args)(同一个 * 出现两次,ast.parse 直接报错);getLocal 还会越界 |
新增 print_function_params():localsplus 顺序是 [位置, kwonly, *args, **kwargs],源码顺序是 [位置, args/裸, kwonly, **kwargs],按下标显式取值输出(具名函数与 lambda 共用) |
| 关键字默认值丢失 | def f(a, *, b=3) → def f(a, *, b) |
MAKE_FUNCTION 的 oparg 是位掩码:0x01 位置默认值(tuple 常量)、0x02 关键字默认值(3.11+ 是 BUILD_CONST_KEY_MAP)、0x03 两者都有。这三种组合都展开,其余(注解/闭包)仍走旧的按数值弹栈逻辑 |
对应的回归语料放在 tools/corpus/semantics_313.py(增广赋值、并行赋值、
链式比较、各类推导式、变参签名、嵌套解包、with、try/except/else/finally),
tools/show_syntax_pyc.py <解释器> <模块> 可以逐点查看失败位置。
效果(标准库压力测试,语法合法 + 有诊断的模块数):
| 版本 | 修复前 syntax / warn / ok | 修复后 syntax / warn / ok |
|---|---|---|
| 3.11 | 27 / 2 / 1 | 27 / 2 / 1(未受影响,SWAP 分支保持不变) |
| 3.13 | 22 / 5 / 1 | 19 / 5 / 4 |
| 3.14 | 23 / 4 / 0 | 20 / 7 / 0 |
| 3.15 | 24 / 3 / 0 | 20 / 7 / 0 |
7.7 源码内置化 + Bug 修复
内核源码并入 native/,全仓库不再有任何地方引用 pycdc-master/;
顺手修掉了这一路上看到的问题:
构建与工具链
| 问题 | 修复 |
|---|---|
build_native.py 的 --jobs 参数收下了却从没用过(永远是单进程一把梭) |
真正并行编译,默认取 CPU 核心数 |
| 每次构建都重编全部 45 个源文件 | 增量构建:按目标文件/头文件时间戳判断,只编过期的 |
tools/show_syntax_pyc.py 里写死了另一台机器另一个项目的绝对路径(D:\modules\exe-reverse-gui\pycdecompile),换机即废 |
改为按 __file__ 推导项目根,并补上标准的 main() 结构 |
tools/dump_dis.py 导入了用不到的 Callable |
删掉 |
C 内核 / C ABI 导出层
| 问题 | 修复 |
|---|---|
非 Windows 平台根本编不过:g_fault_ip / HMODULE / GetModuleHandleExA 只在 #ifdef _WIN32 里定义,却在 opcodeCrashMessage() 里被无条件使用 |
补上同样的条件编译,Linux/macOS 现在能构建出 .so / .dylib |
Windows 上中文(或任何非 ASCII)路径的 .pyc 打不开:CRT 的 fopen 按 ANSI 代码页解释窄字符路径 |
新增 pycdc_fopen_utf8():Windows 转 UTF-16 走 _wfopen,字节串不是合法 UTF-8 时退回 fopen;PycFile、pycdc_probe_file、pycdc_set_trace 全部改走它 |
runGuarded() 在 setjmp/longjmp 之间构造 std::string 返回值:被 longjmp 跨过的析构是 UB |
ostringstream 移到护栏之外,护栏内只写不构造 |
PycFile::atEof() 在文件没打开时对空 FILE* 调 fgetc/ungetc |
空流直接返回「已到末尾」 |
PycBuffer::atEof() 用 == 判断,位置一旦越界就永远返回「未到末尾」,后续 getByte() 越界读 |
改成 >= |
formatted_printv() 在 vsnprintf 失败提前返回时漏了 va_end(va_copy 出来的那份) |
提前返回前补 va_end |
#include "data.h" 重复包含 |
删掉 |
Python 层
| 问题 | 修复 |
|---|---|
_native._load_library() 把 os.add_dll_directory() 的句柄存在局部列表里,函数一返回句柄就被回收,DLL 搜索目录随之失效 |
句柄改为模块级长期持有 |
_decode_result() 在 native 返回 NULL 指针却报 status=OK 时,会拼出「失败(状态码 0)」这种自相矛盾的异常 |
归成 PYCDC_ERR_INTERNAL |
supported_versions() / is_supported_version() 绕过 _CALL_LOCK 直接摸 _native.lib——内核用全局缓冲区保存最近错误,多线程下会读到别人的错误信息 |
新增加锁的 _native.call_supported_range(),core 只调它 |
_is_pathlike() 把 bytes 也算路径(只是当时没人调用它),一旦用上就会把 pyc 字节流当文件名 |
只认 str / os.PathLike,并让 _load_source() 真正用它 |
disassemble() 声明收 pyc 数据,传路径会 TypeError(与 decompile() 行为不一致) |
两者都接受,展示名也跟着走 |
decompile_code_object() / disassemble_code_object() 把 major 写死成 3,只挡了 ≥3.16 |
统一交给 is_supported_version() 判断 |
__init__.py 的 __all__ 漏了 last_report,from pycdecompiler import * 拿不到 |
补进 __all__ |
CLI 把结果写到控制台时,源码里有控制台代码页装不下的字符(Windows GBK 控制台遇到日文/emoji)会直接 UnicodeEncodeError 崩掉 |
启动时把 stdout/stderr 调成 errors="replace";写输出文件失败也给出干净的错误而不是裸 OSError |
example.py:if isinstance(code, type(compile("", "", "exec"))): pass 是段死代码;章节标题写死「Python 3.13」(实际用的是当前解释器);f"{len(...) and ''}3. {ver}" 拼出「3. 3.1」这样的标题 |
逐条改掉 |
复现非 ASCII 路径那条(修复前必然失败):
import py_compile, tempfile
from pathlib import Path
import pycdecompiler as pc
tmp = Path(tempfile.mkdtemp())
src = tmp / "测试模块.py"
src.write_text("a = 1\n", encoding="utf-8")
pyc = tmp / "测试模块.pyc"
py_compile.compile(str(src), cfile=str(pyc), doraise=True)
print(pc.decompile_file(pyc)) # 修复前:PYCDC_ERR_DECOMPILE / Error opening file
8. 已知限制
明确尚未支持,遇到时会报 Unsupported opcode: XXX 并输出
# WARNING: Decompyle incomplete(而不是静默产出错误代码):
- 增广赋值
x.attr += y/x[i] += y(3.13 经COPY/SWAP/STORE_ATTR重构)目前会丢失赋值语句,只剩后续读取 match语句相关:MATCH_MAPPING/MATCH_SEQUENCE/MATCH_KEYS/MATCH_CLASSCHECK_EG_MATCH、PREP_RERAISE_STAR—— 异常组(except*)
仍然能跑但结果不完美的部分(不会崩、不会抛异常,但语法/语义与源码有差异):
- 3.11 及更早的
SWAP多赋值启发式仍是猜测式的:a, b = b, a这类写法 在这些版本上还会退化成顺序赋值(3.12+ 已修正) with语句(BEFORE_WITH/ 3.14 的LOAD_SPECIAL协议)尚未重建, 会输出残缺的with None:- 嵌套 try 的块层次偶尔会把外层
except挂到内层处理器里;try/finally(3.11 的 finally 处理器)尚未重建,finally 体可能重复输出 - 生成器表达式在部分版本上仍会把隐式参数
.0留在输出里(迭代对象无法还原), 这类模块会因非法标识符而在ast.parse阶段失败 - 多条件
if(if a and b and c:)的分支体偶尔被挂到 if 之外 - 带关键字默认值(
def f(a, *, b=1))、注解或闭包的函数定义只保留形参名, 默认值会被丢掉(沿用旧行为;展开这些组合会让 zipfile 一类的模块在内核里崩溃, 因此先保持现状) - 内联推导式(
LOAD_FAST_AND_CLEAR+SWAP)在部分写法下仍会产出近似代码
上游 fork 本身对 3.12/3.13 支持不完整是主要原因;本项目的定位是把 调用链路、构建、接口、测试 全部打通,并尽可能补全最高频的指令。
9. 继续扩展的方法
# 1. 找出缺口:某条指令没被处理时会从这里列出来
python tools/needed_opcodes.py
# 2. 看这条指令在真实字节码里的形态
python tools/dump_dis.py # 或 python -c "import dis; dis.dis(compile(...))"
# 3. 在 ASTree.cpp 的 switch 里补 case(新指令都集中在文件末尾的
# “Python 3.12 / 3.13 新增(或语义有变)的指令” 段落)
# 4. 重建 + 回归
python build_native.py && python tools/official_test.py --compiled
10. 许可与致谢
本项目整体以 GNU General Public License v3(GPL-3.0-only)发布,
许可证全文见根目录的 LICENSE(与 native/LICENSE 内容一致)。
10.1 上游与致谢
反编译内核来自 Decompyle++(pycdc):
Decompyle++ is the work of Michael Hansen and Darryl Pogue. It is released under the terms of the GNU General Public License, version 3.
本项目在 native/ 中分发的是该内核源码(含本项目对 3.11 ~ 3.15 指令的补全与
稳定性修复,逐条列在 native/README.md 与本文第 7 节)。上游的著作权归
Michael Hansen、Darryl Pogue 等原作者所有,本仓库不对其主张任何权利。
tests_data/ 是上游仓库自带的回归语料,同样遵循 GPLv3。
10.2 为什么这样分发不构成侵权
- 许可兼容:内核是 GPLv3,本项目也整体以 GPLv3 发布,没有混入任何
GPL 不兼容的代码;
pycdecompile的全部依赖都是标准库。 - 保留许可证与声明:根目录
LICENSE、native/LICENSE都是 GPLv3 全文, 并且两处都进了 sdist 和 wheel(*.dist-info/licenses/)。 - 提供完整对应源码:sdist 里带完整的
native/C++ 源码与构建脚本 (MANIFEST.in明确包含),符合 GPLv3 第 6 条对源码分发的要求; 构建出来的动态库也随 wheel 一并分发。 - 署名:README、
native/README.md、setup.py的project_urls都指明了上游项目与作者。
10.3 使用者的义务
GPLv3 是传染性许可:如果你基于本项目(或其中的内核)发布衍生作品,
需要同样以 GPLv3 发布,并提供完整的对应源码。把本包用在内部工具里、
或者只是拿它反编译自己的 .pyc,都不受额外限制。
10.4 免责
本工具仅用于合法的逆向分析、取证、兼容性研究与学习。反编译得到的源码 其著作权仍归原作者所有,请勿用于侵犯他人权益的用途。程序按「原样」提供, 不附带任何担保。
Metadata
Release files for pycdecompile 0.1.2.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pycdecompile-0.1.2.2.tar.gz | 294.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pycdecompile-0.1.2.2-py3-none-win_amd64.whl | Python 3 | none | Windows x86-64 | Details |
Total release size: 868.7 kB
Release files / pycdecompile-0.1.2.2.tar.gz
| Download URL | pycdecompile-0.1.2.2.tar.gz |
|---|---|
| Size | 294.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c0ceacddadc37c22eff8316bcb66c94858e53e241f4f6f350ce679dc39fee0bf
|
|
BLAKE2b-256 checksum How to use checksums |
c5c8fc710f2253e3c05c6ef4984cfc2499908478770a8403ea97f5a4c06ddc61
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.1.0 CPython/3.11.9
|
Release files / pycdecompile-0.1.2.2-py3-none-win_amd64.whl
| Download URL | pycdecompile-0.1.2.2-py3-none-win_amd64.whl |
|---|---|
| Size | 574.7 kB |
| Tags | Python 3 Windows x86-64 |
|
SHA-256 checksum How to use checksums |
c18c20d41c54f4ec76328f9eece9e28764cdc9e66eda0a759cdc174f3a6896c1
|
|
BLAKE2b-256 checksum How to use checksums |
902d40088bec49c307e9f83d4a4584b3806813155c58629d06390ac6b3f93532
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.1.0 CPython/3.11.9
|