【论文阅读】Agentic Concolic Execution: 让 LLM 接管 Concolic Execution 的建模与求解
- 1【论文阅读】Dytan: A Generic Dynamic Taint Analysis Framework
- 2【论文阅读】All You Ever Wanted to Know About Dynamic Taint Analysis and Forward Symbolic Execution
- 3【论文阅读】Augur: Dynamic Taint Analysis for Asynchronous JavaScript
- 4【论文阅读】VIPER-MCP: Detecting and Exploiting Vulnerabilities in Model Context Protocol Servers
- 5【论文阅读】Exploring Static Taint Analysis in LLMs: A Dynamic Benchmarking Framework for Measurement and Enhancement
- 6【论文阅读】Locus: Agentic Predicate Synthesis for Directed Fuzzing
- 7【论文阅读】Make Agent Defeat Agent: Automatic Detection of Taint-Style Vulnerabilities in LLM-based Agents
- 8【论文阅读】Sleuth: A Switchable Dual-Mode Fuzzer to Investigate Bug Impacts Following a Single PoC
- 9【论文阅读】Gordian: Defusing Logic Bombs in Symbolic Execution with LLM-Generated Ghost Code
- 10【论文阅读】Agentic Concolic Execution: 让 LLM 接管 Concolic Execution 的建模与求解本文
先说结论:这篇论文解决的到底是什么问题?
传统具体-符号执行(Concolic Execution)有一个很现实的瓶颈:程序越复杂,难维护的重点就越从“如何运行程序”转向“如何把程序行为转换成符号约束,以及如何求解这些约束”。
现代软件里经常同时出现:
- C/C++ 中的指针、
memcpy、类型转换和复杂数据结构; - 文件、网络、命令行参数、环境变量等环境交互;
- 浮点数、字符串、数组以及位级操作;
- Python、Java、C、Go 混合的多语言系统。
传统动态符号执行(Dynamic Symbolic Execution,DSE)工具通常需要为这些语义维护对应的符号模型。支持的语言、库函数和运行时环境越多,模型就越复杂;模型覆盖不足时,又可能不得不近似处理,甚至直接放弃某些路径。
即使符号模型已经建立起来,还存在第二个问题:
路径约束往往过于低级、冗长,而且未必适合直接交给 SMT 求解器处理。
本论文提出的核心思路很直接:让大语言模型(Large Language Model,LLM)智能体参与符号化建模和约束求解,但保留具体执行、代码执行和 SMT 求解器这些精确工具。
论文将这一范式称为智能体驱动的具体-符号执行(Agentic Concolic Execution),具体实现为 CONCOLLMIC。
整个系统可以概括成下面这条链路:
源码 ↓大语言模型生成插桩 ↓具体执行 ↓执行轨迹 ↓执行抽象 ↓路径约束总结智能体 ↓高层语义约束 ↓约束求解智能体 ├── Python ├── Z3 └── 测试驱动程序 ↓具体测试输入 / 运行环境 ↓重新执行并验证其中最值得关注的有四点:
- 插桩也交给大语言模型完成,让不同语言最终产生统一格式的执行信息;
- 先进行具体执行,再将执行轨迹压缩成面向大语言模型的执行抽象;
- 路径约束总结智能体负责选择需要探索的分支,并把低层实现转换成高层语义约束;
- 约束求解智能体可以根据问题类型调用 Python、Z3 或其他工具,并通过重新执行验证结果。
因此,本论文的重点在于让大语言模型承担语义理解、约束抽象和工具编排,同时保留传统工具在精确计算方面的优势。
论文在 12 个真实程序/系统上进行了实验,报告了以下结果:
- 相比 KLEE、KLEE-Pending、SymCC、SymSan,平均分支覆盖率提高 115%–233%;
- 相比 AFL++,平均覆盖率提高 81%;
- 发现 11 个此前未知的漏洞;
- 其中 6 个漏洞没有被现有比较工具发现;
libsoup中的一个问题后来被分配为 CVE-2025-4945。
代价同样明显:CONCOLLMIC 的运行速度和成本都高于传统工具,并且依赖大语言模型。论文更接近于证明“这种架构确实可行”,距离成为低成本、稳定的通用测试基础设施还有一段距离。
1. 先把具体-符号执行讲明白
普通模糊测试在做什么?
假设程序非常简单:
int foo(int x) { if (x == 1234567) { trigger_bug(); } return 0;}覆盖率引导模糊测试(Coverage-guided Fuzzing)会不断修改已有输入:
142100...希望某次变异恰好得到:
1234567如果条件藏在很深的程序路径中,仅靠随机变异往往很难命中。
符号执行怎么处理?
符号执行(Symbolic Execution)不会一开始就把 x 固定成具体数字,而是把它视为符号变量:
x = α执行到:
if (x == 1234567)之后,可以得到路径条件:
α == 1234567如果目标是进入 true 分支,求解器只需要找到:
α = 1234567然后就可以生成对应的具体输入。
具体-符号执行进一步把两者结合起来
具体-符号执行可以理解成“具体执行 + 符号执行”。
一次典型流程是:
初始输入 I0 │ ▼具体执行 │ ├── 程序输出 O └── 执行轨迹 T │ ▼ 符号化 │ ├── 路径约束 PC └── 符号状态 σ │ ▼ 翻转约束 │ ▼ 求解 │ ▼ 新输入 I' │ └──────────────┐ ▼ 工作队列论文中的算法可以概括成:
1 P~ ← INSTRUMENT(P)2 WorkList ← {I0}
3 while WorkList != ∅:4 I ← SELECT_NEXT(WorkList)5 (O, T) ← EXECUTE(P~, I)6 (PC, σ) ← SYMBOLIZE(T)7 PC' ← FLIP_CONSTRAINT(PC)8 I' ← SOLVE(PC', σ)9 WorkList ← WorkList ∪ {I'}真正关键的是第 6~8 步:
具体执行轨迹 ↓符号约束 ↓翻转目标条件 ↓求解新的输入而这三步恰好是传统工具最容易遇到复杂度问题的地方。
2. 为什么传统方案开始变得困难?
论文把问题概括成两个核心挑战,可以记成 C1 和 C2。
| 问题 | 传统动态符号执行的痛点 | CONCOLLMIC 的思路 |
|---|---|---|
| C1:符号化建模复杂 | 不同语言、库函数和环境交互都需要对应的符号模型 | 从源码和具体执行轨迹中恢复程序语义 |
| C2:约束求解困难 | 约束低级、冗长,涉及浮点、字符串、数组等复杂理论 | 在更高语义层级表达约束,再按需调用 Python、Z3 等工具 |
这两个问题会互相叠加。
C1:首先得理解程序究竟在做什么
例如:
float start = atof(argv[1]);
unsigned temp;memcpy(&temp, &cur, sizeof(float));temp++;memcpy(&cur, &temp, sizeof(float));这里如果只从源码表面理解,很容易把它看成:
cur = cur + 1但实际执行的是位级操作,并非普通浮点加法。
代码执行的是:
float │ ▼通过 memcpy 读取原始位 │ ▼unsigned + 1 │ ▼重新解释为 float这里涉及 IEEE-754 浮点数的位表示。单精度浮点数使用固定的 32 位表示:
┌─────────┬───────────┬───────────────────────┐│ Sign │ Exponent │ Fraction ││ 1 bit │ 8 bits │ 23 bits │└─────────┴───────────┴───────────────────────┘因此,传统符号执行器需要同时理解:
memcpy的内存语义;float与unsigned之间的类型转换;- IEEE-754 的浮点表示;
- 位向量运算;
atof的行为。
像 atof、memcpy 这样的函数也需要进入符号模型。
传统工具通常有几种处理方式:
- 将函数实现纳入符号执行;
- 为函数手写符号模型;
- 将部分参数固定为具体值。
第一种方式会显著增加实现规模,第二种需要长期维护大量模型,第三种则可能丢失部分路径。
C2:模型建立之后,求解器还要面对低层约束
现代程序的路径约束可能同时包含:
大量变量+大量条件+位向量+数组+指针+浮点数+字符串+外部函数传统执行器往往会把程序实现机械地转换成低层公式。
于是源码只有几十行:
for (...) { memcpy(...); temp++; memcpy(...);}最后形成的约束却可能远比源码复杂。
论文抓住的关键点是:
程序实现层面的复杂度,并不一定等于程序语义层面的复杂度。
对于前面的浮点循环,从程序语义上看,真正需要回答的问题其实非常简单:
start 和 end 之间有多少个可表示的单精度浮点数?如果能够直接把低层实现总结成这个语义问题,后面的求解就可能简单很多。
3. 一个很能说明问题的浮点例子
论文选用了 FP-Bench 中的一个例子。

图 1(论文第 3 页):代码通过
memcpy直接操作 IEEE-754 位表示,因此一个看似简单的浮点循环,最终可能对应非常复杂的低层符号约束。
核心代码可以简化为:
#define FLOATS_BETWEEN_BUG 20
int cnt = 0;
int count(float start, float end) { for (float cur = start; cur != end; cnt++) { unsigned temp;
memcpy(&temp, &cur, sizeof(float)); temp++; memcpy(&cur, &temp, sizeof(float)); }
if (cnt <= FLOATS_BETWEEN_BUG) { printf("BUG triggered!"); return -1; }}
int main(int argc, char **argv) { float start = atof(argv[1]); float end = atof(argv[2]);
__assume__(start < end);
count(start, end);}这段代码真正表达的逻辑是:
如果
start到end之间需要经过的可表示单精度浮点数不超过 20 个,就进入漏洞分支。
这里的 +1 到底是什么意思?
代码每次执行:
当前浮点数 ↓读取 IEEE-754 原始位 ↓原始位 + 1 ↓重新解释成浮点数因此它实际上是在访问相邻的 IEEE-754 单精度可表示值。
这和普通的:
cur += 1;完全是两回事。
初始输入为什么没有触发漏洞?
论文给出的初始输入是:
start = 1.00end = 1.00001在 1.0 附近,相邻单精度浮点数的间隔约为:
而:
因此大约可以跨越:
个间隔。
所以 [1.0, 1.00001) 中存在 84 个可表示的单精度浮点数,程序最终得到:
cnt = 84于是:
if (cnt <= 20)条件不成立,漏洞分支不会执行。
传统具体-符号执行会得到什么?
设:
start = αend = β定义:
f(x)表示“读取 x 的 IEEE-754 位表示,加 1 后再解释为浮点数”。
经过 i 次循环:
对应的符号状态可以写成:
假设程序需要执行 n 次循环才能让 cur 等于 end,路径约束可以表示为:
如果希望探索漏洞分支,就需要把最后一个条件翻转为:
单独看最后这个条件很简单,真正困难的是:
因为 f 的实现中包含 memcpy、内存操作、类型转换、IEEE-754 语义、atof 以及循环执行。
于是,一个从语义上很简单的问题,经过传统符号化之后,可能变成大量低层约束。
这正是 C1 和 C2 叠加产生的效果:
- C1:这些底层操作如何建立完整的符号模型?
- C2:建立之后,这么复杂的约束如何高效求解?
论文由此引出 CONCOLLMIC。
4. CONCOLLMIC:把符号化和求解变成智能体工作流
传统流程可以概括成:
具体执行 ↓低层符号化 ↓大型 SMT 公式 ↓SMT 求解器CONCOLLMIC 则变成:
具体执行 ↓执行抽象 ↓路径约束总结智能体 ↓高层语义约束 ↓约束求解智能体 ↙ ↓ ↘Python Z3 代码推理 ↓具体测试输入 ↓具体执行验证这里有两个核心变化。
变化一:符号化从“执行过程中同步完成”变成“执行之后再完成”
传统具体-符号执行通常会在程序执行过程中不断构造符号表达式:
执行一条语句 ↓构造对应符号表达式 ↓更新路径约束 ↓继续执行CONCOLLMIC 的思路是:
先完成具体执行 ↓得到完整执行轨迹 ↓生成执行抽象 ↓再让大语言模型理解这次执行这样,语言和运行时相关的细节主要集中在前面的插桩阶段,后面的语义推理则建立在统一的执行信息之上。
变化二:约束不再限定为 SMT 公式
论文允许约束处在不同的抽象层级,例如:
- 自然语言;
- 编程语言;
- SMT 公式。
因此,对于一个问题,大语言模型可以判断:
这个约束用自然语言描述已经足够清楚也可以判断:
这个问题结构比较规整,直接交给 Z3 更合适这也是论文所谓“任意抽象层级”的核心含义。
5. 第一阶段:先让不同语言产生统一的执行信息
这一阶段主要解决 C1。


图 2(论文第 4 页):整个系统可以分成两个阶段。第一阶段负责把源码转化为统一的执行轨迹;第二阶段在执行轨迹之上完成路径探索。
上图是 AI 翻译,顺带记录一下这个风格的学术插图提示词:
Academic research paper figure style, suitable for IEEE/ACM computer science papers.
Clean, precise, publication-quality technical schematic.Minimalist academic visual language, vector-graphics appearance,white background, black and dark-gray outlines, restrained use of light bluefor highlighting important modules.
Use thin and consistent strokes, clean geometric shapes,rounded rectangles, document-shaped boxes, database cylinders,straight directional arrows, dashed boundary boxes,and clearly numbered steps.
Professional typesetting similar to LaTeX-rendered academic papers.Serif or clean academic fonts, mathematically formatted symbols,consistent font sizes, balanced spacing and alignment.
The diagram should look like a figure from a software engineering,program analysis, systems, or security research paper.
Prioritize information density, structural clarity, and technical precision.No decorative illustrations, no gradients, no glossy UI elements,no 3D rendering, no realistic objects, no icons unless technically meaningful,no excessive colors, no marketing infographic style.
Flat 2D vector schematic, monochrome-first academic design,subtle light-gray module fills, restrained light-blue highlighted region,high contrast, crisp edges, publication-ready.传统动态符号执行的一个长期问题是:每增加一种语言、一个运行时或者一种特殊环境交互,就可能需要增加对应的符号模型。
CONCOLLMIC 的做法是先把问题拆开:
第一步只要求程序能够输出统一格式的执行日志,暂时不要求建立完整的符号模型。
源码块与控制流追踪
论文把源码划分成一个个源码块(Source Block)。
例如:
for (...) { ...}可以被插入:
enter count 1
for (...) { ...}
// exit count 1一般形式为:
enter func_name block_nooriginal_code// exit func_name block_no每个源码块由:
(func_name, block_no)唯一标识。
再通过:
<file_path, block_id>映射到具体源码行。
例如论文图 1 中的映射关系:
| 源码块 ID | 源码行 |
|---|---|
<src/count.c, (main, 1)> | 24-26 |
<src/count.c, (count, 1)> | 7-10 |
<src/count.c, (count, 2)> | 16-17 |
程序执行后,就可以得到类似这样的轨迹:
[src/count.c] enter main 1[src/count.c] enter count 1[src/count.c] enter count 1...这样,不同语言的程序最终都可以被描述成统一的“执行过哪些源码块”。
为什么不直接依赖 AST 或编译器分析?
论文希望保持较强的语言无关性。
如果分别为:
CC++PythonJavaGo...实现独立的解析器和编译器级插桩逻辑,系统很快又会重新陷入传统程序分析工具的维护成本。
因此论文让大语言模型负责:
源码 ↓大语言模型生成插桩 ↓插桩后的源码但这里并没有完全把正确性寄托在大语言模型上。
论文采用了非常简单的 enter/exit 结构,并配套确定性的验证器。
例如:
enter A enter B exit Bexit A是合法嵌套。
而:
enter A enter Bexit Aexit B则会被判定为错误。
如果发现错误,系统会把:
哪个源码块哪一行哪个 enter/exit 不匹配反馈给大语言模型,让它重新生成插桩。
这个设计的价值在于:
大语言模型负责生成,确定性验证器负责拦截明显错误。
大文件如何处理?
大文件不能一次性完整交给大语言模型。
论文因此把文件拆成多个代码块:
File ↓Chunk 1Chunk 2Chunk 3...默认分块大小为:
800 lines并尽量在函数边界处分割。
整体流程可以概括为:
读取源码文件 ↓按规模切分 ↓逐块生成插桩 ↓验证 enter/exit 是否匹配 ↓错误则反馈并重新生成 ↓合并结果 ↓建立源码块到源码行的映射这样设计的核心目的在于控制上下文规模,同时保证插桩过程可验证。
执行抽象:真正解决上下文规模的问题
插桩完成后,每次运行只需要执行程序,不需要再次调用大语言模型。
例如输入:
(1.00, 1.00001)会产生大量执行轨迹。
如果把完整轨迹原样交给大语言模型,信息量仍然很大。
因此论文进一步生成执行抽象(Execution Abstraction)。

图 3(论文第 5 页):执行抽象的目标是保留当前执行真正涉及的程序逻辑,同时删除没有执行到的代码,减少后续推理的上下文负担。
例如:
函数调用链:[src/count.c] main → [src/count.c] count以及当前执行涉及的源码:
#define FLOATS_BETWEEN_BUG 20int cnt = 0;
int count(float start, float end) { for (float cur = start; cur != end; cnt++) { unsigned temp; memcpy(&temp, &cur, sizeof(float)); temp++; memcpy(&cur, &temp, sizeof(float)); }
if (cnt <= FLOATS_BETWEEN_BUG) { // 未执行的 16-17 行被删除。行覆盖率:0/2 }}
int main(int argc, char **argv) { float start = atof(argv[1]), end = atof(argv[2]); __assume__(start < end); count(start, end);}这个循环实际上是在逐个遍历 start 和 end 之间的单精度浮点数表示值。
执行抽象的核心是根据一次具体执行结果,对原程序进行裁剪。 以 start = 1.00、end = 1.00001 为例,程序首先从 start 开始,通过修改浮点数的 IEEE-754 位表示逐步得到下一个可表示的浮点数,并持续执行直到 cur == end。这次执行共遍历了 84 个浮点数,因此 cnt 最终为 84。随后判断 cnt <= 20,结果为假,条件分支内部的代码从未执行。基于执行覆盖信息,执行抽象会将该未执行代码块删除,并保留其覆盖信息。最终得到的程序只包含本次具体执行实际涉及的控制流。
输入:测试输入 start = 1.00,end = 1.00001
函数 count(start, end): cnt ← 0 cur ← start
当 cur ≠ end 时: 将 cur 的 IEEE-754 二进制表示解释为无符号整数 temp temp ← temp + 1 将 temp 的二进制表示重新解释为浮点数 cur
cnt ← cnt + 1
如果 cnt ≤ 20: 执行漏洞相关代码
主函数 main: start ← 将第一个命令行参数转换为浮点数 end ← 将第二个命令行参数转换为浮点数
假设 start < end
调用 count(start, end)这里一个很重要的细节是:
删除的是没有执行到的源码块,已经执行的代码仍然保留。
因此执行抽象仍然保留:
- 当前已经执行过的程序逻辑;
if、for等控制结构;- 全局定义和变量声明;
- 当前覆盖率信息;
- 函数调用关系。
最终得到的是一个面向后续推理的程序视图:
具体执行轨迹 ↓执行抽象 ↓“这次运行实际上走了什么”6. 第二阶段:两个智能体分别负责“理解路径”和“构造输入”
第二阶段主要针对 C2,同时进一步利用第一阶段得到的执行信息。
两个核心组件是:
路径约束总结智能体 ↓路径约束 ↓约束求解智能体 ↓具体测试输入这两个组件的职责是分开的:
- 路径约束总结智能体:确定需要探索哪个分支,以及这个分支需要满足什么条件;
- 约束求解智能体:根据这些条件真正构造输入或运行环境。
可以把它理解为:
理解问题 ↓构造答案路径约束总结智能体:先确定要探索哪里
它接收:
当前输入 I+当前执行抽象 EA(P~ | I)然后通过一组动作逐步完成分析:
| 动作 | 作用 |
|---|---|
THINK | 进行推理和规划 |
CODE_REQUEST | 请求某个文件的源码和覆盖率 |
CHOOSE_BRANCH | 选择需要翻转的分支 |
SUMMARIZE | 总结该分支对应的路径约束 |
FINISH | 结束当前输入的探索 |
因此它的工作方式更接近一个循环:
查看当前执行 ↓判断信息是否足够 ↓缺少源码? ├─ 是 → 请求源码 └─ 否 ↓选择尚未覆盖的目标分支 ↓分析分支条件 ↓总结路径约束 ↓继续寻找其他目标分支 ↓结束论文还允许一次执行选择多个目标分支。
原因很简单:当前执行抽象本身已经产生了成本,多选几个目标分支可以让这一轮上下文被充分利用。
为什么要先选分支,再总结约束?
这是一个容易被忽略、但很关键的设计。
传统具体-符号执行通常是:
执行 ↓对整条路径进行符号化 ↓选择某个约束进行翻转CONCOLLMIC 则采用:
执行 ↓确定目标分支 ↓针对目标分支总结约束原因在于:一旦过早把所有实现细节提升到高层语义,例如:
“让 start 和 end 之间最多有 20 个可表示的浮点数”这个条件非常容易理解,但对应源码中的哪个分支可能已经不够明确。
因此论文保留了一个实现级的定位过程:
先确定“要去哪里”,再决定“用什么抽象表达怎么到达那里”。
再跑一次浮点例子


图 4(论文第 8 页):左侧是路径约束总结智能体,右侧是约束求解智能体。整个流程可以理解成“查看执行 → 找目标分支 → 提炼条件 → 构造输入 → 重新执行验证”。**
步骤 1:查看当前执行
当前输入:
1.01.00001执行结果显示:
cnt = 84而目标代码:
if (cnt <= FLOATS_BETWEEN_BUG) { printf("BUG triggered!");}尚未覆盖。
步骤 2:请求执行抽象中没有包含的源码
智能体调用:
CODE_REQUEST获取:
src/count.c:16-17看到:
printf("BUG triggered!");return -1;于是可以确定这个分支确实是值得探索的目标。
步骤 3:选择目标分支
智能体调用:
CHOOSE_BRANCH选择:
if (cnt <= FLOATS_BETWEEN_BUG) => true并记录:
lines_to_cover:src/count.c:16-17步骤 4:把低层实现总结成高层语义约束
随后调用:
SUMMARIZE得到类似这样的约束:
a) 程序需要两个命令行参数 start 和 end。
b) 两个参数都必须是合法的浮点数, 且 start < end。
c) [start, end) 中的不同 IEEE-754 单精度可表示浮点数数量必须 <= 20。这时,原本复杂的实现已经被压缩成一个很清楚的问题:
找到两个浮点数,使它们之间最多经过 20 个可表示的单精度浮点数。
约束求解智能体:重点是“如何构造”
约束求解智能体可以使用:
| 动作 | 作用 |
|---|---|
THINK | 规划求解过程 |
EXECUTE_CODE | 执行 Python 等代码进行精确计算 |
QUERY_SMT | 调用 Z3 等 SMT 求解器 |
GENERATE_TEST | 生成最终测试驱动程序 |
这意味着大语言模型不需要自己完成所有精确计算。
典型过程可以是:
分析约束 ↓需要精确计算? ↓执行 Python ↓得到计算结果 ↓继续推理 ↓需要 SMT? ↓查询 Z3整个过程更接近:
分析 ↓提出假设 ↓调用工具 ↓检查结果 ↓修正浮点例子具体怎么求?
在 1.0 附近,相邻单精度浮点数之间的距离约为:
如果最多允许 20 次相邻浮点数转换,则范围大约为:
论文进一步让智能体生成代码,精确计算下一个单精度可表示浮点数,而不是依赖近似估算。
伪代码可以表示为:
def next_float(f): # 计算下一个 IEEE-754 单精度浮点数 ...
current = 1.0
for _ in range(19): current = next_float(current)
print(next_float(current))最终得到:
1.000002384185791于是可以构造:
start = 1.0end = 1.000002384185791这时程序执行 20 次循环,最终:
cnt <= 20成立,漏洞分支被覆盖。
这里真正重要的设计在于:
大语言模型识别出问题的语义是“寻找下一个可表示的单精度浮点数”,再把精确计算交给代码执行工具。
最后还要重新执行验证
大语言模型总结路径约束时仍然可能出错,因此系统不会直接相信它生成的结果。
得到新输入:
I'之后重新执行:
具体执行 ↓新的执行轨迹 ↓覆盖率 / 目标源码行 ↓验证如果目标源码行确实被覆盖,就保留这个输入。
如果没有命中目标分支,但带来了新的全局覆盖率,也可以保留。
因此形成一个闭环:
智能体提出方案 ↓具体执行 ↓验证结果 ↓成功 → 加入工作队列失败 → 丢弃这也是论文处理大语言模型不可靠性的重要工程手段。
为什么这种架构比“一次性让 LLM 给答案”更稳?
论文实际上采用了三层约束。
1. 输入约束
大语言模型接触到的信息主要来自:
具体执行+执行轨迹+覆盖率+源码因此分析建立在真实程序行为之上。
2. 输出验证
插桩、路径约束和测试输入都尽可能经过验证。
尤其是最终测试输入必须重新执行:
测试输入→ 具体执行→ 覆盖率检查不满足目标就不会进入后续工作队列。
3. 工具增强推理
大语言模型可以调用:
EXECUTE_CODEQUERY_SMTCODE_REQUEST把精确计算、源码获取和部分求解任务交给专门工具。
所以这里的核心架构是:
大语言模型:语义理解、规划、抽象、工具编排
程序运行时:提供真实执行结果
Python / Z3:提供精确计算
验证器:检查生成结果7. 它不只是生成输入:环境、多语言和二进制输入
传统动态符号执行面对环境交互时尤其困难。
例如:
argvstdinfilesocketenvironment variablelibcsystem call原因在于,程序真正依赖的状态可能已经超出目标程序本身,很难为整个操作系统和运行时环境建立完整符号模型。
CONCOLLMIC 采取的思路是:
如果大语言模型能够理解程序语义,就可以进一步推理“什么运行时环境能够让目标分支成立”。
论文给出的 bc 案例非常典型。
案例:如何主动制造一次 malloc() 失败?
目标代码:
void *bc_malloc(size_t size) { void *ptr = (void *)malloc(size);
if (ptr == NULL) { // target branch }
return ptr;}这个分支为什么难覆盖?
因为普通运行环境下:
malloc(size)通常会成功。
因此:
普通输入 ↓malloc 正常返回 ↓无法进入错误处理分支仅靠输入变异,很难把运行时环境变成:
第 N 次 malloc 必须失败路径约束总结智能体先提炼条件
它可以得到:
条件 1:程序必须调用 bc_malloc。
条件 2:bc_malloc 内部的 malloc(size) 必须返回 NULL。第二个条件已经从“输入应该是什么”转变成了:
运行时环境应该是什么?约束求解智能体开始构造运行环境
智能体可以推断:
可以通过替换
malloc的运行时行为,让指定次数之后的分配直接返回NULL。
于是生成一个包装器:
void* malloc(size_t size) { ... malloc_count++;
if (malloc_count >= fail_after) { return NULL; }
return real_malloc(size);}再通过:
gcc ↓malloc_wrapper.so ↓LD_PRELOAD ↓启动 bc把这个运行时环境注入目标程序。

图 6(论文第 11 页):左侧是
malloc == NULL的目标分支,右侧是智能体生成的包装器和LD_PRELOAD测试驱动程序。这个案例体现了系统的一个重要能力:目标不再局限于“生成一个输入文件”,还可以生成满足路径条件所需的运行环境。**
最终测试驱动程序大致执行:
compile_result = subprocess.run( ["gcc", "-shared", "-fPIC", "malloc_wrapper.c", "-o", "malloc_wrapper.so", "-ldl"])
env = os.environ.copy()env["LD_PRELOAD"] = "./malloc_wrapper.so"
subprocess.run( ["./bc/bc", "non_existent_file.bc"], env=env)完整过程可以理解为:
发现目标分支 ↓理解条件:malloc 必须失败 ↓构造分配器包装器 ↓编译共享库 ↓设置 LD_PRELOAD ↓启动目标程序 ↓指定次数后的 malloc 返回 NULL ↓进入错误处理分支因此,这里的测试生成对象实际上已经从“输入数据”扩展到了程序运行环境。
多语言系统为什么也能处理?
论文还测试了:
ultrajson:Python + Cjansi:Java + Cpy4j:Python + Javaprotobuf-go:Go + C++

图 7(论文第 12 页):四个跨语言项目都观察到了持续的内部行覆盖率增长。相对初始输入,论文报告的平均覆盖率增长分别为:
ultrajson3.5×、jansi8.2×、py4j1.9×、protobuf-go1.9×。论文还使用前面的 GCov 实验验证了内部覆盖率与参考真值之间的相关性。
传统工具在多语言场景中需要同时考虑:
C 的符号模型Java 的符号模型Python 运行时Go 运行时跨语言边界...CONCOLLMIC 则统一成:
不同语言源码 ↓大语言模型生成插桩 ↓统一执行轨迹 ↓统一执行抽象 ↓同一套智能体工作流后续智能体关注的是:
函数调用链+已执行源码块+源码行+覆盖率而不需要为每一种语言维护完全独立的符号表达体系。
二进制输入为什么也能处理?
论文中的 woff2 和 oggenc 都涉及二进制输入。
这类输入对大语言模型本身并不友好,例如:
48 46 4F 4E 54 ...仅靠模型直接生成一段复杂的十六进制数据,很难保证格式始终正确。
论文中的工作流更接近:
理解二进制格式 ↓提出结构假设 ↓用 Python 生成十六进制数据 ↓具体执行 ↓观察程序行为 ↓修正假设 ↓再次生成因此真正发挥作用的是:
语义推理+程序执行+工具反馈系统并不要求大语言模型一次生成最终二进制输入。
8. 实现层面:系统并没有被做成一个“巨型智能体”
论文原型约有 8.2k 行 Python,基础模型使用 claude-3.7-sonnet-20250219。
几个实现细节值得关注。
插桩与测试使用不同的 Temperature
Instrumentation Agent:temperature = 0
Summarization / Solving Agent:temperature = 0.5插桩更强调确定性,因此使用 0。
后面的路径总结和约束求解需要一定的探索空间,因此使用 0.5。
提示词缓存
智能体是多轮运行的,前面的上下文会被不断重复使用。
论文采用了后端的增量提示词缓存,以降低重复传输上下文带来的成本。
这类优化对于下面这种循环尤其重要:
THINK→ TOOL→ THINK→ TOOL→ ...如果每次都重新发送完整上下文,成本会快速增加。
多个约束并行求解
路径约束总结智能体一次可以选择多个目标分支:
PC1 ─→ 求解流程 1 /总结智能体 ─── PC2 ─→ 求解流程 2 \ PC3 ─→ 求解流程 3这些相互独立的约束可以并行求解。
这属于比较直接的工程优化:大语言模型调用的主要成本之一是延迟,因此可以并行的求解流程没有必要全部串行执行。
9. 实验:它到底有没有比传统工具更强?
论文的实验并非只针对一个简单示例,而是覆盖了 12 个真实程序和系统。
实验对象
| 项目 | 输入类型 | 语言 | 规模 |
|---|---|---|---|
woff2 | 二进制字体数据 | C++ | 56 kLoC |
oggenc | 二进制音频 + 命令行 | C | 83 kLoC |
bc | 数学表达式 + 命令行 | C | 29 kLoC |
libmatheval | 数学表达式 | C | 15 kLoC |
libyaml | YAML | C | 11 kLoC |
libsoup | HTTP / 网络 | C | 71 kLoC |
krep | 正则表达式 + 命令行 | C | 4 kLoC |
confetti | 自定义配置 | C | 9 kLoC |
ultrajson | JSON | Python + C | 9 kLoC |
jansi | ANSI 转义序列 | Java + C | 72 kLoC |
py4j | Python + Java 对象 | Python + Java | 21 kLoC |
protobuf-go | Protobuf | Go + C++ | 36 kLoC |
其中 krep 和 confetti 在 Claude 3.7 Sonnet 的训练数据截止时间之后才公开,用于降低训练数据泄漏对实验结果的影响。
对比工具包括:
KLEEKLEE-PendingSymCCSymSanKLEE-FloatAFL++
实验环境为:
Docker2 CPUs8 GiB RAM48 h testing budget每个工具 / subject 重复 5 次如果 CONCOLLMIC 连续 30 分钟没有内部覆盖率增长,则提前终止。
C/C++ 分支覆盖率:为什么最终能超过传统工具?

图 5(论文第 10 页):重点观察整体趋势即可。
CONCOLLMIC在多个项目上能够较早进入覆盖率平台期,同时最终覆盖率高于传统动态符号执行工具。**
论文报告:
- 相比
KLEE:平均分支覆盖率提高 233%; - 相比
KLEE-Pending:提高 135%; - 相比
SymCC:提高 130%; - 相比
SymSan:提高 115%; - 相比 48 小时运行的
AFL++:提高 81%。
8 个 C/C++ 基准中,CONCOLLMIC 有 7 个项目的平均表现高于 AFL++,只有 libyaml 低于 AFL++。
这里更值得关注的问题是:
一个明显更慢的系统,为什么最终覆盖率反而更高?
论文给出的原因主要集中在两个方面。
原因一:能够表达高层语义约束
传统工具可能最终需要处理:
位向量数组指针浮点数内存...而智能体可以把它们提升为:
满足某种结构关系的输入然后再根据需要转换成具体输入。
原因二:能够处理运行时环境
例如:
命令行参数组合LD_PRELOAD网络输入运行时环境这些场景对传统动态符号执行的支持通常不够完整。
因此 CONCOLLMIC 的优势更多体现在:
它能够针对普通变异很难命中的路径生成更有针对性的输入或运行环境。
浮点约束实验:最能体现论文的核心思想
FP-Bench 最终使用了 26 个适合测试生成的程序。
结果如下:
| 工具 | 分支覆盖率 | 每个程序时间 | 成本 |
|---|---|---|---|
CONCOLLMIC | 77.87% | 464 s | $1.05 |
KLEE-Float | 64.81% | 46 s | — |
KLEE | 37.67% | 0.03 s | — |
CONCOLLMIC 相比:
KLEE-Float:覆盖率提高 20%;KLEE:覆盖率提高 107%。
这里不应该简单理解成“LLM 比 KLEE-Float 更懂浮点数”。
更准确的解释是:
KLEE
原版 KLEE 的浮点理论支持不足,因此部分路径最终无法求解,或者只能具体化处理,导致覆盖率较低。
KLEE-Float
增加专门的浮点数建模之后,覆盖率明显提高。
这说明:
完整的理论支持对于传统符号执行非常重要。
CONCOLLMIC
它可以从程序语义层面识别:
这个循环实际上是在枚举可表示的浮点数。然后将问题提升成:
[start, end) 中最多允许 N 个可表示浮点数。从而绕过部分实现级约束膨胀。
这才是浮点实验真正想说明的问题。
漏洞发现:它真的找到了新的漏洞吗?
答案是肯定的。
CONCOLLMIC 一共发现了:
11 个此前未知的漏洞。
包括:
| # | 项目 | 漏洞 |
|---|---|---|
| 1 | oggenc | 无效音频格式导致空指针解引用 |
| 2 | oggenc | 重采样过程中的有符号整数溢出 |
| 3 | oggenc | 畸形 WAV INFO 块导致有符号整数溢出 |
| 4 | oggenc | wav_open() 中的内存泄漏 |
| 5 | krep | 字符串模式 -s 下的参数处理错误 |
| 6 | libyaml | 无效 UTF-8 导致内存泄漏 |
| 7 | libyaml | 不完整 UTF-8 序列导致内存泄漏 |
| 8 | libsoup | Cookie 时间戳解析中的整数溢出 |
| 9 | confetti | 部分畸形配置导致内存泄漏 |
| 10 | ultrajson | 嵌套 JSON 键处理中的异常处理错误 |
| 11 | jansi | ANSI 转义序列参数处理导致 ClassCastException |
其中:
- 9 个漏洞已经得到开发者确认或修复;
libsoup的第 8 个问题获得 CVE-2025-4945;- 其他部分进入漏洞披露或 CVE 申请流程;
- 其中 6 个漏洞没有被对比工具发现。
对比工具发现的漏洞数量为:
KLEE 1KLEE-Pending 1SymCC 4SymSan 5AFL++ 3因此,这里的结果并不只是覆盖率曲线上的提升,还体现为实际漏洞发现能力。
为什么有些漏洞特别适合这种方法?
论文进一步做了人工分析。
这些漏洞往往需要同时满足多个相互关联的条件。
例如:
漏洞 #3
需要构造一个:
精确的畸形 WAV INFO 块普通变异很难稳定控制多个字段之间的关系。
漏洞 #5
需要:
特定命令行模式+特定参数组合漏洞 #8
需要:
HTTP 消息解析+畸形 Cookie 时间戳+算术约束这些问题比较适合:
高层语义理解+精确计算+具体执行验证这正好对应 CONCOLLMIC 的设计。
失败案例也很重要
论文从 oggenc 中随机抽取了 50 次失败的测试生成迭代,并进行了分类:
| 失败类型 | 比例 | 含义 |
|---|---|---|
| 不可达路径 | 36% | 目标分支本身不可达 |
| 由漏洞导致的崩溃 | 30% | 程序在到达目标前就因已有漏洞崩溃 |
| 路径总结错误 | 24% | 大语言模型错误理解了程序语义或路径约束 |
| 求解结果不一致 | 6% | 求解器生成的输入与约束不一致 |
| 执行超时 | 4% | 单次执行超过时间限制 |
这个结果很重要。
它说明当前系统的主要失败来源之一来自:
大语言模型仍然可能错误理解程序语义,并进一步生成错误的路径约束。
因此“程序语义 → 路径约束”这个环节仍然缺乏传统符号执行所追求的严格可靠性。
插桩和内部覆盖率可靠吗?
如果第一阶段的插桩本身不准确,后续覆盖率判断也会受到影响。
因此论文使用 GCov 作为参考真值进行验证。

行覆盖率相关性
CONCOLLMIC 内部覆盖率与 GCov 的皮尔逊相关系数:
平均值和中位数均为 94%
说明内部覆盖率与真实覆盖率高度相关。
论文认为差异主要来自:
- 少数控制流结构没有被插桩;
- GCov 对变量声明和赋值的统计方式不同。
验证准确性
内部验证结果相对于 GCov:
Precision = 84%Recall = 79%F1 = 81%验证器本身也存在误差。
论文采用了一个实际的补救策略:
除了保留命中目标源码行的输入,还保留能够带来新的全局覆盖率的输入。
这样,即使目标分支判断出现漏检,只要某个输入能够继续扩大覆盖率,也不会被立即丢弃。
成本到底有多高?
这是 CONCOLLMIC 最现实的问题之一。
论文给出的平均成本为:
插桩:$0.56 / 1 kLoC
测试:$0.21 / 每个测试输入69 秒 / 每个测试输入平均每个项目:
约 4.06 小时约 $53.86传统具体-符号执行可以快很多。
论文以 bc 为例指出:
SymCC在 60 秒内可以生成 700 多个不同输入,论文将其描述为约 800× 更快。
因此 CONCOLLMIC 的定位并不追求单位时间内生成尽可能多的输入。
但它的“慢”也有另一面
如果目标是达到各基线工具运行 48 小时后的最终分支覆盖率,论文报告 CONCOLLMIC 平均只需要:
31 个测试输入≈ $6.1≈ 32 分钟可以把两种思路理解成:
传统工具:大量输入 ↓持续探索 ↓覆盖率逐渐增长
CONCOLLMIC:较少输入 ↓针对难走分支进行推理 ↓定向生成高价值输入因此论文更偏向:
面向质量的定向测试生成
它更强调高价值路径的定向探索。
大语言模型调用的成本主要花在哪里?
论文图 9 对调用进行了拆解。
总结智能体中:
SUM-INITIAL 23.0%SUM-CODE_REQUEST 22.2%求解智能体中:
SOL-THINK 8.4% cost / 15.0% frequencySOL-EXECUTE_CODE 13.9% cost / 17.9% frequency从实际行为来看,比较典型的循环是:
THINK ↓EXECUTE_CODE ↓VALIDATE ↓THINK ↓EXECUTE_CODE ↓...这也是智能体式设计和“一次性提示词”的主要区别之一:模型可以在工具反馈之后继续修正自己的假设。
智能体还会根据问题选择不同的约束表示
论文发现,不同基准项目中工具调用方式差异明显。
例如:
woff2会更频繁地调用:
QUERY_SMT原因是其二进制格式包含大量字段之间的结构关系,这类问题比较适合结构化表示和 SMT 求解。
对应的流程可能是:
语义分析 ↓发现约束结构比较规整 ↓转换为 SMT ↓Z3对于其他问题,自然语言约束或直接编写 Python 程序可能已经足够。
因此:
约束表示可以根据问题类型选择合适的抽象层级。
这正是本论文相较传统固定符号表示方式比较有意思的一点。
10. 这篇论文真正值得关注的地方是什么?
如果只把论文理解成:
“大语言模型可以做具体-符号执行。”
其实会低估它的贡献。
更准确地说,论文重新划分了传统程序分析系统中的几个职责。
第一层:把符号化建模从运行时执行中拆出来
传统系统:
执行 + 符号化通常高度耦合。
CONCOLLMIC:
具体执行 ↓执行轨迹 ↓大语言模型符号化符号化被推迟到具体执行之后,语言和运行时相关的问题主要集中在插桩阶段。
第二层:把“公式接口”变成“语义接口”
传统组件之间的接口更接近:
程序 ↓符号表达式 ↓SMT 求解器CONCOLLMIC 则更接近:
程序 ↓执行抽象 ↓语义约束 ↓约束求解智能体两个智能体之间不再要求共享一种固定的低层符号表示。
约束可以处在:
自然语言编程语言SMT等不同层级,并且可以在这些层级之间转换。
第三层:让求解器成为工具,服务于上层推理
传统方式:
约束 ↓求解器CONCOLLMIC:
约束 ↓约束求解智能体 ├── 推理 ├── Python ├── Z3 └── 生成测试驱动程序这样,求解器不需要承担所有程序语义理解工作,只需要在擅长的精确计算部分提供可靠结果。
可以概括成:
大语言模型负责编排与语义推理,传统工具负责精确执行和计算。
几个比较漂亮的工程设计
1. enter/exit 插桩足够简单
没有设计复杂的插桩语言,而是:
entercodeexit然后用确定性验证器检查结构。
对于大语言模型生成代码而言,简单、可验证的中间表示通常比复杂但灵活的表示更容易控制错误。
2. 执行抽象没有试图总结整个项目
它主要提供:
当前执行过什么+哪些地方没有覆盖+需要时再请求源码相当于对大语言模型的上下文进行了主动预算。
3. 先选目标分支,再总结路径约束
这个顺序同时解决两个问题:
实现级信息 ↓确定“要去哪里”
语义级信息 ↓确定“怎么去”这样既不会丢失目标分支的定位信息,也可以避免让符号约束始终停留在低层表示。
4. 约束求解智能体可以直接写程序
真实程序中的路径条件经常涉及:
创建文件设置环境变量编译包装器LD_PRELOAD启动进程检查输出这些任务很难被压缩成一个简单的数学公式。
这时,直接生成测试驱动程序往往比强行构造 SMT 公式更自然。
5. 具体执行验证始终处于闭环中
整体流程是:
语义推理 ↓生成测试输入 ↓具体执行 ↓验证 ↓反馈这让系统不必假设大语言模型始终正确,而是通过真实程序执行阻止错误结果继续传播。
11. 局限:这套方法目前还远没有成熟到可以替代传统工具
论文对局限的讨论其实比较坦率。
成本仍然很高
如果:
每个输入 ≈ $0.21一个项目生成几百个输入,成本就会迅速上升。
对于长期运行的大规模模糊测试任务,这很难接受。
论文提出的一个自然方向是和:
AFL+++传统具体-符号执行+CONCOLLMIC进行混合式集成。
例如:
模糊测试器 ↓高吞吐量探索 ↓发现长期无法覆盖的复杂分支 ↓CONCOLLMIC ↓定向生成高价值输入 ↓回到模糊测试器这种架构比直接让 CONCOLLMIC 独立承担全部测试任务更现实。
上下文窗口仍然是硬限制
论文中的测试程序大多只有几十 kLoC,没有明显耗尽 Claude Sonnet 3.7 的 200K 上下文窗口。
但如果:
程序规模更大+执行轨迹更长+执行抽象更复杂仍然可能遇到上下文规模问题。
论文提出的方向是增量式总结:
执行轨迹 ↓片段 1 → 总结 1片段 2 → 总结 2片段 3 → 总结 3... ↓增量式执行轨迹总结这可能是后续扩展到大型软件的重要方向。
大语言模型本身缺乏严格可靠性
CONCOLLMIC 更适合:
测试漏洞发现覆盖率探索而不适合:
证明程序不存在漏洞原因很直接:
大语言模型可能理解错程序大语言模型可能总结错约束大语言模型可能生成错误的测试驱动程序论文的核心策略是利用:
具体执行+工具调用+结果验证限制错误传播,而不依赖大语言模型自身具备严格的符号执行可靠性。
数据泄漏问题
由于实验使用:
Claude Sonnet 3.7而不少基准项目是公开项目,因此存在一种潜在风险:
基准代码 ↓可能出现在训练数据中 ↓模型提前“见过”代码 ↓实验结果被高估论文专门加入了:
krepconfetti这两个项目在模型训练数据截止时间之后才公开。
论文观察到,在这两个项目上没有看到明显的性能下降。
这不能彻底证明不存在数据泄漏,但至少提供了一项合理的检查。
路径选择并非主要收益来源
论文还进行了路径选择的初步实验,对比了:
基于大语言模型的路径选择和:
深度优先搜索等传统策略结果没有发现明显的性能差异。
这个结果反而帮助明确了论文真正的贡献:
性能提升的关键并不主要来自“大语言模型更会选择下一条路径”,而更可能来自符号化建模、约束表示、约束求解以及运行时环境操控。
因此,论文更值得关注的是让大语言模型处理传统符号执行中最难维护的程序语义。
12. 如果把整个系统压缩成一张图
可以把 CONCOLLMIC 记成:
源码 │ ▼ ┌──────────────────┐ │ 大语言模型插桩器 │ └────────┬─────────┘ │ 插桩后的程序 │ ▼ 具体执行 │ ▼ 执行轨迹 │ ▼ 执行抽象 │ ▼ ┌────────────────────────┐ │ 路径约束总结智能体 │ │ │ │ THINK │ │ CODE_REQUEST │ │ CHOOSE_BRANCH │ │ SUMMARIZE │ └───────────┬────────────┘ │ 语义约束 │ ▼ ┌────────────────────────┐ │ 约束求解智能体 │ │ │ │ THINK │ │ EXECUTE_CODE │ │ QUERY_SMT │ │ GENERATE_TEST │ └───────────┬────────────┘ │ 测试驱动程序 │ ▼ 具体执行 │ ┌─────┴─────┐ │ │ 有效 无效 │ │ ▼ ▼ 工作队列 丢弃 │ └──────→ 下一轮探索因此,CONCOLLMIC 可以概括为四个部分:
具体执行+大语言模型语义推理+传统精确工具+验证反馈四者共同形成闭环。
13. 工作价值分析
这篇工作的价值也不在于“大语言模型可以写测试”本身。
让大语言模型生成:
单元测试模糊测试输入测试驱动程序本身已经不新鲜。
真正有意思的是,论文把大语言模型放进了一个长期高度依赖人工语义建模的程序分析流程中,同时没有简单地把传统工具全部替换掉。
整个系统实际上是在重新分工:
大语言模型:理解语义、规划、抽象、工具编排
具体运行时:提供真实执行结果
Python / Z3:负责精确计算
验证器:检查生成结果这个职责划分比较合理。
尤其是 bc + LD_PRELOAD + malloc failure 这个案例,非常能说明这种设计的价值。
如果目标只是:
让大语言模型生成一个测试输入这个方向本身并不新。
但如果任务变成:
发现目标路径需要 malloc == NULL ↓分析运行时环境条件 ↓生成 malloc 包装器 ↓编译共享库 ↓设置 LD_PRELOAD ↓启动目标程序 ↓验证目标分支被覆盖智能体的价值就变得非常具体。
再看一下 IEEE 的审稿意见:这篇工作下一步最需要补什么?
论文附录中还保留了 2026 IEEE Symposium on Security and Privacy 的元评审意见。这个部分对理解论文边界很有帮助。
审稿意见认可的主要优点包括:
- 用大语言模型智能体处理具体-符号执行中长期存在的环境建模和约束求解问题;
CONCOLLMIC本身提供了新的工具和研究方向;- 在真实软件上发现了新的漏洞;
- 智能体 + 工具增强的设计具有一定新颖性。
同时也提出了几个值得继续研究的问题。
1. 基准规模和代表性
评审认为当前测试程序总体规模仍然有限,而且并非全部来自统一的标准基准套件。
因此一个重要问题是:
当前观察到的覆盖率提升,能否推广到更大、更复杂的软件?
论文说明,除 krep 和 confetti 外,C/C++ 基准主要来自既有符号执行、模糊测试研究和 OSS-Fuzz,并且基准选择有意覆盖不同约束理论和输入形式。
这个解释可以回应部分疑问,但大规模软件上的可扩展性仍然是后续工作。
2. 高层约束与低层约束的消融实验还不够
这是一个很关键的问题。
论文的重要观点之一是:
高层语义约束 vs.低层 SMT 约束但如果缺少更系统的消融实验,就很难精确回答:
性能提升究竟来自大语言模型智能体,还是来自高层约束表示本身?
以及:
什么类型的问题适合高层约束,什么类型的问题仍然适合直接使用 SMT?
论文也将这一点作为值得进一步研究的方向。
3. 大语言模型的非确定性会影响可复现性
传统流程:
程序 + 输入 + 求解器相对容易复现。
智能体系统则变成:
程序+提示词+大语言模型+工具调用+模型版本变量明显更多。
论文通过固定模型版本、使用工具进行约束以及开源实现来缓解这一问题,但从研究方法论角度看,大语言模型的随机性仍然会增加实验复现成本。
更现实的落地方向:与传统工具形成混合系统
从实验结果看,CONCOLLMIC 确实能够找到传统工具较难触及的路径和漏洞,但当前系统仍然具有:
成本高+非确定+缺乏严格可靠性+受上下文规模限制等特点。
因此,更现实的未来架构可以考虑:
CONCOLLMIC 替代 AFL++而是:
AFL++ / SymCC │ │ 高吞吐量探索 ▼ 长期无法覆盖的复杂分支 │ ▼ CONCOLLMIC │ ├── 高层语义推理 ├── 运行时环境操控 ├── Python / SMT └── 定向生成输入 │ ▼ 新测试输入 │ ▼ 回到传统测试框架也就是说,可以把 CONCOLLMIC 看成一个高价值路径生成器和语义推理后端。
14. 总结
如果只记住这篇论文的一句话:
传统具体-符号执行倾向于把程序机械地翻译成符号公式再求解;
CONCOLLMIC则尝试先让大语言模型理解程序真正表达的语义,再由智能体调用精确工具,将语义约束落实为具体输入或运行时环境。
整套方法可以压缩成:
传统动态符号执行:程序语义复杂 +低层约束复杂 ↓符号模型和求解成本不断上升
CONCOLLMIC:具体执行 ↓执行抽象 ↓大语言模型语义推理 ↓灵活的约束表示 ↓工具增强求解 ↓具体执行验证这篇论文真正值得关注的方向,也可以概括成一句话:
让大语言模型承担传统程序分析器最难维护、同时又最依赖语义理解的部分;让传统程序分析工具继续承担需要严格精确性的部分。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!









京公网安备11011402057358号