一、符号执行初探:什么是符号执行?为什么需要符号执行?

大家好,我是你们这门课的老朋友。今天咱们来聊聊符号执行——这个听起来有点高大上、其实特别接地气的技术。

先问个问题:你平时怎么测代码的?

大概率是写几个测试用例,跑一跑,看看结果对不对。对吧?

但你想过没有——你写的测试用例,真的覆盖了所有可能的情况吗?

嗯,这就是传统测试最让人头疼的地方。而符号执行,恰恰就是来解决这个问题的。

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 符号执行的核心流程

为了让你更直观地理解,我画了一张流程图:

符号执行核心流程 1. 输入符号化 2. 符号执行引擎 3. 路径约束收集 4. 约束求解 5. 生成测试用例 关键说明 • 输入变量用符号表示 • 引擎逐语句执行 • 遇到分支时分裂路径 • 记录每个路径的条件 • 求解器计算具体值 • 输出可复现的测试数据 ⚠ 注意: 循环和系统调用 可能导致路径爆炸 需设置超时或限制

这个流程看起来简单,但每一步都有不少门道。比如第二步「符号执行引擎」,它需要维护一个「符号状态」——记录每个变量当前是哪个符号表达式。第三步「路径约束收集」,它要处理分支语句,把条件记录下来。

我刚开始学的时候,觉得这些概念很抽象。后来用 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 这个工具,一步步把理论变成实战。


专注资料整理