最近在自学符号执行,所以,这篇经典文章(Symbolic Execution for Software Testing: Three Decades Later)[1]做为入门必读。git
符号执行 (Symbolic Execution)是一种程序分析技术,它能够经过分析程序来获得让特定代码区域执行的输入。顾名思义,使用符号执行分析一个程序时,该程序会使用符号值做为输入,而非通常执行程序时使用的具体值。在达到目标代码时,分析器能够获得相应的路径约束,而后经过约束求解器来获得能够触发目标代码的具体值。github
软件测试中的符号执行主要目标是[1]:web
在给定的探索尽量多的、不一样的程序路径(program path)。对于每一条程序路径,
1) 生成一个具体输入的集合(主要能力);
2) 检查是否存在各类错误(errors,包含assertion violations, uncaught exceptions, security vulnerabilities, and memory corruption)。express
从测试生成的角度,符号执行能够生成高覆盖率的测试用例。
从bug finding的角度,符号执行能够提供一个具体的输入用于触发bug(过去经常用于调试bug)。缓存
符号执行的主要思想就是将输入(input)用符号来表征而不是具体值,同时将程序变量表征成符号表达式。所以,程序的输出就会被表征成一个程序输入的函数,即fun(input)。在软件测试中,符号执行被用于生成执行路径(execution path)的输入。
执行路径(execution path):一个true和false的序列seq={p0,p1,...,pn}。其中,若是是一个条件语句,那么pi=ture则表示这条条件语句取true,不然取false。
执行树(execution tree):一个程序的全部执行路径则可表征成一棵执行树。
以下图所示:
对应的执行树:
3条不一样的执行路径构成了一棵执行树。三组输入{x=0,y=1},{x=2,y=1},{x=30,y=15}覆盖了全部的执行路径。所以,符号执行的目标就是在给定的时间内,生成一个输入的集合使得全部的(或让尽量多的)执行路径依赖于由符号表征的输入。并发
符号状态(symbolic state):符号执行维护一个符号状态,它是一个<变量,符号表达式(symbolic expressions)>的mapping。app
符号路径约束(symbolic path constraint): a quantifier-free first-order formula over symbolic expressions.dom
符号执行后的结果以下图所示:
分布式
当代码中包含循环和递归时,若是终止条件是符号的话,那么符号执行会产生无限数量的执行路径。例以下图所示,
ide
这个时候,符号路径要么就是一串有限长度的ture
后面跟着一个false
(跳出循环),要么就是无限长的true
。如图所示,
符号执行的主要缺点:若是符号路径约束不可解(不能被solver解决)或者是不能被高效(时间效率)的解,则不能生成input。回到以前的例子(见图 ~\ref{example-program}),若是把函数twice
改为(v*v)%50
(非线性):
假设采用的sovler不可解非线性约束,那么,符号执行将失败,即没法生成input。
现代符号执行技术的特色是同时执行精确(Concrete)执行和符号(Symbolic)执行。
当给定若干个具体的输入时,Concolic testing动态地执行符号执行。Concolic testing会同时维护两个状态:
不一样于传统的符号执行技术,因为Concolic testing须要维护程序执行时的整个精确状态,所以它须要一个精确的初始值。举例说明,仍是以前这个例子:
表明工具:
EGT在执行每一个操做以前,检查每一个相关的值是精确的仍是已经符号化了的,而后动态地混合精确执行和符号执行。若是全部的相关值都是一个实际的值(即精确的,concrete),那么,直接执行原始程序(即,操做,operation)。不然(至少一个值是符号化的),这个操做将会被符号执行。举例说明,假设以前那个例子,第17行改为y=10
。那么,在调用twice时,传入的参数是concrete的,返回的也是一个concrete value,所以z
也是一个concrete value。第七、8行中的z
和y+10
也会被转换成concrete vaule。而后再进行符号执行。
表明工具:
因而可知,不管是concolic testing仍是EGT,他们都是动态地mix use concrete and symbolic execution。所以,也被称做“动态符号执行”。
不精确性:代码调用了第三方代码库(因为种种缘由没法进行代码插装),假设传入的参数都是concrete value,那么就像EGT中的同样,能够所有看成concrete execution。即便传入的参数中包含符号,动态符号执行仍是能够对符号设一个具体的值。Concolic和EGT有不一样的解决方法,后面再介绍。除了调用外部代码,对于难以处理的操做数(如浮点型)或是一些复杂的函数(solver没法解决或须要耗费大量时间),使用concrete value能够帮助符号执行环节这种impression。
完整性:动态符号执行经过concrete value能够简化约束,从而防止符号执行get stuck。可是这也会带来一个新问题,就是因为这种简化,它可能会致使一些不完整性,即有时候没法对全部的execution path都能生成input。可是,当遇到不支持的操做或外部调用时,这显然优于简单地停止执行。
描述:
首先,要知道,符号执行implicitly过滤两种路径:
解决方案:
描述:
约束求解是符号执行的技术瓶颈。所以,对于solver的优化(提升solver的求解能力)成了解决这个技术瓶颈的手段。
解决方案:
(x+y>10)^(z>0)^(y<12)^(z-x=0)
。假设咱们如今想生成知足(x+y>10)^(z>0)^¬(y<12)
,其中咱们想创建对¬(y<12)
(与y有关)的feasibility。那么,(z>0)
和(z-x=0)
这两个约束均可以去掉,由于与y不相关。(x+y<10)^(x>5)=>{x=6,y=3}
。对于新的约束,首先判断这个新约束的搜索空间是缓存里约束的超集仍是子集。若是是新的约束的搜索空间是缓存的约束的子集,那么,就把缓存中的约束去掉多余的条件后继续求解。若是是超集,那么直接把解代入去验证。描述:
程序语句转化成符号约束的精度对符号执行的覆盖率有很大的影响。例如,内存建模是用一个具体的数值去近似一个固定位数的整数变量可能会颇有效,可是另外一方面,它也会致使imprecision,好比丢失一些符号执行的路径,或探索一些无用的解。另外一个例子就是指针,一些工具如DART[3]只能处理精确的指针。
解决方案:
precision和scalability是一个trade-off。须要考虑的因素有:
描述:
不少现实世界中的程序是并发的,这也意味着他们不少都是不肯定的(non-deteminism)。尽管如此,符号执行已经被有效地运用在测试并发系统,分布式系统。
后续,我会记录一些step by step使用部分符号执行工具的blog。
[1]: Symbolic Execution for Software Testing: Three Decades Later
[2]: DART: Directed Automated Random Testing
[3]: CUTE: A concolic unit testing engine for C
[4]: EXE: Automatically generating inputs of death
[5]: KLEE: Unassisted and automatic generation of high-coverage tests for complex systems programs