Skip to main content

pycdecompiler

CI Release PyPI Python License: GPLv3

把 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 会依次在以下位置查找动态库:

  1. 环境变量 PYCDC_LIBRARY_PATH(也可以是库文件所在目录)
  2. pycdecompiler/_lib/
  3. pycdecompiler/
  4. <项目根>/build/
  5. 当前工作目录的 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 与真正的 yield
  • YIELD_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_CLASS
  • CHECK_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)

Source distribution for pycdecompile 0.1.2.2
File Size Uploaded
pycdecompile-0.1.2.2.tar.gz 294.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pycdecompile 0.1.2.2
File Interpreter ABI Platform
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

Release history Release notifications | RSS feed

This release

0.1.2.2 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page