科学岛地下机房的应急灯亮了一整夜。
程思语靠在机架旁睡了不到三个小时,醒来时陈默还坐在屏幕前。ThinkPad 的电池从满格掉到了 63%,桌角堆着两包空的压缩饼干包装袋。
「你睡过吗?」她问。
「睡了。」陈默没回头,「五分钟。」
屏幕上,`RaiseGuard` 类比昨晚多了七行。第三行之后的进展很慢——不是写不出来,是每次写几行就要停下来想:如果这条路被程泰来堵了,有没有备用路径。
程思语走到他身后,看着屏幕。
```python
class RaiseGuard:
"""编译验证器:确保每一条路径上的 raise 都真实存在。"""
def __init__(self):
self._trusted_compile = compile
```
「你存了一份原生的 `compile`?」程思语问。
「程泰来在 PYC 后处理做手脚。如果我直接从源码编译,不走文件系统——字节码应该是干净的。」
「应该?」
陈默敲下一段测试代码:
```python
def test_raise():
try:
raise ValueError("test")
except ValueError as e:
return str(e)
# 使用原生 compile,不落盘
src = """
def test_raise():
try:
raise ValueError("test")
except ValueError as e:
return str(e)
"""
code = compile(src, '<trusted>', 'exec')
```
然后他打开了 `dis`:
```python
import dis
exec_scope = {}
exec(code, exec_scope)
dis.dis(exec_scope['test_raise'])
```
屏幕输出:
```
0 LOAD_GLOBAL ValueError
2 RAISE_VARARGS 1
4 ...
```
「`RAISE_VARARGS` 在。」陈默说,「直接 `compile` → `exec`,不经过 PYC 持久化——字节码完好。」
程思语没有高兴。她盯着那行 `RAISE_VARARGS`,缓缓说:「可你每次重启程序都要重新编译全部代码?生产环境不可能的。」
陈默沉默了三秒。
「所以不能每次都编译。但要保证每次加载的代码都是可信的。」
他敲下了 `RaiseGuard` 的核心方法:
```python
import types
def verify_code_object(self, code_obj, path='<root>'):
"""递归验证代码对象及其所有嵌套函数。返回 (可信, 证据列表)"""
evidence = []
# 1. 验证 co_code 中 RAISE_VARARGS 的存在
raw = code_obj.co_code
has_raise = False
i = 0
while i < len(raw):
op = raw[i]
if op == 130: # RAISE_VARARGS
has_raise = True
evidence.append((path, i, 'RAISE_VARARGS 存在'))
i += 1
# 2. 递归验证嵌套的函数
for idx, const in enumerate(code_obj.co_consts):
if hasattr(const, 'co_code'):
sub_path = f'{path}.co_consts[{idx}]'
sub_ok, sub_ev = self.verify_code_object(const, sub_path)
evidence.extend(sub_ev)
return has_raise, evidence
```
程思语看着代码,忽然说:「这个验证是事后诸葛亮。你想验证,得先有代码对象——但代码对象已经是编译后的了。如果编译过程本身有毒呢?」
陈默的手指悬在键盘上。
她说对了。
程泰来的后处理发生在 PYC 写入阶段——文件系统层面。而 `compile()` 本身是干净的。但如果程泰来连 `compile` 内置函数也替换了呢?如果他在运行时的 `builtins` 层面就下毒了呢?
「那我们就不能信任任何已经存在的 `compile`。」陈默敲了一行新的代码:
```python
import sys
_original_compile = sys.gettotalrefcount # 占位
```
他删掉那行。这不是开玩笑的时候。
他重新写:
```python
import __builtins__
# 在模块加载时,捕获一份绝对原始的 compile
# 方法:在 Python 解释器刚启动时,通过 types.CodeType 重构编译器
```
「没用的。」程思语说,「如果解释器本身被污染了,你在哪里捕获都是脏的。」
陈默盯着屏幕。
她说的都对。每一个层面的对策,都假设有一层更底层的东西是干净的。但如果每一层都可能被污染——
「那我们就不信任任何一层。」他说。
他关掉文件,重新打开一个空白窗口。
「我们手写字节码。」
程思语怔了一下:「用手写 `bytes`?」
「用 `types.CodeType`。」
陈默敲下了整个 `RaiseGuard` 最核心的部分——一个不依赖 `compile()`、直接通过 `types.CodeType` 构造函数造出可信代码对象的工厂:
```python
def build_raise_wrapper(original_code):
"""为给定代码对象包装一个可信的异常处理层。"""
# 提取原始函数的关键属性
orig = original_code
# 构造一个新的字节码片段:try: original_func() except: raise
# 手动构造 RAISE_VARARGS 指令:opcode 130, arg 1
trusted_bytecode = bytes([
# 1. LOAD_FAST 'self'
124, 0,
# 2. LOAD_FAST 'args' — 解包参数
# ... (简化:实际需要完整参数传递)
# 3. RAISE_VARARGS 1
130, 1,
# 4. RETURN_VALUE
83, 0,
])
# 构造新的 code object
trusted_code = types.CodeType(
orig.co_argcount, # 参数数量
orig.co_posonlyargcount, # 仅位置参数
orig.co_kwonlyargcount, # 仅关键字参数
orig.co_nlocals, # 局部变量数
orig.co_stacksize, # 栈大小
orig.co_flags, # 标志位
trusted_bytecode, # 可信字节码 ← 核心替换
orig.co_consts, # 常量表(保留)
orig.co_names, # 名称表(保留)
orig.co_varnames, # 变量名表(保留)
orig.co_filename, # 文件名
orig.co_name, # 函数名
orig.co_firstlineno, # 首行号
orig.co_lnotab, # 行号映射表
orig.co_freevars, # 自由变量
orig.co_cellvars # 闭包变量
)
return trusted_code
```
程思语看着这段代码,嘴唇动了动。
「你在重新发明编译器。」
「不是重新发明。」陈默说,「是做一个比编译器更底层的东西——直接用字节码造字节码。编译器可以中毒,字节码构造函数的参数不会骗人。`types.CodeType` 的每一个参数值都是整数、元组、字节串——程泰来改不了整数。」
他按下回车,让 `build_raise_wrapper` 跑在一个从神码模块里提取的代码对象上。
屏幕输出:
```
<code object settle at 0x7f..., file "<trusted>", line 1>
```
没有报错。
「它活了。」程思语轻声说。
「还没。」陈默说,「现在只是有一个壳。真正的 raise 字节码还没和原来的业务逻辑拼在一起。」
他盯着 `trusted_bytecode` 那行 bytes 字面量——总共六个字节的手写指令。要能让一个完整的金融结算函数在异常时真正 `raise`,需要在这六个字节前面,把原来的整个函数逻辑用 `try` 包裹起来。
这需要解析原始 `co_code`,识别函数体边界,插入 `SETUP_FINALLY`,然后在 `except` 块里安放 `RAISE_VARARGS`。
「这工作量——」程思语看着屏幕,「三天不一定够。」
「够。」陈默说,「因为我不用处理所有情况。只需要处理一种——」
他调出神码 AI 核心模块的字节码统计报告。
「——`try` 块里只有一个 `except: pass` 的情况。占全部异常处理缺陷的 87%。」
他敲下新函数——一个针对这种单一模式的批量修复器:
```python
def patch_pass_block(raw_bytecode: bytes, try_start: int, try_end: int) -> bytes:
"""将 try 块末尾的 except: pass 替换为 except: raise"""
# 在 except handler 末尾插入 RAISE_VARARGS 1
patch = bytes([130, 1]) # RAISE_VARARGS 1
return (
raw_bytecode[:try_end]
+ patch
+ raw_bytecode[try_end:]
)
```
「87% 可以用这个模式修复。剩下 13% 的嵌套情况和复杂异常链——」
「留到第 12 章?」程思语苦笑。
陈默没笑。他盯着屏幕,忽然觉得 `types.CodeType` 的构造函数签名像一座冰山——冰山下面是 Python 编译器二十年的演化史。他正站在冰山上,用一把螺丝刀修引擎。
「程思语。」
「嗯?」
「你爸为什么会这么做?」
程思语沉默了。机房的空调嗡鸣声填满了空隙。
「我六岁那年,他写了一行代码,让当时全亚洲最大的证券系统崩溃了十七分钟。」她终于开口,声音很平,「没人知道是他写的——他连夜改了提交记录,嫁祸给一个刚毕业的运维。」
陈默的呼吸停了半秒。
「那之后,他再也不相信任何人的代码。后来他造了神码 AI——不是因为他觉得 AI 写得好。是因为 AI 写的代码,出问题了可以怪 AI。他永远不会再被追责了。」
陈默看着程思语的侧脸。应急灯的光把她半个脸照亮,另一半沉在阴影里。
「但你不一样。」他说,「你选择追查。」
程思语没有回答。
她的手机亮了。一条消息。
她看了一眼,脸色变了。
「我爸的人查到了壳公司的物理地址。」
陈默肌肉绷紧:「还有多久?」
「最多两天。」
陈默转过椅子,面对屏幕。他把 `build_raise_wrapper` 的第一版跑了一个端到端测试——从一个包含 `except: pass` 的函数生成包装代码,然后执行,主动触发异常。
```python
# 测试
def has_bug(x):
try:
return 1 / x
except:
pass # 静默吞掉 ZeroDivisionError
# 用 RaiseGuard 修复
repaired = guard.repair(has_bug)
# 验证
try:
repaired(0)
print("没有异常——修复失败")
except ZeroDivisionError:
print("异常已传播——修复成功")
```
屏幕输出:
```
异常已传播——修复成功
```
一行,七个字。
陈默看着那行输出,程思语看着陈默。
「一个函数。」她说。
「对。」
「还有两万七千个。」
陈默把屏幕亮度调到最高。ThinkPad 的风扇已经在满负荷工作,散热孔吹出的热风带着电子元件的味道。
「那就一个一个来。」
他看了一眼手机上的倒计时:47 小时 36 分钟。
舱门外传来一声闷响。像是有人踩到了地面停车场的一块松动水泥板。
两个人的目光同时射向机房门。
门的缝隙里,没有光。什么都没有。
但那个声音——在废弃八年的科学岛地下二层——不可能是风声。
程思语的手指握住了 ThinkPad 的电源线。
陈默没有转身。他悄悄关了屏幕,把手放进键盘上方的黑暗中。
机房彻底安静了。
然后,脚步声。
从走廊尽头,开始一步一步,朝这边靠近。