符号执行(静态执行、动态执行、并行执行、选择执行

目录

为什么需要符号执行:

符号执行基本模型

符号执行的基本思想

符号模型的执行语义

1、符号数据对象:

2、程序语句:

程序执行状态

路径约束条件pc

约束求解:

符号执行实例:

动态符号执行:

动态符号执行的基本思想

动态符号执行的执行过程

并行符号执行技术

如何有效避免各节点对程序执行路径的重复搜索是并行系统需要解决的首要问题。

分布式环境下的负载均衡策略

选择符号执行技术

选择符号执行技术的基本思想

具体执行至符号执行的转换

符号执行到具体执行的转换


为什么需要符号执行:

20世纪70年代软件正确性测试方法主要分为两类:
以模糊测试为代表的随机性测试方法
但其具有的盲目性和随机性使其无法提供完整可靠的测试结果。
以模型检测为代表的形式化证明方法
通过归纳法来证明程序是否具有期望的性质,但证明过程的复杂性使其在面对大规模程序时几乎不可用。

在此背景下,1976年Jame C. King提出了符号执行技术,可以将其看作是上述两种方法的折中。
King希望在无法获取程序特性说明等信息的情况下,仍然能够对其进行快速全面的自动化安全性检测。

符号执行基本模型

符号执行的基本思想

使用符号变量代替具体值作为程序或函数的参数,并模拟执行程序中的指令;
各指令的操作都是基于符号变量进行的,其中操作数的值由符号和常量组成的表达式来表示。

对于任意程序,其执行流程是由指令序列的执行语义控制的。
执行语义包括:
变量定义语句对数据对象的描述;
声明语句对程序数据对象的修改;
条件语句对程序执行流程的控制。
当程序的输入参数确定时,其指令序列也被固定下来,因此,程序执行语义和控制流也得到确定。
若不使用具体数值,而是用符号值作为程序的输入参数,则指令序列的操作对象就从具体数值变为符号值,相应地,程序的执行语义和控制流也变成与符号变量相关的符号表达式。

符号执行可以视为程序具体执行的自然扩展
符号变量使得程序执行语义变得不确定,也使得符号执行技术在理想情况下可以遍历程序执行树的所有路径。
同时,程序的一次具体执行也可以视为符号执行的一个实例
当需要对某条程序路径进行遍历分析时,只需要根据符号执行方法对该路径的分析结果,即可引导控制流遍历该路径的程序输入。

符号模型的执行语义

虽然程序语义因为符号变量的加入而发生变化,但无论是程序语法还是程序语句的操作流程均不会因为符号变量的存在而发生变化,从而保证了符号执行技术的有效性。
程序执行语义在符号执行模式下会发生变化,主要涉及
1、符号数据对象
2、程序语句
3、程序执行状态

1、符号数据对象:

为了简化描述,假设每次程序需要新的输入时,均从符号列表{a1, a2, a3, …}中选取。
程序输入参数中的符号变量通过变量声明、数学运算操作等方式传递至程序中的变量。
在理想模型中,程序使用的每个符号变量均应该是一个有符号整数。

2、程序语句:

程序语句中有如下几类语句:
变量操作语句:
数学运算符 数据读写操作;
无条件跳转语句;
声明语句及条件跳转语句;

数学运算符:

程序具体执行时,数学运算操作可以描述成操作符、圆括号和整数变量构成的多项式表达式。
例如,a = 1 + (2 * 4)。
符号执行模式下同样可以进行类似的描述,只需要将表达式中的整数替换成符号值集合即可。
例如,a = a1 + (a2 * a3)。
可见,符号值的引入并未修改数学运算符的操作语义,仅将运算结果由整数多项式变成了符号多项式。

数据读写操作

以读操作read(addr)为例,
当addr中的值为具体值时,read(addr)返回具体值;
若addr中的值为符号值,则返回符号值。
可见,符号执行并没有影响读写操作的语义。

无条件跳转语句

goto语句称为无条件跳转语句,其一般格式为
goto 语句标号
其中,语句标号是按照程序标识符规范书写的符号,放在某一语句行前面,标号后需要添加冒号(:),起到标识语句的作用,与goto语句配合使用。
语句标号为loop的示例
goto loop:
……
loop: while (x < 7);
符号执行模式中,goto语句与具体执行中的语义是完全一致的。

声明语句及条件跳转语句

具体执行时,无论是条件跳转语句还是声明语句,语句内表达式的取值均是具体值。
例如,if (a < 0),表达式a < 0的真值在语句执行完成后即可计算得到,由于a的取值是已知的,因而可以根据真值决定条件分支的跳转。
符号执行时,if语句的语义没有变化,同样是根据a < 0的计算结果进行跳转,但此时a为符号变量,无法计算出a < 0的真值,因而改变了程序的执行语义。
因此,符号执行与具体执行的关键区别在于:
符号变量的引入使得程序执行到路径分支时无法确定程序的走向。

程序执行状态

具体执行时,程序状态中通常包括程序变量的具体值、程序指令计数等描述信息,使用这些信息即可描述程序执行的控制流向。
符号执行时,因为符号变量的引入导致分支走向不确定,仅凭原有的信息已经无法完整描述符号执行的状态。
因此,King为程序状态增加了变量:
路径约束条件,以pc(path constraint)表示。

路径约束条件pc

简单地说,pc是符号执行过程中对路径上条件分支走向的选择情况,根据状态中的pc变量即可确定一次符号执行的完整路径。
在符号执行过程中,在每个if条件语句处没有实际值决定程序执行哪条分支,从而需要符号执行引擎主动选择执行分支并记录整个执行过程,pc可以辅助完成该项工作。
例如,假设符号执行过程中经过3个与符号变量相关的if条件语句if1、if2和if3,每个条件语句处的表达式如下所示:
if1: a1 ≥ 0
if2: a1 + 2 * a2 ≥ 0
if3: a3 ≥ 0
假设引擎在3个if条件分支处分别选择的是if1: true,if2: true,if3: false,则pc表示为
pc=(a_1≥0∧a_1+2∗a_2≥0∧¬(a_3≥0))

pc是一个bool表达式,由符号执行路径上涉及的if条件语句中的表达式及表达式的真值选择拼接而成。

pc的初始值为true。

当选择then分支时,假设输入变量是满足q的,该过程可以描述为pc=pc∧q。

类似地,当选择else分支时,该过程可以描述为
pc=pc∧¬q。

由于根据pc的内容即可确定一条唯一的程序执行路径,因而,pc被称为条件路径

约束求解:

符号执行过程中,执行树中每个叶节点对应的pc会被输入约束求解器进行求解。
若pc表达式有解,则求解器会输出满足pc的一组符号变量的具体值;
若pc表达式无解,则说明该叶节点对应的执行路径是不可达的。

约束求解问题可以形式化表示为一个三元组<V, D, C>,
其中变量V是变量的有限集合,表示为V = {v1, v2, …, vn};
变量的论域D是变量可能取值的有限集合,变量vi只能在其论域Di中取值;
约束C是一个有限约束集合,某个约束关系Ci包含V中一个或多个变量,若Ci包含k个变量,则称Ci为在这k个变量集合上的k元约束。

约束求解就是找到约束问题的一个解,该解对变量集合中所有变量都赋一个取自其论域的值,且这些变量的取值满足该问题所有的约束条件。

符号执行实例:

用类PL/1语言的语法编写的计算三数之和的代码。
1: SUM: PROCEDURE(A, B, C);
2: X ← A + B;
3: Y ← B + C;
4: Z ← X + Y – B;
5: RETURN(Z);
6: END

SUM函数在具体执行过程中各变量的变化

函数的初始输入为1,3,5,程序输出为9。

 

 在符号执行过程中,以A、B、C代表3个整数时各变量的变化

语句1是函数的入口,符号执行引擎将三个输入参数符号化为𝝰1、𝝰2和𝝰3 ,同时将pc初始化为true。

语句2为X=A+B,使用符号进行数学运算,并将符号表达式赋予变量X,X=𝝰1+𝝰2。

语句3为Y=B+C,使用符号进行数学运算,并将符号表达式赋予变量Y,Y=𝝰1+𝝰3。

语句4为Z=X+Y-B,将各变量的符号值带入运算得Z=𝝰1+𝝰2+𝝰3。

函数将符号表达式𝝰1+𝝰2+𝝰3作为返回值。

动态符号执行:

动态符号执行技术是符号执行优化过程中出现的一种新方法。
是为了缓解传统静态符号执行中的误报率高、效率低等问题。

动态符号执行的基本思想

以具体的数值作为输入,执行程序代码,在程序实际执行路径的基础上,用符号执行技术对路径进行分析,提取路径的约束表达式。

根据路径搜索策略(广度、深度)对约束表达式进行变形,求解变形后的表达式并生成新的测试用例。

不断迭代上述过程,直到完全遍历程序的所有执行路径

动态符号执行的执行过程

使用随机的具体值作为程序初始输入开始具体执行,同时对具体执行路径上的代码进行符号执行(程序的初始输入作为符号),并从当前执行路径的分支条件语句的谓词中搜集所有符号约束条件及其对应的真值(例如,z – y == 0: true)。
根据收集到的符号约束条件,按照一定的路径选择策略(深度优先或广度优先),对其中的某个约束条件进行取反,构造出一条新的可行的路径约束。
使用约束求解器求解出新约束集合对应的具体输入。
使用符号执行引擎对新输入值进行新一轮的分析。

实例:

1、假设需要使用动态符号执行技术测试函数test_me()。

 

在main函数中生成两个随机整数t1和t2作为test_me函数的初始实例输入。
调用test_me函数后,在程序具体执行的同时启动符号执行引擎,对test_me函数的输入参数x、y创建符号变量x、y。
具体执行路径上的每条指令均需同时进行符号化执行处理。

变量z在具体执行中经过计算被赋值为72,在符号执行引擎中被赋值为2* x,程序继续执行。

 

在分支条件if(z == y)处,
由于具体执行中,z=72,y=99,不满足判定条件,因而选择else分支,即执行到黑色箭头处;
符号执行引擎也按照该路径执行,初始用例对应的执行路径遍历完成。
整个执行路径上只有一个条件分支语句if(z==y),其中的谓词语句z==y在符号执行过程中表示为2* x==y,真值为true。
根据符号执行引擎分析结果生成路径对应的路径约束条件pc=¬(2* x–y = 0)。

在获取执行路径上的路径约束后,使用该路径约束完成对程序执行树的遍历,包含以下步骤:
动态执行程序提取路径约束pc0。
使用路径搜索算法,按搜索策略对路径中的约束条件进行取反。
假设此处对b进行取反,生成新的路径约束条件pcb。
使用约束求解器对pcb进行求解并生成新的测试用例testb。
使用testb对程序进行测试,引导程序执行b所在条件语句的else(或then)分支,实现对b所在条件分支的完全遍历,并进入新的子树区域进行探索。

 

路径约束条件为pc0 = ¬(2 * x – y = 0) ,因为pc0中只有一个约束条件,所以不需要使用路径搜索算法,直接对其中的约束条件取反,得到新的路径约束条件表达式pc1 = (2 * x – y == 0),使用约束求解器求解pc1得到一组可行解为x = 1, y = 2,使用该用例即可对if(y == x + 10)的then分支进行遍历。
当路径条件中包含多个约束表达式时,例如pc0 = q1 ∧ q2 ∧ … ∧ qn,则需要按照一定的策略,每次选取部分约束进行变形。
策略的选取决定了动态符号执行遍历程序所有执行路径的效率。

在具体执行中,路径表达式pc1可能无解。
这种情况或者说明该路径不可达。
或者程序在执行到q3前即进入了其他分支,未按照预定的路径执行。
或者,最坏的情况是对程序的测试进入循环结构,适中存在未完全遍历的路径分支,导致测试过程无法终止。
因此,无论是使用深度搜索算法还是其他路径遍历算法,均需要辅以其他方法指引测试遍历未覆盖过的路径和代码块,才能保证测试的高效。

动态符号执行存在的非线性约束求解问题

假设约束求解器只能求解线性约束。
示例程序

void foo(int x, int y) {
    int z = x * x * x;    /* could be z = h(x) */
    if(z == y) {
        abort();                /* error */
}

假设初始值为x = 3和y = 7,程序执行到代码z = x * x * x处时,z的实际值为27,符号表示为z = x * x * x,此处出现了非线性表示。
若变量z出现在程序的路径约束条件表达式中,求解器无法对该路径约束条件进行求解。

可以采用折中方法。
既然z已经超出了分析范围,则使用具体值来替换符号值,即,在符号执行中将z的表示从x * x * x替换为27,然后继续后面的指令。
因为y != z,所以该轮执行完成,符号执行引擎提取的路径约束条件为pc0 = ¬(27 == y)。
使用搜索策略对路径约束取反得到pc1 = (27 == y),求解路径约束表达式。
下一轮执行的程序输入即为x = 3和y = 27,用例对应的执行过程进入if(z == y)分支并触发程序异常。

并行符号执行技术

当符号执行引擎测试大规模程序时,通常会遇到路径爆炸问题,导致测试时间超出测试周期可接受的范围。
同时,路径保证问题还会导致测试节点的内存使用接近满负荷状态,很多情况下符号执行引擎无法继续运行均是因为内存不足导致的。
因此,并行符号执行技术试图通过计算集群可无穷扩展的内存空间和CPU资源来缓解符号执行中的路径爆炸问题。

如何有效避免各节点对程序执行路径的重复搜索是并行系统需要解决的首要问题。

虽然一个程序的执行树在测试开始前是未知的,但对于传统的单节点符号执行引擎来说影响较小。

这是因为每次只需对一个路径进行探索,通过简单的标记等辅助手段即可避免对路径的重复探索。

然而,对并行系统来说,执行树未知的影响是巨大的。
这是因为同一时刻各工作节点都在对路径进行探索,且受限于带宽等因素,各节点很难实时共享已探索路径的信息,因而很容易造成不同节点对同一路径的冗余探索,影响系统的运行效率。

分布式环境下的负载均衡策略

如何划分任务集合,使得各工作节点避免出现负载不均衡的情况是所有并行系统都需要解决的核心问题。
假设并行系统能够将程序执行树动态划分成多个互不重合的子树,并保证没有冗余的路径探索行为,并行系统很难保证这些子树的规模是均衡的。

部分工作节点可能很快会进入空闲等待状态,而部分子树可能仍需要花费大量时间才能完成测试,这样的并行系统对效率的提升是有限的。
因此,并行系统需要路径搜索算法和负载均衡算法相配合,在减少节点间冗余工作的同时,尽量保证各节点的负载均衡。

选择符号执行技术

静态、动态以及并行符号执行技术均需要对测试程序的所有执行路径进行遍历,即,探索执行树中的所有分支节点和叶子节点,以保证测试结果的完整性。
然而,对于规模庞大的测试程序,完全遍历其执行树的时间代价极高。
研究发现,在一个程序的执行树中,通常不是所有路径对于测试人员均有分析价值,可能只有部分子树或子路径与测试目标相一致。

选择符号执行技术的基本思想

当测试程序执行到感兴趣的程序段时,对代码进行符号执行分析,即,对遇到的所有路径分支进行分析。
当程序执行到不感兴趣的外部调用函数段时,对代码进行单路径具体执行。
代表性的选择符号执行工具有S2E等。

S2E的分析过程

 

对程序app的分析由单路径具体执行模式(concrete)开始自上而下执行。
当进入目标测试代码lib库函数时,S2E使得测试进入多路径符号执行模式(symbolic)。
当程序运行离开lib库函数代码段进入内核Kernel代码段时,S2E再次使测试切换到单路径模式。

测试过程中的模式转换分为两种类型
具体执行至符号执行的转换
符号执行到具体执行的转换

具体执行至符号执行的转换

当appFn调用libFn时,需要进行具体执行到符号执行的模式切换,将libFn函数的输入参数从具体值转换为符号值,即,libFn(10)转换成libFn(a),转换时可对符号变量添加约束条件。
当转换发生后,S2E对libFn函数同时进行符号执行和具体执行(libFn(10))。
当符号执行完成对libFn函数所有路径的遍历后,S2E使用具体执行libFn函数得到的具体值作为函数返回值跳转回appFn函数。
从而,在完成对libFn函数路径遍历的同时,并未影响程序原本的执行状态。

符号执行到具体执行的转换

从libFn到sysFn的过程需要进行符号执行到具体执行的转换,过程相对复杂。
libFn函数的代码,且输入参数x在函数入口处没有任何约束条件(x ∊ (-∞, +∞))。

 

当程序执行到第1个if条件分支对应的指令时,符号执行引擎从当前搜索状态分裂成两个状态,分别表示对if语句两个分支的探索:一个为输入参数a添加约束条件x ∊ (-∞, 5),另一个添加约束条件x ∊ [5, +∞)。
符号执行引擎继续探索libFn函数的执行路径,进入if语句的then分支后,会调用sysFn(x)函数,sysFn函数为非目标代码段,需要对x变量进行实例化,并将测试模式从符号执行切换到具体执行。
根据x的约束条件x ∊ (-∞, 5),选择一个具体值,例如x = 4,并执行sysFn(4),同时为if分支添加新的约束条件x = 4。

  • 12
    点赞
  • 68
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值