逆向工程这件事,过去一直被认为是"脏活累活"——拿到一个编译后的二进制,靠人工梳理字符串、摸清调用关系、猜测数据结构,再一点一点还原逻辑。哪怕工具链已经很成熟,面对动辄几百万行代码规模的系统,依然像是用放大镜在拼一副巨型拼图。我们这次做了一个完全不同的尝试:把逆向和重构当成一个数据工程问题来处理,用AI模型在大规模代码语料上训练得到的语义理解能力,去辅助甚至主导整个逆向解读和代码重构过程。整个项目跑下来,覆盖了从语料准备、模型训练、目标分析到代码重构的完整闭环,数据规模达到了5000亿Token,最终重构产物与原始目标的字节匹配度做到了83%。这个数字放在实际工程里,已经足够让重构后的代码在行为上等价、在优化空间上可控,也让我们验证了一条值得记录的路径。
这篇文章适合正在做逆向工程、代码分析、AI辅助开发,或者在大规模代码数据上训练模型的人。我会把项目拆成几个核心环节讲清楚:数据怎么洗、模型怎么训、逆向分析怎么做、重构结果怎么量化,以及我们踩过的坑和最后总结出来的实操经验。
1. 项目背景与目标拆解
1.1 要解决的工程难题
先说背景。我们接手的是一个已经停止维护多年的历史系统,业务逻辑复杂,文档缺失,原始的源代码几乎等于不存在——只留下了编译后的可执行文件和一些零散的调试符号。传统方案是做反编译加人工梳理,我们初步估算下来,几个核心模块全部靠人力还原,至少需要两个人全职投入一年以上,而且出错率不低。
所以我们的思路是:能不能让AI先"读懂"这个二进制,再把它的行为和语义迁移到一份新工程里?这里的"读懂"不是简单地反汇编、反编译生成伪代码,而是要理解函数之间的调用关系、数据流、状态机迁移、异常处理等高层结构,然后生成一份干净、可读、可维护的源码。
这个目标很强硬,它要求我们同时解决两件事:一是AI模型要有足够强的代码语义理解能力,二是我们得有一套工程管线,能把模型输出自动转换成实际可编译、可运行的代码。于是我们设计了一个闭环:数据收集与清洗、模型训练与评估、目标二进制分析、重构代码生成、编译验证与差异比对,每个环节的结果都反馈给数据集和模型,形成迭代。
1.2 为什么字节匹配率是核心指标
衡量重构成功与否,大家第一反应是用功能测试。但功能测试覆盖有限,历史系统根本没有完整测试集。我们也试过用行为对比,但真实业务场景的状态空间太大,无法穷举。最终我们引入了一个非常严苛的客观指标:字节匹配率。
这里的字节匹配,指的是将重构后的源码用与原始构建环境尽可能一致的编译链和参数重新编译,生成新二进制,然后逐字节比对新旧二进制中可执行代码段的匹配程度。83%的字节匹配,意味着输出文件中有83%的字节与原始二进制完全一致。
这个指标有多难?只要编译器版本、优化级别、内联策略、寄存器分配顺序有任何不同,匹配率就会急剧下降。能做到83%,说明我们的重构代码在语法结构、数据布局、函数划分上都相当接近原始实现,这比单纯的功能等价要可信得多。
当然,字节匹配不是万能的。它无法保证语义完全一致,有些逻辑可能用不同方式实现但产生相同结果。所以我们在83%之外,还配了另一套行为验证机制,后面会细讲。
2. 数据工程:5000亿Token是怎么炼成的
2.1 语料来源与清洗链路
5000亿Token不是凭空捏造,也不会直接从网上爬下来就能用。我们构建的语料库包括三大部分:公开的高质量代码仓库、我们内部积累的历史系统类似代码、以及从目标二进制本身通过反编译产生的伪代码和上下文数据。
公开代码仓库作为基础,保证了模型对通用语法和编程范式的理解。内部积累的代码则提供了与目标系统风格相近的样本,尤其是老式C语言风格、特定硬件平台上的内联汇编写法、旧版标准库的调用方式。最特殊的是第三部分——目标二进制自身的反编译产物。我们先把目标文件拆成成千上万个函数片段,用反汇编器和反编译器生成控制流图和伪代码,再把它们与符号信息、字符串引用、地址关联信息一起组合成训练样本。
清洗环节的教训很多。第一步去除的是重复样板代码,比如自动生成的文件头、第三方库的完整拷贝,这些内容虽然量大但噪声高。第二步是规范化处理,把变量名统一成可预测的风格,把编译器生成的特定代码序列标注出来,避免模型学会的是工具链格式而不是代码逻辑。第三步是抽掉敏感和许可风险高的代码,确保训练语料本身安全合规。
2.2 Token化策略与数据配比
5000亿Token这个规模需要仔细考虑Token化。普通代码模型用的Token化器对空格、缩进、分号比较敏感,但我们在训练时额外加了几个特殊Token,专门编码地址偏移、寄存器名、调用约定等逆向工程专属信息。这样做的目的是让模型能同时理解"代码长什么样"和"代码在内存里怎么排布"。
数据配比上,我们针对不同来源设置了权重。通用代码语料占60%,内部风格语料占25%,目标二进制反编译语料占15%。其中反编译语料在每次模型迭代后都会自动更新——因为新模型生成的重构代码经过编译验证后,如果表现出更好的结果,我们就把这些结果作为正样本重新加入语料,形成闭环。
这个策略不是拍脑袋定的。我们做过对照实验:不加目标二进制语料时,模型生成的代码在结构上更像"想象中的旧代码";加入之后,重构代码在函数边界、栈布局上明显更贴近原始实现。15%这个比例是反复调出来的,低于10%效果衰减明显,高于20%则开始过拟合特定反编译器格式,影响泛化。
2.3 数据质量的经验教训
5000亿Token的数据集,在训练初期暴露了大量问题。一个典型问题是我们低估了编译器生成代码和手写代码之间的差异。很多开源语料是手写代码,风格自然,但目标二进制是编译产物经过反编译之后的形式,带有大量工具链特有的痕迹。用纯手写语料训练的模型,看到这种反编译伪代码时表现很差,经常丢掉一些"看起来冗余"但实际上用于对齐或状态加载的指令。
另一个教训是,仅仅清洗文本是不够的,必须做结构级清洗。我们最终实现了一套管道,用解析器把每一份代码转成AST,检查括号匹配、语法树完整性、函数数量,过滤掉解析失败或结构异常的文件。这一步大概砍掉了初始语料的17%,但剩余数据的有效性远超从前。
还有数据泄漏的问题。我们严格划分了训练集和验证集,确保目标系统相关的任何片段都不会出现在预训练阶段。这样评估出来的字节匹配率才可信。
3. 模型选型与训练方案
3.1 基座模型的选择逻辑
在决定从零预训练还是用开源模型继续预训练时,我们对比了很久。从零预训练需要至少数万张卡日,成本太高,周期太长。所以最终选择了一个在代码领域有较强基础能力的开源模型作为底座,继续在5000亿Token领域语料上做持续预训练,再做后续的指令微调。
选择底座的标准有三个:第一,它在代码生成、代码补全的任务上得分不差;第二,它的上下文窗口足够长,因为反编译出来的函数经常超过1024甚至2048个Token;第三,它的注意力实现能支持我们做序列级比对,而不是只能逐词预测。
我们没有选择参数规模最大的模型,而是选了一个性能与资源消耗更平衡的尺寸。因为整个项目里,模型推理要跑在批量流程里,吞吐量上不去的话,整个闭环的迭代速度会被卡死,后面想调参都没机会。
3.2 持续预训练与指令微调的策略
数据工程完成后,我们分成了两个阶段。第一阶段是持续预训练,用全量5000亿Token,把模型继续训练十几万步,学习率设得比正常预训练低一些,大概1e-4量级。这一步的主要目标是让模型在逆向工程风格的代码上降低困惑度。
第二阶段是指令微调。我们构造了一批任务模板,例如:"请分析下面这个函数的反编译伪代码,找出它的主要输入、输出和副作用"、"把这段汇编逻辑重写成等价的C语言"、"识别这段代码中可能的数据竞争"等等。每一条指令样本都要求有可验证的对应答案。指令微调阶段只用了约2亿Token,但效果提升非常明显,尤其是模型生成的代码在可读性和结构规整度上跳了一个台阶。
微调过程中我们要特别控制幻觉问题。模型有时会生成看似合理但实际不成立的函数调用,或者把寄存器赋值错误地理解成变量赋值。为了减少这种情况,我们在损失函数里对关键数据结构相关的Token做了加权,让模型在预测结构体偏移、字段名、调用约定这些关键信息时更谨慎。
3.3 评估与迭代
模型评估不能只看困惑度。我们在每个checkpoint下跑一个小批量目标函数重构,观察字节匹配率的变化。这个指标虽然计算成本高,但比任何语言模型层面的指标都更能代表任务真实效果。
我们还在整个管道里保留了人工审计环节。每周随机抽取50个重构完成的中等复杂度函数,由资深工程师检查代码逻辑和原始行为的对应关系,标注错误类型。模型迭代后,这些标注过的错误样本会进入下一轮的负样本训练,让模型逐步修掉高级语义上的错误类型。
这套评估迭代机制跑下来,我们发现一个有意思的现象:字节匹配率在前中期提升很快,到70%以上后增速放缓,后面每提升一个百分点,都需要在数据质量和约束生成上做大量细节工作。最终停在83%,不是不能往上加,而是每加一步的边际成本变得很高,我们选择把余力留给行为验证。
4. 逆向分析与重构的实现细节
4.1 从二进制到语义图
拿到目标二进制后,我们没有直接让模型"从零生成源码",因为那样自由度太高。我们做了一层中间表示,叫语义图。语义图把二进制中的每个地址、寄存器、栈偏移都抽象成逻辑节点,同时保留与原始地址的对应关系。
具体流程是:先用符号执行和类型分析工具,把目标文件的函数、基本块、调用关系提取出来。然后我们开发了一套脚本,把每个函数翻译成一张控制流图,再结合数据流分析,标记出循环、分支、跳转表、函数指针等结构。这张图就是模型输入的一部分。
语义图的核心价值在于约束范围。模型不需要凭空发明这三个分支是从哪来的,而要把语义图上已经存在的三个分支翻译成合法的if/else结构。这大幅降低了生成难度,也让生成结果与原始字节的对齐关系更容易保持。
4.2 重构代码的生成与约束
生成阶段,我们采用的是渐进式重构而不是一次生成整份文件。首先由模型对每个函数单独生成"语义描述",包括输入参数、返回值、静态变量、全局数据引用。接着,模型基于语义描述和原始伪代码,生成候选源码函数。最后再用匹配算法把生成的源码函数映射回二进制地址,确认字节对齐关系。
为了支持这种生成方式,模型输入不只是伪代码文本,还包括函数签名、调用者清单、被调用者清单、涉及到的外部符号表,以及一个特殊标记:"你需要保证生成代码的局部变量布局与以下栈偏移一致"。这种显式约束把模型的想象空间关进笼子里,生成的代码才不会跑偏。
我们试过用普通代码补全模型直接预测源码,结果看起来像那么回事,但编译出来字节匹配率只有20%上下,因为变量命名可以天马行空,但栈上的顺序完全不合原始安排。加入语义图约束后,匹配率才真正起飞。
4.3 83%字节匹配的度量细节
字节匹配率的计算方式需要明确,不然容易被误解。我们不是简单对整个文件做diff。我们忽略了两类字节:一是编译器生成的元信息区,比如调试段、注释段、符号表;二是绝对地址重定位的差异,因为链接顺序等原因导致的地址偏移变化。我们把目标限定在.text代码段和部分只读数据段。
在这个限定下,算法在两个二进制之间做滑动对齐,允许中间有少量插入和删除。只有当匹配的连续字节块长度超过32字节时,才计入有效匹配,这样可以避免随机巧合导致的虚高。最终83%是一个真实的、可复现的数字。
要做到83%,编译选项必须和原始构建基本一致。我们通过逆向分析还原了当时的编译器家族、优化级别、栈对齐选项,甚至模拟了内存对齐策略。如果你跳过这部分去盲目生成,可能连30%都到不了。
4.4 人工兜底与行为验证
字节匹配率再高,也不能排除某些逻辑上等价但文本上不同的情况。所有重构后的代码,都会进入一套行为验证管线:我们构造了高覆盖率的输入向量,跑原始二进制和新二进制,对比输出结果和状态差异。由于历史系统涉及大量外部交互,我们还做了模拟沙箱,把所有外部依赖mock掉,保证行为可比性。
当行为验证通过后,人工工程师会重点检查高复杂度函数、涉及全局状态的函数和异步逻辑。这部分工作量依然不小,但比全量重写少了几个数量级。最后整套代码会进入代码评审和演进流程。这里我想强调,AI逆向重构的价值不是替代工程师,而是把工程师从70%的重复劳动里解放出来,让他们聚焦真正的逻辑校验和架构优化。
5. 工程闭环与常见问题排查
5.1 闭环管线怎么搭
我们最终跑通了一条自动化的流水线,从目标二进制进入,到重构源码和验证报告出来,中间所有步骤都可以暂停、续跑、回滚。整个管线用脚本组织,数据流大概是:
- 二进制解析:生成符号表、调用图、段信息
- 反编译与语义图构建:生成伪代码和结构化约束
- 模型批量推理:生成候选源码
- 编译与对齐:用还原的编译环境编译新源码,与原始二进制比对
- 行为验证:跑测试集和沙箱
- 结果回写:把验证通过的重构代码和对应的语义图加入语料库
这个闭环的威力在于,每一次模型更新都能立刻用全量目标系统测试一遍,每通过一个模块,模型就多了一批高质量样本,下一次重构下一个模块时表现就更稳。我们实际跑下来,从第一轮匹配率不足50%,到最终83%,中间经过七个大版本迭代,每轮耗时从最初的两周缩短到后来的三天。
5.2 常见问题速查表
| 问题表现 | 可能原因 | 解决思路 |
|---|---|---|
| 生成代码大量命名模糊 | 模型听不懂符号信息 | 在输入中加入符号表和变量名映射 |
| 字节匹配率骤降 | 编译选项没对齐原始环境 | 用退火搜索逼近编译器参数 |
| 函数指针关系丢失 | 语义图只分析了静态调用 | 补充间接调用目标分析,用运行迹辅助 |
| 生成代码存在语法错误 | 约束条件与语法树冲突 | 在生成后加一层模板校验和AST修复 |
| 重构后运行崩溃 | 全局数据初始化顺序被改变 | 对比二进制中的初始化段和构造函数调用顺序 |
| 训练loss下降但匹配率不升 | 模型在拟合语料本身而非任务目标 | 调整微调样本比重,增加任务约束的信号 |
这个表里的每个问题都是我们实际遇到过的。最让人头疼的是"loss下降但匹配率不升",这意味着模型的语言能力在进步,但对逆向任务的关键模式没有敏感起来。后来我们把损失函数里的目标关键Token权重拉高,同时给模型输入加上了"结果导向"的提示词,告诉它当前需要重点保持栈布局一致,问题才缓解。
5.3 独门避坑技巧
有几件小事很有价值,简单分享一下。
第一,语料中保留一部分未完全反编译成功的"残片"。这些残片模型生成结果经常是错的,但它们的存在反而能让模型学会识别"工具链输出的边界",减少生成时过度发挥。
第二,字节匹配率的计算不要只看静态对齐。我们把两个二进制都丢进相同输入集执行,记录执行路径上的指令地址序列,再做序列匹配。这种做法能捕捉到大量静态对比看不出的等价结构,最终贡献了约5个百分点的匹配率提升。
第三,重构过程中保留一张"地址映射表"非常关键。每一步生成都记录源码行号对应的原始地址列表,这样一旦字节匹配失败,可以立刻定位到具体是哪个函数、哪条语句出了问题,不用从头排查。
这些技巧说穿了都是经验,不去跑一遍完整闭环根本意识不到它们的重要性。
5.4 最后的个人感受
从5000亿Token到83%字节匹配,这个数字背后是整个工程团队在数据、模型、工具链、验证机制上反复打磨的结果。我个人最大的体会是:AI逆向工程不是一个模型单打独斗就能成功的奇迹,而是一个系统工程。模型聪明与否只是第一步,数据的相关性、约束的设计、度量方式的选择、反馈闭环的构建,每一个环节都决定了最后的数字能站多高。
如果你也想做类似的事,建议你先不要急着追求庞大的数据规模,而是先把一条最小闭环跑通——拿一个小模块,从二进制到重构代码再到字节比对,把每一个环节的成本和瓶颈摸清楚。然后,再一点点扩张数据量级和代码规模。5000亿Token不是起点,而是这个循环转起来之后自然积累的结果。
关于这个方向,后续其实还有很大空间,比如把字节匹配率用于更细粒度的代码搜索、把语义图直接作为跨语言迁移的中间表示、把人工审计的标注反馈自动转化为奖励模型信号。这些都值得继续深挖,但那是下一个项目的事了。这次,先让83%这个数字自己说话。