一、符号执行初探:什么是符号执行?为什么需要符号执行?
大家好,我是你们这门课的老朋友。今天咱们来聊聊符号执行——这个听起来有点高大上、其实特别接地气的技术。
先问个问题:你平时怎么测代码的?
大概率是写几个测试用例,跑一跑,看看结果对不对。对吧?
但你想过没有——你写的测试用例,真的覆盖了所有可能的情况吗?
嗯,这就是传统测试最让人头疼的地方。而符号执行,恰恰就是来解决这个问题的。
1.1 什么是符号执行?
符号执行,说白了就是:把程序里的变量,不当作具体的数值,而是当作一个「符号」。
什么意思呢?我举个例子。
假设你有一段代码:
int x = input();
int y = x * 2;
if (y > 10) {
// 分支A
} else {
// 分支B
}
传统测试会怎么做?给 x 赋个值,比如 x=3,然后 y=6,走 else 分支。再给 x=6,y=12,走 if 分支。你得手动构造两个测试用例。
符号执行呢?它把 x 当作一个符号,记作 α。然后 y 就是 α * 2。走到 if 语句时,它自动产生两个路径约束:
- 路径A:
α * 2 > 10→ 即α > 5 - 路径B:
α * 2 <= 10→ 即α <= 5
然后它调用一个叫「约束求解器」的东西,自动算出:α=6 可以走路径A,α=3 可以走路径B。
看到了吗?你不需要手动构造测试数据,符号执行帮你自动生成了。
核心概念总结:
- 符号值:用符号(如 α、β)代替具体数值
- 路径约束:记录每条路径需要满足的条件
- 约束求解器:自动求解路径约束,生成具体测试数据
1.2 为什么需要符号执行?
你可能会问:我手动写测试用例不也挺好的吗?
嗯,我刚开始也这么想。直到有一次,我在项目中遇到一个漏洞——一个整数溢出导致的缓冲区溢出。那个漏洞藏在三层嵌套的 if 语句里,手动构造测试用例?我试了三天,愣是没触发出来。
后来用符号执行,几秒钟就找到了触发条件。
为什么会这样?因为人脑处理「组合爆炸」的能力是有限的。你想想看:
- 一个 if-else,2 条路径
- 两个 if-else,4 条路径
- 十个 if-else,1024 条路径
- 二十个 if-else,超过 100 万条路径
手动测试?根本不可能覆盖完。
符号执行的优势就在这里:它能自动探索所有可能的路径,并且为每条路径生成具体的输入。
我个人习惯:在接手一个陌生项目时,先用符号执行跑一遍关键函数。不是为了找 bug,而是为了快速理解代码的所有行为分支。这比读代码快多了。
1.3 符号执行与传统测试的区别
咱们用一张表来对比,一目了然:
| 对比维度 | 传统测试 | 符号执行 |
|---|---|---|
| 输入方式 | 手动指定具体数值 | 自动使用符号变量 |
| 路径覆盖 | 依赖测试人员经验,容易遗漏 | 自动探索所有可达路径 |
| 测试数据生成 | 手动构造 | 自动求解生成 |
| 发现隐藏漏洞 | 困难,尤其是深层嵌套 | 擅长发现边界条件和复杂路径 |
| 执行效率 | 快(直接运行具体值) | 较慢(需要约束求解) |
| 适用场景 | 功能验证、回归测试 | 漏洞挖掘、路径分析、逆向工程 |
说白了,传统测试是「你告诉程序怎么走」,符号执行是「程序告诉你它能怎么走」。
但这里有个坑,我得提醒你:
我曾经踩过的坑:符号执行不是万能的。遇到循环次数不确定、或者调用外部系统调用时,符号执行可能会「路径爆炸」——路径数量指数级增长,直接跑死。所以实际使用中,我们通常会设置超时时间,或者用「混合执行」来缓解这个问题。
1.4 符号执行的核心流程
为了让你更直观地理解,我画了一张流程图:
这个流程看起来简单,但每一步都有不少门道。比如第二步「符号执行引擎」,它需要维护一个「符号状态」——记录每个变量当前是哪个符号表达式。第三步「路径约束收集」,它要处理分支语句,把条件记录下来。
我刚开始学的时候,觉得这些概念很抽象。后来用 angr 跑了一个实际程序,看到它自动生成了几十个测试用例,才真正理解了它的威力。
1.5 一个简单的例子
咱们来看一个更完整的例子,感受一下符号执行是怎么工作的:
int mystery(int a, int b) {
int result = 0;
if (a > 10) {
result = a + b;
} else {
result = a - b;
}
if (result < 0) {
return -1;
}
return result;
}
传统测试,你得想:a 和 b 取什么值能覆盖所有分支?
符号执行呢?它自动分析出:
- 路径1:a > 10, a+b >= 0 → 返回 a+b
- 路径2:a > 10, a+b < 0 → 返回 -1
- 路径3:a <= 10, a-b >= 0 → 返回 a-b
- 路径4:a <= 10, a-b < 0 → 返回 -1
然后求解器自动给出:a=11, b=5 走路径1;a=11, b=-20 走路径2……等等。
这就是符号执行的魅力——它替你思考所有可能性。
我建议:刚开始接触符号执行时,不要急着上大型项目。先拿这种几行的小函数练手,理解路径约束和求解的过程。等熟练了,再挑战真实世界的二进制程序。
好了,第一章的内容就到这里。符号执行的核心思想其实不复杂——把具体值换成符号,让计算机替你探索所有路径。后面的章节,我们会用 angr 这个工具,一步步把理论变成实战。