pycdecompile 0.1.2.1__py3-none-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,639 @@
1
+ Metadata-Version: 2.4
2
+ Name: pycdecompile
3
+ Version: 0.1.2.1
4
+ Summary: 把 Python 1.0 ~ 3.15 的 .pyc 反编译回源码(Decompyle++ 内核 + C ABI + ctypes)
5
+ Home-page: https://github.com/hyy-aaa/pycdecompile
6
+ Author: hyy
7
+ Author-email: hyy@hyysn.cn
8
+ License: GPL-3.0-only
9
+ Project-URL: Source, https://github.com/hyy-aaa/pycdecompile
10
+ Project-URL: Bug Tracker, https://github.com/hyy-aaa/pycdecompile/issues
11
+ Project-URL: Upstream (Decompyle++), https://github.com/zrax/pycdc
12
+ Keywords: pyc decompiler decompile bytecode pycdc decompyle reverse-engineering
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
17
+ Classifier: Operating System :: POSIX :: Linux
18
+ Classifier: Operating System :: MacOS
19
+ Classifier: Operating System :: Microsoft :: Windows
20
+ Classifier: Programming Language :: C++
21
+ Classifier: Programming Language :: Python :: 3
22
+ Classifier: Programming Language :: Python :: 3 :: Only
23
+ Classifier: Programming Language :: Python :: 3.8
24
+ Classifier: Programming Language :: Python :: 3.9
25
+ Classifier: Programming Language :: Python :: 3.10
26
+ Classifier: Programming Language :: Python :: 3.11
27
+ Classifier: Programming Language :: Python :: 3.12
28
+ Classifier: Programming Language :: Python :: 3.13
29
+ Classifier: Programming Language :: Python :: Implementation :: CPython
30
+ Classifier: Topic :: Security
31
+ Classifier: Topic :: Software Development :: Disassemblers
32
+ Classifier: Topic :: Utilities
33
+ Requires-Python: >=3.8
34
+ Description-Content-Type: text/markdown
35
+ License-File: LICENSE
36
+ License-File: native/LICENSE
37
+ Dynamic: author
38
+ Dynamic: author-email
39
+ Dynamic: classifier
40
+ Dynamic: description
41
+ Dynamic: description-content-type
42
+ Dynamic: home-page
43
+ Dynamic: keywords
44
+ Dynamic: license
45
+ Dynamic: license-file
46
+ Dynamic: project-url
47
+ Dynamic: requires-python
48
+ Dynamic: summary
49
+
50
+ # pycdecompiler
51
+
52
+ 把 Python 的 `.pyc` 反编译回源码,内核基于 [Decompyle++ / pycdc](https://github.com/zrax/pycdc),
53
+ 用 **C++ 实现反编译内核**,通过 **C ABI 动态库** 暴露,再由 **Python(ctypes)封装成可调用的函数**。
54
+
55
+ > 内核源码已经**内置**在本仓库的 `native/` 目录(以前是外挂的 `pycdc-master/`),
56
+ > 克隆下来直接 `python build.py` 就能用,不需要额外下载任何东西。
57
+
58
+ 支持 Python 1.0 ~ 3.15 的字节码;本项目重点补全了 **3.12 / 3.13** 的实现,
59
+ 并验证了 **3.1 / 3.11** 等版本。
60
+
61
+ | | |
62
+ | --- | --- |
63
+ | 版本 | **0.1.2.1** |
64
+ | 作者 | hyy <hyy@hyysn.cn> |
65
+ | 仓库 | <https://github.com/hyy-aaa/pycdecompile> |
66
+ | PyPI | <https://pypi.org/project/pycdecompile/> |
67
+ | 许可 | GPL-3.0-only(内核 Decompyle++ 为 GPLv3,详见「许可与致谢」) |
68
+
69
+ ```bash
70
+ pip install pycdecompile
71
+ ```
72
+
73
+ ```python
74
+ import pycdecompiler as pc
75
+
76
+ print(pc.decompile_file("app.pyc")) # 磁盘 pyc -> 源码
77
+ print(pc.decompile(open("app.pyc","rb").read()))
78
+ print(pc.decompile_code_object(compile("a = 1 + 2", "<s>", "exec")))
79
+ ```
80
+
81
+ ```bash
82
+ pycdecompile app.pyc -o app.py # 命令行(装了包之后可用)
83
+ python -m pycdecompiler app.pyc # 等价写法
84
+ ```
85
+
86
+ ---
87
+
88
+ ## 1. 目录结构
89
+
90
+ ```
91
+ pycdecompile/
92
+ ├── native/ Decompyle++ 内核源码(内置,含本项目新增/修改)
93
+ │ ├── pycdecompile_api.h/.cpp ★ 新增:C ABI 导出层
94
+ │ ├── pycdecompile_log.h/.cpp ★ 新增:内核诊断信息出口
95
+ │ ├── ASTree.cpp ★ 修改:补全 3.12 ~ 3.15 指令
96
+ │ ├── ASTNode.h ★ 修改:新增 setter
97
+ │ ├── data.cpp ★ 修改:std::exit → 抛异常、UTF-8 路径
98
+ │ ├── pyc_module.h/.cpp ★ 修改:支持内存加载 / 放行 3.13 ~ 3.15
99
+ │ ├── bytes/python_*.cpp 各版本 opcode 映射表(1.0 ~ 3.15)
100
+ │ ├── CMakeLists.txt 可选的 CMake 构建(等价于 build.py)
101
+ │ └── README.md 内核目录说明 + 相对上游的改动清单
102
+ ├── tests_data/ 上游回归语料(input/compiled/tokenized/xfail)
103
+ ├── pycdecompiler/ ★ Python 包(对外 API)
104
+ │ ├── __init__.py 导出全部公开函数
105
+ │ ├── core.py 对外 Python 函数实现
106
+ │ ├── _native.py ctypes 绑定
107
+ │ └── cli.py / __main__.py 命令行入口
108
+ ├── build.py ★ 一键构建动态库(增量、可并行)
109
+ ├── setup.py / pyproject.toml ★ 打包(PyPI: sdist / wheel)
110
+ ├── MANIFEST.in sdist 要带上的文件(含内核源码)
111
+ ├── LICENSE GPLv3 全文(与 native/LICENSE 一致)
112
+ ├── example.py ★ 使用示例(当前解释器 / 3.1 / 3.12)
113
+ ├── tests_pycdecompiler.py ★ pytest 单元测试
114
+ └── tools/ ★ 分析与回归工具
115
+ ├── official_test.py 按上游 tokenized 口径回归
116
+ ├── corpus_test.py AST 级别比对
117
+ ├── gap_analysis.py 查 ASTree 缺哪些 opcode
118
+ ├── needed_opcodes.py 逐文件定位缺失 opcode
119
+ ├── diff_one.py 单个文件差异对比
120
+ ├── dump_dis.py 导出各语法结构的反汇编参考
121
+ ├── show_syntax.py 反编译标准库并展示 ast.parse 失败处上下文
122
+ ├── stress_stdlib.py 标准库压力测试(子进程隔离,统计 ok/warn/syntax/error/CRASH)
123
+ ├── stress_multi.py 指定解释器(3.13/3.14/3.15…)编译标准库后压力测试
124
+ ├── gen_opcode_map.py 从目标解释器导出 bytes/python_X_Y.cpp 并检查缺口
125
+ └── corpus/ 针对 3.11 语法结构的回归语料
126
+ ├── unpack_star_311.py 星号解包 / ** 字典展开 / 星号显示
127
+ ├── try_patterns_311.py try/except 各种写法
128
+ ├── loops_311.py while / break / continue / while-else
129
+ └── none_test_311.py is None 系列条件
130
+ ```
131
+
132
+ ## 2. 架构
133
+
134
+ ```
135
+ ┌──────────────────────────────────────────┐
136
+ │ Python (pycdecompiler 包) │
137
+ │ decompile_file / decompile_code_object │
138
+ └───────────────┬──────────────────────────┘
139
+ │ ctypes
140
+ ┌───────────────▼──────────────────────────┐
141
+ │ pycdecompile.dll (C ABI,extern "C") │
142
+ │ pycdc_decompile_file / _data / ... │
143
+ └───────────────┬──────────────────────────┘
144
+ │ C++
145
+ ┌───────────────▼──────────────────────────┐
146
+ │ pycxx 内核 │
147
+ │ PycModule(pyc 头 + marshal 解析) │
148
+ │ PycCode / PycObject …(对象模型) │
149
+ │ ASTree.cpp(字节码 → AST → 源码) │
150
+ └──────────────────────────────────────────┘
151
+ ```
152
+
153
+ 三层职责分明:
154
+
155
+ | 层 | 文件 | 说明 |
156
+ | --- | --- | --- |
157
+ | 内核 | `native/*.cpp` | 解析 `.pyc`、反汇编、构建 AST、打印源码 |
158
+ | 导出层 | `pycdecompile_api.cpp` | 把内核包成稳定 C 接口,消化所有异常 |
159
+ | 调用层 | `pycdecompiler/*.py` | 参数校验、编码转换、异常映射、结果缓存 |
160
+
161
+ ## 3. 安装与构建
162
+
163
+ ### 3.1 从 PyPI 安装
164
+
165
+ ```bash
166
+ pip install pycdecompile
167
+ ```
168
+
169
+ 包里的 Python 代码是纯 Python,真正干活的是 `native/` 编译出来的动态库,所以:
170
+
171
+ * **平台 wheel**(如 `pycdecompile-0.1.2.1-py3-none-win_amd64.whl`)里**已经带了**
172
+ 对应平台的动态库,装完即可用;
173
+ * **源码包(sdist)**里带的是内核 C++ 源码,装完还需要编译一次:
174
+
175
+ ```bash
176
+ pip download --no-binary :all: pycdecompile # 或直接 git clone
177
+ python build.py # 需要 g++ / clang++ / cl.exe
178
+ ```
179
+
180
+ 动态库是延迟加载的:即使还没编译好,`import pycdecompile`、`pycdecompile --help`
181
+ 都能正常工作,只有真正调用反编译时才报错,并给出「先跑 python build.py」的提示。
182
+
183
+ ### 3.2 从源码构建
184
+
185
+ 内核源码随仓库分发在 `native/`,**不需要**单独下载 pycdc。
186
+ 需要任意 C++ 编译器即可(本项目在 MinGW-w64 g++ 14.2 上验证通过),CMake 是可选的。
187
+ 构建是增量的:只重编源文件/头文件有变化的那些目标文件。
188
+
189
+ ```bash
190
+ python build.py # 自动探测编译器,产物 build/pycdecompile.dll
191
+ python build.py --jobs 8 # 并行编译(默认 = CPU 核心数)
192
+ python build.py --debug # -O0 -g
193
+ python build.py --cxx clang++ # 指定编译器
194
+ python build.py --clean
195
+ ```
196
+
197
+ `pycdecompiler` 会依次在以下位置查找动态库:
198
+
199
+ 1. 环境变量 `PYCDC_LIBRARY_PATH`(也可以是库文件所在目录)
200
+ 2. `pycdecompiler/_lib/`
201
+ 3. `pycdecompiler/`
202
+ 4. `<项目根>/build/`
203
+ 5. 当前工作目录的 `build/`
204
+
205
+ 装了 CMake 的话(产物同样落在 `<项目根>/build/`):
206
+
207
+ ```bash
208
+ cmake -S native -B build/cmake
209
+ cmake --build build/cmake --config Release
210
+ ```
211
+
212
+ ### 3.3 发布到 PyPI
213
+
214
+ ```bash
215
+ python build.py # 先确保 build/ 里有本平台的动态库
216
+ python setup.py sdist bdist_wheel # 产物在 dist/(wheel 会自动带上动态库)
217
+ python -m twine check dist/* # 检查元数据与 README 渲染
218
+ python -m twine upload dist/* # 需要 PyPI API Token
219
+ ```
220
+
221
+ 几点约定:
222
+
223
+ * **wheel 会按本机平台打标签**(Windows 上是 `win_amd64`)。带原生库的包绝不能打成
224
+ `py3-none-any`,否则 Linux/macOS 的 pip 也会装这个 Windows 包。想给其它平台发包,
225
+ 就在对应平台上各构建一次(推荐用 CI)。
226
+ * Linux 上 `setup.py bdist_wheel` 得到的是 `linux_x86_64`,**PyPI 不收**这个标签,
227
+ 需要用 `auditwheel`(manylinux)或 `auditwheel repair` 处理后再上传;
228
+ 实在不方便就只发 sdist。
229
+ * 没有编译器时 `build_py` 只告警不中断,但这种 wheel 是空壳,别传上去;
230
+ 加 `PYCDC_SKIP_NATIVE_BUILD=1` 可以显式跳过原生构建。
231
+ * sdist 里包含完整的 `native/` 内核源码——这既是构建需要,也是 GPLv3 的要求。
232
+
233
+ ## 4. Python 对外接口
234
+
235
+ ### 4.1 反编译
236
+
237
+ | 函数 | 说明 |
238
+ | --- | --- |
239
+ | `decompile_file(path, *, include_header=True)` | 反编译磁盘上的 `.pyc` |
240
+ | `decompile(data, *, include_header=True, display_name=None)` | 反编译内存中的 pyc 字节 |
241
+ | `decompile_code_object(code, *, include_header=True)` | 反编译运行中的 `types.CodeType` |
242
+ | `decompile_marshalled(data, major, minor, ...)` | 反编译纯 marshal 的 code object |
243
+ | `decompile_to_file(source, output, ...)` | 反编译并写入文件 |
244
+
245
+ ### 4.2 反汇编(等价 pycdas)
246
+
247
+ `disassemble_file(path, *, verbose=False, show_caches=False)`、
248
+ `disassemble(data, ...)`、`disassemble_code_object(code, ...)`
249
+
250
+ ### 4.3 版本信息
251
+
252
+ | 函数 | 说明 |
253
+ | --- | --- |
254
+ | `get_pyc_version(source)` | 探测 pyc 版本,返回 `PycVersion(3, 13)` |
255
+ | `get_magic_version(magic)` | magic number → 版本 |
256
+ | `read_pyc_magic(source)` | 读取前 4 字节 magic |
257
+ | `supported_versions()` | 内核支持的版本列表 |
258
+ | `is_supported_version(major, minor)` | 版本是否受支持 |
259
+ | `backend_version()` | 内核版本字符串 |
260
+ | `last_report()` | 最近一次反编译产生的诊断信息 |
261
+
262
+ ### 4.4 异常
263
+
264
+ ```python
265
+ try:
266
+ src = decompile_file("broken.pyc")
267
+ except pc.PycDecompileError as exc:
268
+ print(exc.status_name) # PYCDC_ERR_BAD_MAGIC / PYCDC_ERR_MARSHAL / ...
269
+ print(exc) # [PYCDC_ERR_BAD_MAGIC] 反编译 xxx 失败: ...
270
+ ```
271
+
272
+ 状态码见 `pycdecompile_api.h` 的 `PycdcStatus`。
273
+
274
+ ## 5. 命令行
275
+
276
+ 装好包之后可以用 `pycdecompile` 命令,也可以始终用 `python -m pycdecompiler`:
277
+
278
+ ```bash
279
+ pycdecompile demo.pyc # 打印到 stdout
280
+ pycdecompile demo.pyc -o demo.py # 写入文件
281
+ pycdecompile -d demo.pyc # 反汇编(等价 pycdas)
282
+ pycdecompile --version-info demo.pyc # 只打印 pyc 版本
283
+ pycdecompile --list-versions # 列出内核支持的所有版本
284
+ pycdecompile --version # 打印本包版本
285
+ ```
286
+
287
+ 退出码:`0` 成功;`1` 反编译/写入失败;`2` 参数或文件问题;
288
+ `3` 找不到原生核心(尚未 `python build.py`)。
289
+
290
+ ## 6. 测试
291
+
292
+ ```bash
293
+ python tools/official_test.py # 用本机 Python 编译 input/*.py 后比对
294
+ python tools/official_test.py --compiled # 跑仓库自带 pyc(覆盖 3.1/3.12 等)
295
+ python tools/corpus_test.py # AST 级别比对
296
+ python tools/needed_opcodes.py # 逐文件列出缺失的 opcode
297
+ python tools/diff_one.py f-string.py # 单文件差异
298
+ python tools/show_syntax.py argparse.py # 看标准库反编译结果哪里语法不合法
299
+ python tools/stress_stdlib.py # 标准库压力测试(崩溃/语法分类统计)
300
+ python -m pytest tests_pycdecompiler.py # 单元测试(需要 pytest)
301
+ python run_tests.py # 零依赖的单元测试运行器
302
+ python smoke_test.py # 冒烟:能不能加载库 + 反编译几个样例
303
+ python -m twine check dist/* # 打包前后的元数据自检
304
+ ```
305
+
306
+ ### 标准库压力测试
307
+
308
+ 每一轮修改后建议跑一次 `python tools/stress_stdlib.py`:它把 32 个标准库模块
309
+ 用本机 Python 编译后逐个反编译(每个都在独立子进程里,任何一个把进程搞崩都
310
+ 会被记成 `CRASH` 而不是拖垮整轮测试),并按结果分类:
311
+
312
+ | 分类 | 含义 |
313
+ | --- | --- |
314
+ | `ok` | 反编译成功且内核无诊断 |
315
+ | `warn` | 成功但有诊断(`Unsupported opcode`、块栈未清空等) |
316
+ | `syntax` | 反编译出结果,但 `ast.parse` 失败(语法不合法) |
317
+ | `error` | 抛了 `PycDecompileError` |
318
+ | `CRASH` | 子进程被信号/异常终止 |
319
+
320
+ ### 当前实测结果
321
+
322
+ `tools/official_test.py --compiled`(仓库自带 191 个 pyc,与上游 `tests/tokenized`
323
+ 期望输出逐 token 比对,即上游官方测试口径):
324
+
325
+ | 版本 | 一致率 |
326
+ | --- | --- |
327
+ | Python 3.1 | 4/4 (100%) |
328
+ | Python 3.11 | 8/8 (100%) |
329
+ | Python 3.12 | 4/6 (67%) |
330
+ | Python 3.13 | 本机自编译语料 17/54 (31.5%) |
331
+ | 1.0 ~ 3.13 全量 | 143/191 (75%) |
332
+
333
+ `tools/stress_multi.py --python <解释器>` 用真实解释器编译标准库再反编译,
334
+ 覆盖 3.13 / 3.14 / 3.15(本机实测版本:3.13.15 / 3.14.3 / 3.15.0rc1):
335
+
336
+ | 目标版本 | error / CRASH | syntax | warn | ok |
337
+ | --- | --- | --- | --- | --- |
338
+ | 3.13 | **0** | 24 | 3 | 1 |
339
+ | 3.14 | **0** | 24 | 3 | 0 |
340
+ | 3.15 | **0** | 24 | 3 | 0 |
341
+
342
+ > 三个版本都能完整解析 `.pyc`(magic / marshal / opcode)并产出源码,没有崩溃或异常;
343
+ > 语法/语义层面的差异见「已知限制」。
344
+
345
+ 标准库压力测试(`tools/stress_stdlib.py`,32 个模块,本机 Python 3.11):
346
+
347
+ | 阶段 | error(崩溃/异常) | syntax | warn | ok |
348
+ | --- | --- | --- | --- | --- |
349
+ | 修复前 | 17 | 12 | 2 | 1 |
350
+ | 修复后 | **0** | 27 | 3 | 2 |
351
+
352
+ > 崩溃类(17 个模块直接失败)已经清零:现在 32 个模块都能跑完并产出源码,
353
+ > 其中 5 个与原始源码完全一致、27 个仍有语法/语义差异(见「已知限制」)。
354
+
355
+ > 3.13 一致率偏低的原因见下方「已知限制」——绝大多数用例能产出**语法合法**的
356
+ > Python 代码,但与原始源码存在语义差异(上游 fork 对 3.11+ 的若干结构本身就不完整)。
357
+
358
+ ## 7. 本项目对内核做的改动
359
+
360
+ ### 7.1 新增
361
+
362
+ | 文件 | 内容 |
363
+ | --- | --- |
364
+ | `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` |
365
+ | `pycdecompile_log.h/.cpp` | 统一的诊断出口:既写 stderr(命令行行为不变),又留一份给 `pycdc_last_report()`,供其它语言宿主读取 |
366
+
367
+ ### 7.2 修改
368
+
369
+ | 文件 | 改动 |
370
+ | --- | --- |
371
+ | `data.cpp` | `PycFile/PycBuffer` 的 `std::exit(1)` 改为抛 `std::runtime_error`——作为库被嵌入时不能再杀宿主进程 |
372
+ | `pyc_module.h/.cpp` | 新增 `loadFromData` / `loadFromMarshalledData`(内存加载)、`magicToVersion`、`maxSupportedMinor`;`isSupportedVersion` 放行 3.13 |
373
+ | `pyc_module.cpp` / `pycdas.cpp` / `pycdc.cpp` | 加载失败改为异常路径,命令行工具已加 try/catch |
374
+ | `ASTNode.h` | `ASTFunction::setDefArgs/setKwDefArgs`、`ASTFormattedValue::setFormatSpec` |
375
+ | `ASTree.cpp` | 见下 |
376
+
377
+ ### 7.3 `ASTree.cpp` 补全的 Python 3.12 / 3.13 指令
378
+
379
+ **函数构造**
380
+
381
+ * `MAKE_FUNCTION`(3.13 无参形式)
382
+ * `SET_FUNCTION_ATTRIBUTE`(默认值 / 关键字默认值,展开 tuple、dict 后复用原有打印逻辑)
383
+
384
+ **f-string**
385
+
386
+ * `CONVERT_VALUE`、`FORMAT_SIMPLE`、`FORMAT_WITH_SPEC`
387
+
388
+ **调用约定**
389
+
390
+ * 3.13 的栈布局由 `[NULL, callable, args]` 变为 `[callable, self, args]`,
391
+ 相应修正 `CALL`、`CALL_FUNCTION_EX`、`LOAD_ATTR`、`LOAD_BUILD_CLASS` 的识别
392
+ * 新增 `CALL_KW`(含类定义 `class A(B, metaclass=M)`)、`CALL_FUNCTION_EX`、
393
+ `DICT_MERGE`、`DICT_UPDATE`
394
+ * `LOAD_SUPER_ATTR`(`super().m()`)、`CALL_INTRINSIC_1/2`
395
+
396
+ **条件与循环**
397
+
398
+ * `POP_JUMP_IF_NONE` / `POP_JUMP_IF_NOT_NONE`(`if x is None:`)
399
+ * `TO_BOOL`、`RETURN_CONST`(含 `if` 分支收尾)、`RETURN_GENERATOR`
400
+
401
+ **闭包与帧**
402
+
403
+ * `MAKE_CELL`、`COPY_FREE_VARS`、`LOAD_FAST_CHECK`、`LOAD_FAST_AND_CLEAR`、
404
+ `DELETE_DEREF`、`LOAD_FROM_DICT_OR_DEREF` / `_GLOBALS`、`EXIT_INIT_CHECK`
405
+
406
+ **微融合指令**
407
+
408
+ * `STORE_FAST_LOAD_FAST`、`STORE_FAST_STORE_FAST`
409
+
410
+ **推导式与容器**
411
+
412
+ * `MAP_ADD`、`SET_ADD`
413
+
414
+ **await / async**
415
+
416
+ * `SEND`、`END_SEND`、`CLEANUP_THROW`、`END_ASYNC_FOR`、`BEFORE_ASYNC_WITH`;
417
+ 用 `inAwait` 标记区分 await 协议内部的 `YIELD_VALUE` 与真正的 `yield`
418
+ * `YIELD_VALUE_A`、`GET_AWAITABLE_A`(3.12+ 起这两条指令带 oparg)
419
+
420
+ **其它**
421
+
422
+ * `GET_LEN`、`LOAD_ASSERTION_ERROR`、`ENTER_EXECUTOR`
423
+ * 各种 `INSTRUMENTED_*` 变体
424
+ * `decompyle()` 结尾的隐式 `return` 清理改为循环剥离(3.12 起可能出现连续两条)
425
+ * 内核里 31 处 `fprintf(stderr, ...)` 改走 `pycdc_report()`
426
+
427
+ ### 7.4 本次针对 Python 3.11+ 的修复
428
+
429
+ **稳定性(进程不再被杀)**
430
+
431
+ | 问题 | 原因 | 修复 |
432
+ | --- | --- | --- |
433
+ | 反编译标准库时 `PycBuffer::getByte(): Unexpected end of stream` | `RETURN_VALUE` 分支无条件多读一条指令「跳过分支多余的指令」,最后一条指令恰好是 `RETURN_VALUE` 时就跨过字节码末尾 | 读之前判断 `source.atEof()`(`RETURN_CONST` 早已有此守卫,`RETURN_VALUE` 漏了) |
434
+ | 进程被 abort / 访问越界杀死(17/32 个标准库模块) | `std::stack::pop()/top()` 作用在空栈上是 UB:`stack_hist.pop()`、`blocks.top()` 在块栈/历史栈提前耗尽时踩空 | 新增空栈安全的 `stackhist_t` / `BlockStack`(`FastStack.h`),空栈时 `pop` 退化为空操作、`top` 返回占位块;`JUMP_BACKWARD` 的块栈循环加空栈守卫 |
435
+ | 坏掉的异常表让整次反编译失败 | `exceptionTableEntries()` 直接 `getByte()` 越界抛异常 | 改成有界解析(`_try_parse_varint`),截断的表只告警不抛异常 |
436
+ | 宿主进程被内核带走(GUI 窗口成孤儿) | 调用方在自己的进程里直接调 ctypes 内核 | 内核侧加了 VEH + longjmp 护栏:访问越界转成带 opcode 定位的 `PycDecompileError`;调用方仍应把不可信 pyc 放在子进程里跑(见 `tools/stress_stdlib.py` 的隔离写法) |
437
+
438
+ **功能补全**
439
+
440
+ | 指令 / 结构 | 说明 |
441
+ | --- | --- |
442
+ | `UNPACK_EX` | 星号解包 `a, *b, c = seq`;新增 `ASTStarred` 节点,按 oparg 的 before/after 定位星号目标 |
443
+ | `POP_JUMP_FORWARD/BACKWARD_IF_NONE`、`_IF_NOT_NONE`、`POP_JUMP_BACKWARD_IF_FALSE/TRUE` | 3.11 的条件跳转;`is None` 的条件按「不跳转即进入分支体」取反 |
444
+ | 3.8+ 的 `while` 循环重建 | 3.8 起没有 `SETUP_LOOP`,改为识别回边:`if cond: body; if cond: goto body` → `while cond: body` |
445
+ | `DICT_UPDATE` | `{**a, **b}`:字典条目允许「无键」表示 `**mapping` |
446
+ | `LIST_EXTEND` / `SET_UPDATE` / `LIST_TO_TUPLE` | `(*a, *b)` / `[*a, *b]` / `{*a, *b}` 的星号显示;修掉原来遇到非常量就丢弃左值导致的栈失衡 |
447
+ | 3.11+ try/except(零开销异常表) | 用「处理器的代码形态」区分 except / finally(`PUSH_EXC_INFO` 后有没有 `CHECK_EXC_MATCH`),支持裸 `except:`、`except X as e:`(含隐式 `del e` 清理的省略)、循环内的 try;没有配对处理器的 `try` 退化为普通语句块,不再产出悬空 `try:` |
448
+ | `PRINT_EXPR` / `ASYNC_GEN_WRAP` | 源码层面不可见,按无操作处理 |
449
+ | `MAKE_FUNCTION` 的位置默认值 | 3.6 起默认值在栈上是「一个 tuple 常量」(`def f(a, b=1)` → `LOAD_CONST (1,)`),原先直接当成单个默认值,输出成 `b = (1,)`;现在按 3.6+ 且 oparg==1 展开成逐个默认值 |
450
+
451
+ 另外去掉了 `RETURN_VALUE` 里那条「多读一条指令」的逻辑——它会吞掉紧跟其后的
452
+ `elif` 条件(`LOAD_FAST`),是 `if/elif` 链产出 `None is not None` 之类错乱的根因。
453
+
454
+ ### 7.5 Python 3.13 / 3.14 / 3.15 支持
455
+
456
+ | 版本 | magic | 支持程度 | 说明 |
457
+ | --- | --- | --- | --- |
458
+ | 3.13 | `0x0A0D0DF3` | 完整(沿用既有实现 + 本轮补的 `RETURN_VALUE`/块栈/异常表修复) | opcode 映射由 3.13.15 解释器导出,138 条 |
459
+ | 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`)** 也已解析 |
460
+ | 3.15 | `0x0A0D0E52` | 简要支持 | 在 3.14 基础上:`GET_ITER` 带参、`TRACE_RECORD`、`LOAD_COMMON_CONSTANT` 表扩充、**`IMPORT_NAME` 的 namei = oparg >> 2**、类体新增的 `__classdict__`/`__classdictcell__` 内部赋值在输出时剔除 |
461
+
462
+ opcode 映射表由脚本从**真实解释器**导出,避免手抄出错:
463
+
464
+ ```bash
465
+ python tools/gen_opcode_map.py /path/to/python3.14 --write # 生成 bytes/python_3_14.cpp
466
+ python tools/gen_opcode_map.py /path/to/python3.15 # 只检查缺口
467
+ ```
468
+
469
+ 脚本会解析 `bytecode_ops.inl` 里已有的枚举符号(`NAME` = 无参、`NAME_A` = 带参),
470
+ 把新版本的 opcode 名字映射过去,并列出「未声明」「arg 属性冲突」两类需要人工处理的项。
471
+ 新版本若给某条指令加了参数(例如 3.15 的 `GET_ITER`),需要先在
472
+ `bytecode_ops.inl` 里补一个 `OPCODE_A(GET_ITER)`(无参的 `OPCODE(GET_ITER)` 保留给旧版本)。
473
+
474
+ ### 7.6 语义重建(操作数栈失同步的修复)
475
+
476
+ 「能跑完」和「跑对」之间差的是**操作数栈语义**。本轮针对 3.12+ 的
477
+ `SWAP`/`COPY`/`STORE_FAST_STORE_FAST` 与推导式做了重建:
478
+
479
+ | 问题 | 现象 | 修复 |
480
+ | --- | --- | --- |
481
+ | `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 及更早才走多赋值启发式 |
482
+ | `STORE_FAST_STORE_FAST` 拆成两条 store | `a, b = b, a` 输出成 `b = a; a = b`(**语义错误**,交换失效) | 合并成一次元组赋值 `(a, b) = (b, a)`;CPython 语义是 TOS → 高半字节、TOS1 → 低半字节,源码顺序的**第一个目标在低半字节** |
483
+ | 推导式 / 生成器表达式的 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)` |
484
+ | 链式比较被判成 `or` | `if x == y == z:` → `if x == y or y == z:`(**语义错误**) | 新增判据:两次条件跳转**极性一致 = and**、相反 = or。链式比较的两次 `POP_JUMP_IF_FALSE` 目标并不相同(一次落在清理分支上),只比较位置会误判 |
485
+ | 形参打印顺序违反源码语法 | `def f(*args, x=1)` → `def f(*, x=1, *args)`(同一个 `*` 出现两次,`ast.parse` 直接报错);`getLocal` 还会越界 | 新增 `print_function_params()`:localsplus 顺序是 [位置, kwonly, *args, **kwargs],源码顺序是 [位置, *args/裸*, kwonly, **kwargs],按下标显式取值输出(具名函数与 lambda 共用) |
486
+ | 关键字默认值丢失 | `def f(a, *, b=3)` → `def f(a, *, b)` | `MAKE_FUNCTION` 的 oparg 是位掩码:0x01 位置默认值(tuple 常量)、0x02 关键字默认值(3.11+ 是 `BUILD_CONST_KEY_MAP`)、0x03 两者都有。这三种组合都展开,其余(注解/闭包)仍走旧的按数值弹栈逻辑 |
487
+
488
+ 对应的回归语料放在 `tools/corpus/semantics_313.py`(增广赋值、并行赋值、
489
+ 链式比较、各类推导式、变参签名、嵌套解包、with、try/except/else/finally),
490
+ `tools/show_syntax_pyc.py <解释器> <模块>` 可以逐点查看失败位置。
491
+
492
+ 效果(标准库压力测试,语法合法 + 有诊断的模块数):
493
+
494
+ | 版本 | 修复前 syntax / warn / ok | 修复后 syntax / warn / ok |
495
+ | --- | --- | --- |
496
+ | 3.11 | 27 / 2 / 1 | 27 / 2 / 1(未受影响,SWAP 分支保持不变) |
497
+ | 3.13 | 22 / 5 / 1 | **19 / 5 / 4** |
498
+ | 3.14 | 23 / 4 / 0 | **20 / 7 / 0** |
499
+ | 3.15 | 24 / 3 / 0 | **20 / 7 / 0** |
500
+
501
+ ### 7.7 源码内置化 + Bug 修复
502
+
503
+ 内核源码并入 `native/`,全仓库不再有任何地方引用 `pycdc-master/`;
504
+ 顺手修掉了这一路上看到的问题:
505
+
506
+ **构建与工具链**
507
+
508
+ | 问题 | 修复 |
509
+ | --- | --- |
510
+ | `build.py` 的 `--jobs` 参数收下了却从没用过(永远是单进程一把梭) | 真正并行编译,默认取 CPU 核心数 |
511
+ | 每次构建都重编全部 45 个源文件 | 增量构建:按目标文件/头文件时间戳判断,只编过期的 |
512
+ | `tools/show_syntax_pyc.py` 里写死了**另一台机器另一个项目**的绝对路径(`D:\modules\exe-reverse-gui\pycdecompile`),换机即废 | 改为按 `__file__` 推导项目根,并补上标准的 `main()` 结构 |
513
+ | `tools/dump_dis.py` 导入了用不到的 `Callable` | 删掉 |
514
+
515
+ **C 内核 / C ABI 导出层**
516
+
517
+ | 问题 | 修复 |
518
+ | --- | --- |
519
+ | **非 Windows 平台根本编不过**:`g_fault_ip` / `HMODULE` / `GetModuleHandleExA` 只在 `#ifdef _WIN32` 里定义,却在 `opcodeCrashMessage()` 里被无条件使用 | 补上同样的条件编译,Linux/macOS 现在能构建出 `.so` / `.dylib` |
520
+ | **Windows 上中文(或任何非 ASCII)路径的 .pyc 打不开**:CRT 的 `fopen` 按 ANSI 代码页解释窄字符路径 | 新增 `pycdc_fopen_utf8()`:Windows 转 UTF-16 走 `_wfopen`,字节串不是合法 UTF-8 时退回 `fopen`;`PycFile`、`pycdc_probe_file`、`pycdc_set_trace` 全部改走它 |
521
+ | `runGuarded()` 在 setjmp/longjmp 之间构造 `std::string` 返回值:被 longjmp 跨过的析构是 UB | `ostringstream` 移到护栏之外,护栏内只写不构造 |
522
+ | `PycFile::atEof()` 在文件没打开时对空 `FILE*` 调 `fgetc`/`ungetc` | 空流直接返回「已到末尾」 |
523
+ | `PycBuffer::atEof()` 用 `==` 判断,位置一旦越界就永远返回「未到末尾」,后续 `getByte()` 越界读 | 改成 `>=` |
524
+ | `formatted_printv()` 在 `vsnprintf` 失败提前返回时漏了 `va_end`(`va_copy` 出来的那份) | 提前返回前补 `va_end` |
525
+ | `#include "data.h"` 重复包含 | 删掉 |
526
+
527
+ **Python 层**
528
+
529
+ | 问题 | 修复 |
530
+ | --- | --- |
531
+ | `_native._load_library()` 把 `os.add_dll_directory()` 的句柄存在局部列表里,函数一返回句柄就被回收,DLL 搜索目录随之失效 | 句柄改为模块级长期持有 |
532
+ | `_decode_result()` 在 native 返回 NULL 指针却报 `status=OK` 时,会拼出「失败(状态码 0)」这种自相矛盾的异常 | 归成 `PYCDC_ERR_INTERNAL` |
533
+ | `supported_versions()` / `is_supported_version()` 绕过 `_CALL_LOCK` 直接摸 `_native.lib`——内核用全局缓冲区保存最近错误,多线程下会读到别人的错误信息 | 新增加锁的 `_native.call_supported_range()`,core 只调它 |
534
+ | `_is_pathlike()` 把 `bytes` 也算路径(只是当时没人调用它),一旦用上就会把 pyc 字节流当文件名 | 只认 `str` / `os.PathLike`,并让 `_load_source()` 真正用它 |
535
+ | `disassemble()` 声明收 pyc 数据,传路径会 `TypeError`(与 `decompile()` 行为不一致) | 两者都接受,展示名也跟着走 |
536
+ | `decompile_code_object()` / `disassemble_code_object()` 把 major 写死成 3,只挡了 ≥3.16 | 统一交给 `is_supported_version()` 判断 |
537
+ | `__init__.py` 的 `__all__` 漏了 `last_report`,`from pycdecompiler import *` 拿不到 | 补进 `__all__` |
538
+ | CLI 把结果写到控制台时,源码里有控制台代码页装不下的字符(Windows GBK 控制台遇到日文/emoji)会直接 `UnicodeEncodeError` 崩掉 | 启动时把 stdout/stderr 调成 `errors="replace"`;写输出文件失败也给出干净的错误而不是裸 `OSError` |
539
+ | `example.py`:`if isinstance(code, type(compile("", "", "exec"))): pass` 是段死代码;章节标题写死「Python 3.13」(实际用的是当前解释器);`f"{len(...) and ''}3. {ver}"` 拼出「3. 3.1」这样的标题 | 逐条改掉 |
540
+
541
+ 复现非 ASCII 路径那条(修复前必然失败):
542
+
543
+ ```python
544
+ import py_compile, tempfile
545
+ from pathlib import Path
546
+ import pycdecompiler as pc
547
+
548
+ tmp = Path(tempfile.mkdtemp())
549
+ src = tmp / "测试模块.py"
550
+ src.write_text("a = 1\n", encoding="utf-8")
551
+ pyc = tmp / "测试模块.pyc"
552
+ py_compile.compile(str(src), cfile=str(pyc), doraise=True)
553
+ print(pc.decompile_file(pyc)) # 修复前:PYCDC_ERR_DECOMPILE / Error opening file
554
+ ```
555
+
556
+ ## 8. 已知限制
557
+
558
+ 明确**尚未支持**,遇到时会报 `Unsupported opcode: XXX` 并输出
559
+ `# WARNING: Decompyle incomplete`(而不是静默产出错误代码):
560
+
561
+ * 增广赋值 `x.attr += y` / `x[i] += y`(3.13 经 `COPY`/`SWAP`/`STORE_ATTR` 重构)目前会丢失赋值语句,只剩后续读取
562
+ * `match` 语句相关:`MATCH_MAPPING` / `MATCH_SEQUENCE` / `MATCH_KEYS` / `MATCH_CLASS`
563
+ * `CHECK_EG_MATCH`、`PREP_RERAISE_STAR` —— 异常组(`except*`)
564
+
565
+ 仍然**能跑但结果不完美**的部分(不会崩、不会抛异常,但语法/语义与源码有差异):
566
+
567
+ * 3.11 及更早的 `SWAP` 多赋值启发式仍是猜测式的:`a, b = b, a` 这类写法
568
+ 在这些版本上还会退化成顺序赋值(3.12+ 已修正)
569
+ * `with` 语句(`BEFORE_WITH` / 3.14 的 `LOAD_SPECIAL` 协议)尚未重建,
570
+ 会输出残缺的 `with None:`
571
+ * 嵌套 try 的块层次偶尔会把外层 `except` 挂到内层处理器里;
572
+ `try/finally`(3.11 的 finally 处理器)尚未重建,finally 体可能重复输出
573
+ * 生成器表达式在部分版本上仍会把隐式参数 `.0` 留在输出里(迭代对象无法还原),
574
+ 这类模块会因非法标识符而在 `ast.parse` 阶段失败
575
+ * 多条件 `if`(`if a and b and c:`)的分支体偶尔被挂到 if 之外
576
+ * 带关键字默认值(`def f(a, *, b=1)`)、注解或闭包的函数定义只保留形参名,
577
+ 默认值会被丢掉(沿用旧行为;展开这些组合会让 zipfile 一类的模块在内核里崩溃,
578
+ 因此先保持现状)
579
+ * 内联推导式(`LOAD_FAST_AND_CLEAR` + `SWAP`)在部分写法下仍会产出近似代码
580
+
581
+ 上游 fork 本身对 3.12/3.13 支持不完整是主要原因;本项目的定位是把
582
+ **调用链路、构建、接口、测试** 全部打通,并尽可能补全最高频的指令。
583
+
584
+ ## 9. 继续扩展的方法
585
+
586
+ ```bash
587
+ # 1. 找出缺口:某条指令没被处理时会从这里列出来
588
+ python tools/needed_opcodes.py
589
+
590
+ # 2. 看这条指令在真实字节码里的形态
591
+ python tools/dump_dis.py # 或 python -c "import dis; dis.dis(compile(...))"
592
+
593
+ # 3. 在 ASTree.cpp 的 switch 里补 case(新指令都集中在文件末尾的
594
+ # “Python 3.12 / 3.13 新增(或语义有变)的指令” 段落)
595
+
596
+ # 4. 重建 + 回归
597
+ python build.py && python tools/official_test.py --compiled
598
+ ```
599
+
600
+ ## 10. 许可与致谢
601
+
602
+ **本项目整体以 GNU General Public License v3(GPL-3.0-only)发布**,
603
+ 许可证全文见根目录的 `LICENSE`(与 `native/LICENSE` 内容一致)。
604
+
605
+ ### 10.1 上游与致谢
606
+
607
+ 反编译内核来自 [Decompyle++(pycdc)](https://github.com/zrax/pycdc):
608
+
609
+ > Decompyle++ is the work of **Michael Hansen** and **Darryl Pogue**.
610
+ > It is released under the terms of the GNU General Public License, version 3.
611
+
612
+ 本项目在 `native/` 中分发的是该内核源码(含本项目对 3.11 ~ 3.15 指令的补全与
613
+ 稳定性修复,逐条列在 `native/README.md` 与本文第 7 节)。上游的著作权归
614
+ Michael Hansen、Darryl Pogue 等原作者所有,本仓库不对其主张任何权利。
615
+ `tests_data/` 是上游仓库自带的回归语料,同样遵循 GPLv3。
616
+
617
+ ### 10.2 为什么这样分发不构成侵权
618
+
619
+ * **许可兼容**:内核是 GPLv3,本项目也整体以 GPLv3 发布,没有混入任何
620
+ GPL 不兼容的代码;`pycdecompile` 的全部依赖都是标准库。
621
+ * **保留许可证与声明**:根目录 `LICENSE`、`native/LICENSE` 都是 GPLv3 全文,
622
+ 并且两处都进了 sdist 和 wheel(`*.dist-info/licenses/`)。
623
+ * **提供完整对应源码**:sdist 里带完整的 `native/` C++ 源码与构建脚本
624
+ (`MANIFEST.in` 明确包含),符合 GPLv3 第 6 条对源码分发的要求;
625
+ 构建出来的动态库也随 wheel 一并分发。
626
+ * **署名**:README、`native/README.md`、`setup.py` 的 `project_urls`
627
+ 都指明了上游项目与作者。
628
+
629
+ ### 10.3 使用者的义务
630
+
631
+ GPLv3 是传染性许可:如果你基于本项目(或其中的内核)发布衍生作品,
632
+ 需要同样以 GPLv3 发布,并提供完整的对应源码。把本包用在内部工具里、
633
+ 或者只是拿它反编译自己的 `.pyc`,都不受额外限制。
634
+
635
+ ### 10.4 免责
636
+
637
+ 本工具仅用于**合法的逆向分析、取证、兼容性研究与学习**。反编译得到的源码
638
+ 其著作权仍归原作者所有,请勿用于侵犯他人权益的用途。程序按「原样」提供,
639
+ 不附带任何担保。
@@ -0,0 +1,14 @@
1
+ pycdecompile-0.1.2.1.dist-info/licenses/LICENSE,sha256=jOtLnuWt7d5Hsx6XXB2QxzrSe2sWWh3NgMfFRetluQM,35147
2
+ pycdecompile-0.1.2.1.dist-info/licenses/native/LICENSE,sha256=jOtLnuWt7d5Hsx6XXB2QxzrSe2sWWh3NgMfFRetluQM,35147
3
+ pycdecompiler/__init__.py,sha256=-M-wkPF_4v_e5KbZYkNY4parRavy9nQgolOKxVzauZ8,1508
4
+ pycdecompiler/__main__.py,sha256=MHKZ_ae3fSLGTLUUMOx15fWdeOnJSHhq-zslRP5F5Lc,79
5
+ pycdecompiler/_native.py,sha256=5vQwMjPSQylt7ijM9Y9IR6RVB77wZAZB2h-375vUARQ,13896
6
+ pycdecompiler/_version.py,sha256=s2nAeTk3jpDTvL03W204b-HQSOioToP6fOsi2Y3y8-o,251
7
+ pycdecompiler/cli.py,sha256=uyoFID31rJGKu7BdtCD3ckbv7L7coFfdgwnQoMqcdGc,4917
8
+ pycdecompiler/core.py,sha256=Ai3Hh-DNu6tu6pF1rkn0sK1TeOxsvJtL8R4WG1ZePPI,11424
9
+ pycdecompiler/_lib/pycdecompile.dll,sha256=vILPnJWdWoUM_b_HO76sAHyIwTFR-i9fhKtv1Ca19KI,1438720
10
+ pycdecompile-0.1.2.1.dist-info/METADATA,sha256=HY3x3MFh2UQ8e0fHp3iouTYPYaJfGo4SEbed0YrtfU4,35946
11
+ pycdecompile-0.1.2.1.dist-info/WHEEL,sha256=ZjXRCNaQ9YSypEK2TE0LRB0sy2OVXSszb4Sx1XjM99k,97
12
+ pycdecompile-0.1.2.1.dist-info/entry_points.txt,sha256=T-JgFTDfbCpiz17bPLqOKJb_vHF8vIur-OGAaf_sDUY,56
13
+ pycdecompile-0.1.2.1.dist-info/top_level.txt,sha256=lnVsicnePHCdzCl1Z3bt_OEfrou7v_6GzmcK4IBZObc,14
14
+ pycdecompile-0.1.2.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-win_amd64
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pycdecompile = pycdecompiler.cli:main