先讲一个我们去年真实撞上的场景:线上某个模块开始间歇性报错,日志把问题锁定在一个二进制库里,但这个库没有源码。前任维护团队解散时,源码跟着那台早已报废的开发机一起消失了,文档里只留下一句“此模块结构复杂,勿轻易改动”。改,怕改坏;不改,故障持续吞吞吐吐。我们最终选择了一条听起来有点大费周章、但后来被证明是唯一能长期解决问题的路:用AI把二进制逆向翻译成可维护的C源码,再借助编译回验和字节比对,把AI的输出变成可信任的工程资产。
整条流水线跑下来,我们攒了两个最值得说的数字:5000亿Token的逆向配对语料,和83%的字节级匹配率。前者是我们敢让AI干这件事的底气来源,后者是整个闭环的质量底线。这篇文章就把这两个数字背后的工程细节摊开讲。如果你手头也有老二进制、丢源码、或者正在评估AI辅助逆向重构的路子,下文应该能给你一套可以直接参照的思考框架。
1. 没有源码的老二进制,为什么必须被“翻译”回来
1.1 一个几乎每个技术团队都会撞上的维护噩梦
我们的处境并不特殊,很多团队都遇到过类似情况:系统还在线上跑,但源代码已经没了。可能是版本管理混乱,可能是离职交接断档,也可能干脆就是当年外包交付时只给了编译产物。这种时候,技术债会集中爆发成几个具体问题——出故障只能靠黑盒观察和设备日志硬猜,想改逻辑无从下手,想迁移架构更是连底层的边界都摸不清。
最开始有人提出“干脆推倒重写”。这听起来痛快,实际上风险极大:老系统里积累的是多年运营沉淀下来的边界情况和业务规则,黑盒状态下根本测不全。某个异常分支可能一年只触发两次,但每次都造成过线上事故,重写团队不可能知道。所以我们很快排除了重写,把目标锁定为“恢复源码”。只有拿到可读、可编译、可修改的源码,这个模块才重新变得可维护。
1.2 为什么传统反编译器给不了“可维护源码”
传统反编译器的定位是“把汇编变成可读结构”,它并不负责“把结构变成可维护的源码”。这两者之间有巨大的差距。反编译器的伪代码保存了底层的全部细节,但变量名全是 var_1、var_2 这种,控制流被编译器折腾得面目全非,if、else、循环的嵌套关系经常要靠人肉脑补。更麻烦的是,反编译器输出的伪代码基本不具备“重新编译”的能力,你拿到手的是一个只能看、不能跑的中间产物。
AI辅助的路径不一样。大模型能学习函数的意图,能恢复有意义的变量名,能识别常见的算法模式,甚至能理解某个函数在整个调用关系里的位置。它输出的C代码虽然也需要验证,但至少是可读的、接近正常人写的、可以直接进入代码评审流程的。这一点,传统反编译器给不了。
1.3 三条路线对比:为什么我们选了AI闭环
当时内部认真评估过三条路:纯人肉逆向、传统反编译器二次整理、AI辅助逆向重构闭环。对一个中等规模模块(几百个函数)的预估工作量如下:
| 路线 | 预估人力成本 | 输出可维护性 | 可信度验证手段 |
|---|---|---|---|
| 纯人肉逆向 | 两到三个月 | 中,依赖个人水平 | 代码评审 |
| 传统反编译器整理 | 一个月以上 | 低,伪代码不可编译 | 无 |
| AI辅助逆向重构闭环 | 两到三周 | 高,接近原生源码 | 编译回验 + 字节比对 |
我们选第三条路的核心逻辑是:AI负责把人力从“翻译汇编”里解放出来,机器校验负责给AI踩刹车,人工只处理那些真正需要判断力的问题。这个分工,决定了我们整个系统后来的架构走向。
2. 5000亿Token语料:配对数据怎么造,模型怎么喂
2.1 配对语料:从开源C/C++项目到(汇编,源码)二元组
通用代码模型很多,但能“看汇编、写C”的模型并不存在现成的,必须自己造。造数据的基本思路很直白:找大量开源C/C++项目,统一编译成多个配置(不同编译器、不同优化级别的排列组合),再把编译产物反汇编、按函数切出来,得到同一函数的“汇编表示”和“源码表示”成对出现。
这个过程的工程量远大于想象。我们积累到约5000亿Token的配对语料,其中大量时间花在过滤上。有几个过滤策略特别关键:只保留能够唯一对齐到源码的函数,编译期被完全内联的函数直接丢弃;用库函数签名做去重,防止标准库重复语料把模型带偏;按函数大小分层采样,避免小函数过多导致模型变成只会翻译短片段。这些规则每一条都在事后被证明是有价值的——不加这些过滤,模型在某些常见库函数上背得滚瓜烂熟,遇到真实业务函数却表现平平。
2.2 两阶段微调:先建立映射直觉,再学指令输出
模型训练上我们没有走从头预训练的路子,而是在一个适合代码任务的基座模型上做两阶段微调。第一阶段用配对语料做继续预训练,目标是让模型建立“汇编—源码”的映射直觉,这个阶段不要求输出格式,只要求隐空间里两种表示能对齐。第二阶段把它整理成指令格式,输入是一段反汇编和函数上下文信息,输出是重写后的C函数,并用人工偏好排序来学习“哪些输出更接近可编译状态”——可编译、可对字节,比“读起来通顺”优先级高得多。
实测下来,把数据喂熟比换更大的基座有效得多。同样参数量下,清洗干净的数据带来的匹配率提升,远大于盲目升级模型尺寸。5000亿Token这个规模的另一个意义是,它足够让模型稳定区分不同编译器的代码生成风格——GCC和Clang在指令选择、栈布局上都有明显差异,模型见过足够多样本后,这种差异会内化成一种“风格感知”。
2.3 训得起还要推得起:缓存与去重的成本账
很多人只盯着训练成本,其实推理成本往往更高。我们的流水线里,每个函数要做多次生成——候选生成、编译失败重试、低置信度二次生成,都是实打实的模型调用。为了不让账单失控,我们做了两层优化。
第一层是缓存:同一个二进制的同一段汇编,哈希命中后直接复用结果,不做重复劳动。第二层是相似度去重:不同优化级别下,同一个源码函数生成的汇编往往高度相似,我们用嵌入向量做相似度检索,特征相近的函数直接引用上一次的生成结果,只做局部调整。这两层优化把单个模块的分析成本压到了可接受范围。如果一开始就忽略推理成本设计,后面跑十万级函数时大概率会卡在预算上。
3. 逆向重构闭环主链路:切函数、带上下文改写、编译回验
3.1 函数边界切分:错误边界的代价是全部下游失真
整个闭环的第一步是从二进制里切出函数。这一步听起来基础,实际上是最容易翻车的环节。二进制的函数边界并不天然清晰:编译器会做基本块重排,开启某些优化后还会做函数合并;如果符号表被去掉,函数边界只能靠反汇编器的控制流分析推断。
我们的做法是用可用的符号信息做锚点,辅助反汇编器的控制流分析来推断边界。但光推断不够,还加了一道回退校验:每个切出来的函数,只有重编译后能和原目标对齐,才认定边界是合理的。函数边界错了,后面AI翻译的输入就是一段“拼接错误”的汇编,模型再强也还原不出正确答案。这道校验是整个流水线能闭环的基石。
3.2 给AI的“半成品”:上下文分析远比裸汇编有效
直接丢一段裸汇编给模型,结果通常很差。我们做了大量实验后确认,模型非常依赖上下文信息。现在流水线里喂给模型的,是一份经过机器预处理的“半成品”,包含三块内容:
- 函数用到的全局变量符号、偏移量和疑似类型;
- 被调用函数的地址和已知签名;
- 控制流图的基本块拓扑结构,而不是一行行扁平文本。
这三块连同汇编文本一起拼成上下文。AI看到的不再是“一段汇编在做什么”,而是“这段汇编旁边的数据是什么、它会调用谁、控制流怎么走”。语义恢复精度因此提升了不止一个档次。
生成策略上,我们不用单次best-of-1,而是生成多个候选(温度设得比较低,保留一定多样性),再用一个快速排序模型按“可编译性”打分,目标是先把能通过的候选捞出来,而不是追求一次到位。
3.3 结构感知的字节比对:逐字节相等不是唯一标准
AI生成的C代码,只有编译通过且字节对得上,才算过了第一关。我们使用与原始二进制一致的编译器版本和优化级别做重编译,然后把目标函数的原始字节段和重编译结果做规范化对齐。
字节比对不是简单逐字节相等。合法差异必须容忍:地址重定位导致的绝对地址不同、栈变量顺序调整、指令调度差异,这些都可能是“同一段逻辑的不同合法编码”。我们的比对比的是“结构”——先对齐指令边界,再逐条比对指令字节和操作数,只有真正结构性不同的地方才标出来。
这一步的运行日志大致长这样:
FUNC 0x00401234 [restore_config] status=MATCH bytes=342/342 FUNC 0x00401560 [parse_header] status=PARTIAL bytes=210/256 diff=memcmp_len FUNC 0x00401888 [crc_update] status=MISMATCH bytes=64/198有了这个物理校验,AI生成结果的可信度才真正有了底线。我们内部常说一句话:AI写的是“感觉对”,编译器验证的是“真的对”,字节比对才是“对到比特级”。
4. 83%字节匹配的口径、变量和它的真实含金量
4.1 两个口径的83%:还原率与一致率必须分开
对外汇报时被问得最多的问题就是:83%到底是谁的83%?这个口径其实分两层。第一层是“能成功还原的函数比例”,也就是整个二进制里有多少函数被我们成功切分并生成出了可读代码。第二层是“重编译后字节完全一致的比例”。我们对外说的83%,指的是在测试二进制集合中,所有被成功切分出的函数里,重编译后与原函数在规范化比对下达到字节级一致的比例。
这个口径是内部刻意收紧过的。我们试过更宽松的口径:只要求控制流等价、只要求函数签名字节一致,那些口径都能刷到95%以上。但对“可维护源码”这个目标来说,宽松口径没有意义——你拿到一个控制流等价但变量类型全错的代码,照样不敢上线。字节级一致是最硬的标尺,宁可让它限制我们,也不能用一个看着漂亮但说明不了问题的指标。
4.2 哪些变量在偷偷影响匹配率
跑了几轮大规模实验后,我们总结出影响匹配率的三个主要变量,按影响大小排序:编译配置一致性、函数复杂度、上下文完整性。
| 变量 | 影响方向 | 实际案例 |
|---|---|---|
| 编译配置一致性 | 影响最大 | 基准二进制用了补丁版编译器,回验用标准版,匹配率从83%掉到40%级 |
| 函数复杂度 | 函数越大越吃亏 | 几千字节的大函数,任何一处还原偏差都会导致整体比对失败 |
| 上下文完整性 | 状态依赖越强越难 | 依赖全局状态的函数,容易在全局变量类型上出错 |
最典型的事故是编译环境漂移。我们曾发现一整批函数的匹配率断崖式下跌,排查到最后,问题出在基准二进制是用某个补丁版本编译器构建的,而回验流程用的是标准版本,字节差异集中在指令选择上。
4.3 字节一致是强信号,但不是全部真相
要诚实地说,字节级一致是一个“强充分条件”的近似:一个函数重编译后字节级一致,它大概率被正确还原了。但反过来,没有达到字节一致,不代表逻辑一定错了。判定是“错”还是“只是编码不同”,需要结合比对差异区的具体内容,这也是为什么我们不能把所有不一致都自动判为失败。
另一层不能推导的是整个模块可维护性。83%还原了函数,剩下17%里如果恰好包含核心状态机,这个模块照样不能直接接手。我们内部从来不说“还原到83%系统就安全了”,而是说“83%的确定性让团队的维护信心从0变成及格”。剩下的工作,靠人工复核和动态测试补齐。
5. 十万级函数规模下,工程闭环的可运行细节
5.1 从单函数到十万函数:任务队列与优先级
单函数层面跑通逻辑以后,真正的挑战是批量化。我们的测试集合里有一批二进制的函数数量达到十万级,不可能逐个交互式调用模型,必须把流程做成流水线。
反汇编和函数切分并行处理,按二进制哈希建立任务队列;AI生成调用按权重排队,优先处理外部可见的导出函数——它们最容易被调用关系验证;编译回验同样并行,失败任务自动短暂重试(换随机种子重新生成),超过两次进入人工队列。这套调度里最关键的认知是“失败一定要有去处”。如果所有失败函数都堆给人工,人很快会被淹没。
5.2 置信度分级:不要用人工淹没AI
我们按置信度把产物分成三个层级:高置信产物直接入库;中等置信给一个快速抽检;低置信才进入逐个人工复核。高置信的标准不只是字节一致,还包括函数复杂度在中等以下、上下文完整、调用关系清晰。中等置信通常是“字节一致但函数偏大”或“部分字节差异但差异区间可解释”的情况,这类产物做抽检就能压住风险。
人工复核工具的界面设计直接决定效率。如果给复核员的是“一段AI代码加一段汇编”,人根本看不过来。我们做的是三栏并排视图:左边是原始反汇编控制流图,中间是AI生成的C代码,右侧实时显示“如果把这个C代码重编译,和原函数差在哪几个字节”。复核员可以不看汇编,只看差异指令区域,快速选择“采纳、修改、退回重生成”。没有这个视图之前,复核一个函数平均要一刻钟;有了它之后,大部分函数两三分钟就能过。
5.3 闭环的第二层含金量:每次人工修正都在回流模型
工程闭环还有一个容易被忽略的维度——数据回流。我们给每个函数建立完整留痕记录:原始切分边界、汇编哈希、AI候选列表、编译结果、比对差异、人工操作记录。这些记录不只是追溯用的,更是模型迭代的原料。每过一轮,我们用人工修正过的结果当新样本,定期做增量微调。
这就是“闭环”的第二层含义:工程流程上是“反汇编—生成—编译—比对—复核”的循环,数据层面上是“人工修正—增量训练—能力提升”的循环。两个循环叠起来,系统会越跑越顺。后期我们发现,同一个二进制家族的模块,第二次进来时的低置信比例明显比第一次低,数据回流是主要原因。
6. 复盘与下一步:那些差点让闭环断掉的坑
6.1 三个拖慢我们最久的坑
第一个坑是编译环境漂移。前面提过,基准二进制用的编译器和我们回验用的编译器版本只要差一个patch,匹配率就会断崖式下跌。后来我们把基准编译器直接容器化锁定,和二进制一起作为不可变资产归档,才彻底解决这个问题。
第二个坑是“部分一致”的错觉。早期我们允许一个函数如果90%字节一致就算通过,结果发现这种“大部分一致”的函数往往藏着结构性逻辑错误,反而比完全不一致更有迷惑性。最后我们改成硬性门槛:要么字节级一致,要么进人工复核,不做中间地带。
第三个坑是忽略数据段和字符串。函数字节一致不代表整个模块可用——如果AI把全局数组的长度或字符串常量改错,即便函数本身还原了,运行时也会出错。后期我们把数据段和只读常量也纳入比对范围,这一步让整体可靠度又上了一个台阶。
6.2 从83%往上走:性价比最低但最有价值的游戏
如果给同行一个真实预期,我会说:从0到83%是半年内的成就感;从83%到90%可能还要半年;从90%再往上,就不是算法问题,而是要把剩余17%里的每个顽固分子单独拿出来研究。每个顽固函数各有各的原因:有的因为编译器优化太激进,有的包含内置指令,有的混杂了手写汇编。这已经不是“模型行不行”的问题,而是“工程能不能一例一策”的问题。
我们接下来的方向有三条线并行。一是改进编译配置自动推断,让回验流程对配置差异更鲁棒;二是引入动态行为测试,在字节比对之外增加“运行结果对比”作为第二重验证,专门盯那些字节不一致但逻辑可能等价的情况;三是继续用人工修正样本回流模型,目标是把前20%的低置信函数慢慢变成高置信。
6.3 关于这套闭环,我最想对同行说的一段话
最后说点个人体会。做这套逆向重构闭环,最大的收获其实不是那个83%的数字,而是我们建立了一个健康的流程。流程里的每个环节都不完美:AI会瞎编,切分会有误差,字节比对会漏掉语义,人工复核会疲劳。但这些不完美的环节组合起来,互相补位,就能让一个团队放心地长期维护原本根本不敢碰的老系统。
如果你也要做类似的事,我的建议是:别急着上大模型,先把“怎么验证AI输出”这个问题想透。验证机制的设计,直接决定了你的天花板——5000亿Token给了AI起飞的能力,但真正让它落地的,是那条能接住它的闭环跑道。