Metadata-Version: 2.4
Name: pycdecompile
Version: 0.1.2.1
Summary: 把 Python 1.0 ~ 3.15 的 .pyc 反编译回源码（Decompyle++ 内核 + C ABI + ctypes）
Home-page: https://github.com/hyy-aaa/pycdecompile
Author: hyy
Author-email: hyy@hyysn.cn
License: GPL-3.0-only
Project-URL: Source, https://github.com/hyy-aaa/pycdecompile
Project-URL: Bug Tracker, https://github.com/hyy-aaa/pycdecompile/issues
Project-URL: Upstream (Decompyle++), https://github.com/zrax/pycdc
Keywords: pyc decompiler decompile bytecode pycdc decompyle reverse-engineering
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
Classifier: Operating System :: POSIX :: Linux
Classifier: Operating System :: MacOS
Classifier: Operating System :: Microsoft :: Windows
Classifier: Programming Language :: C++
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Disassemblers
Classifier: Topic :: Utilities
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: native/LICENSE
Dynamic: author
Dynamic: author-email
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: home-page
Dynamic: keywords
Dynamic: license
Dynamic: license-file
Dynamic: project-url
Dynamic: requires-python
Dynamic: summary

# pycdecompiler

把 Python 的 `.pyc` 反编译回源码，内核基于 [Decompyle++ / pycdc](https://github.com/zrax/pycdc)，
用 **C++ 实现反编译内核**，通过 **C ABI 动态库** 暴露，再由 **Python（ctypes）封装成可调用的函数**。

> 内核源码已经**内置**在本仓库的 `native/` 目录（以前是外挂的 `pycdc-master/`），
> 克隆下来直接 `python build.py` 就能用，不需要额外下载任何东西。

支持 Python 1.0 ~ 3.15 的字节码；本项目重点补全了 **3.12 / 3.13** 的实现，
并验证了 **3.1 / 3.11** 等版本。

| | |
| --- | --- |
| 版本 | **0.1.2.1** |
| 作者 | hyy <hyy@hyysn.cn> |
| 仓库 | <https://github.com/hyy-aaa/pycdecompile> |
| PyPI | <https://pypi.org/project/pycdecompile/> |
| 许可 | GPL-3.0-only（内核 Decompyle++ 为 GPLv3，详见「许可与致谢」） |

```bash
pip install pycdecompile
```

```python
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")))
```

```bash
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.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.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 安装

```bash
pip install pycdecompile
```

包里的 Python 代码是纯 Python，真正干活的是 `native/` 编译出来的动态库，所以：

* **平台 wheel**（如 `pycdecompile-0.1.2.1-py3-none-win_amd64.whl`）里**已经带了**
  对应平台的动态库，装完即可用；
* **源码包（sdist）**里带的是内核 C++ 源码，装完还需要编译一次：

  ```bash
  pip download --no-binary :all: pycdecompile   # 或直接 git clone
  python build.py                               # 需要 g++ / clang++ / cl.exe
  ```

动态库是延迟加载的：即使还没编译好，`import pycdecompile`、`pycdecompile --help`
都能正常工作，只有真正调用反编译时才报错，并给出「先跑 python build.py」的提示。

### 3.2 从源码构建

内核源码随仓库分发在 `native/`，**不需要**单独下载 pycdc。
需要任意 C++ 编译器即可（本项目在 MinGW-w64 g++ 14.2 上验证通过），CMake 是可选的。
构建是增量的：只重编源文件/头文件有变化的那些目标文件。

```bash
python build.py                # 自动探测编译器，产物 build/pycdecompile.dll
python build.py --jobs 8       # 并行编译（默认 = CPU 核心数）
python build.py --debug        # -O0 -g
python build.py --cxx clang++  # 指定编译器
python build.py --clean
```

`pycdecompiler` 会依次在以下位置查找动态库：

1. 环境变量 `PYCDC_LIBRARY_PATH`（也可以是库文件所在目录）
2. `pycdecompiler/_lib/`
3. `pycdecompiler/`
4. `<项目根>/build/`
5. 当前工作目录的 `build/`

装了 CMake 的话（产物同样落在 `<项目根>/build/`）：

```bash
cmake -S native -B build/cmake
cmake --build build/cmake --config Release
```

### 3.3 发布到 PyPI

```bash
python build.py                      # 先确保 build/ 里有本平台的动态库
python setup.py sdist bdist_wheel    # 产物在 dist/（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 的要求。

## 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 异常

```python
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`：

```bash
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.py`）。

## 6. 测试

```bash
python tools/official_test.py              # 用本机 Python 编译 input/*.py 后比对
python tools/official_test.py --compiled   # 跑仓库自带 pyc（覆盖 3.1/3.12 等）
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 映射表由脚本从**真实解释器**导出，避免手抄出错：

```bash
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.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 路径那条（修复前必然失败）：

```python
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. 继续扩展的方法

```bash
# 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.py && python tools/official_test.py --compiled
```

## 10. 许可与致谢

**本项目整体以 GNU General Public License v3（GPL-3.0-only）发布**，
许可证全文见根目录的 `LICENSE`（与 `native/LICENSE` 内容一致）。

### 10.1 上游与致谢

反编译内核来自 [Decompyle++（pycdc）](https://github.com/zrax/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 免责

本工具仅用于**合法的逆向分析、取证、兼容性研究与学习**。反编译得到的源码
其著作权仍归原作者所有，请勿用于侵犯他人权益的用途。程序按「原样」提供，
不附带任何担保。
