KLEE软件分析


KLEE软件分析

原文地址:https://github.com/chyyuu/symexe/blob/master/klee_document/klee_analysis.md

1 klee概述

1.1 符号执行概述

符号执行技术使用符号值代替数字值执行程序,得到的变量的值是由输入变 量的符号值和常量组成的表达式。符号执行技术首先由King在1976年提出 ,经过三十多年的发展,现在仍然被广泛研究,它在软件测试和程序验证中发挥着重 要作用。符号执行是一种重要的形式化方法和静态分析技术,它使用数学和逻辑 首先定义一些基本概念。程序的路径(path)是程序的一个语句序列,这个 语句序列包括程序的一些顺序的代码片段,代码片段之间的连接是由于分支语句 导致的控制转移。一个路径是可行的(feasible),是指存在程序输入变量的至少一组值,如果以这组值作为输入,程序将沿着这条路径执行。否则,路径就是不 可行的(infeasible)。路径条件(path condition,PC)是针对一个路径的,它是一 个关于程序输入变量的符号值的约束,一组输入值使得程序沿着这条路径执行当 且仅当这组输入值满足这条路径的路径条件。符号执行系统的整体结构如下图图所示。

Test 图 符号执行系统整体结构

1.1.1 符号执行原理

符号执行是指在不执行实际程序的前提下,把源程序翻译为一种中间语言,用符号值表示程序变量的值,然后基于中间语言模拟程序执行来进行相关分析的技术,它可以分析代码的所有语义信息,也可以只分析部分语义信息(如只分析“内存是否释放”这一部分的语义信息)。 符号执行通常可表示为三元组的形式:<指令指针,路径函数,路径条件>。指令指针指出了当前被分析的指令;路径函数表示程序路径中不同位置处变量的值(可以是符号值,也可以是常量,或二者形成的函数),可用函数映射表示;路径条件是路径在各个分支执行点(位置)的分支条件的合取逻辑表示,可用布尔公式表示。

符号执行分为过程内分析和过程间分析(又称全局分析)。过程内分析是指只对单个过程的代码进行分析,全局分析指对整个软件代码进行上下文敏感的分析。所谓上下文敏感分析是指在当前函数入口点要考虑当前的函数间调用信息和环境信息等。程序的全局分析是在过程内分析的基础上进行的,但过程内分析中包含了函数调用时就也引入了过程间分析,因此两者之间是相对独立又相互依赖的关系。

如果要进行源代码的安全性检测,则需要在过程内分析时,根据具体的规则知识库来添加安全约束。例如,如果要添加缓冲区溢出的安全约束,则在执行时遇到对内存进行操作的语句时,就要对该语句所操作的内存对象的边界添加安全约束。以上面的方式来进行安全约束的添加,并且每次在添加之后就使用约束求解器对所有的安全约束进行求解,以判定当前是否可能潜在一个安全问题。

在程序全局分析处理过程中,首先需要为整个程序代码构建函数调用图。在函数调用图中,节点表示函数,边表示函数间的调用关系。根据预设的全局分析调度策略,对CG中的每个节点(对应一个函数)进行过程内分析,最终给出函数调用图每种可行的调用序列的分析结果。

动态符号执行是以具体数值作为输入来模拟执行程序代码,与传统静态符号执行相比,其输入值的表示形式不同。动态符号执行使用具体值作为输入,同时启动模拟执行器。模拟器可基于不同层次的,比如用户态级别,只支持printf, write, read等基本IO函数;也可是全系统级别的,可以支持应用程序和操作系统在模拟器上执行。然后,并从当前路径的分支语句的谓词中搜集所有符号约束。然后修改该符号约束内容构造出一条新的可行的路径约束,并用约束求解器求解出一个可行的新的具体输入,接着符号执行引擎对新输入值进行一轮新的分析。通过使用这种输入迭代产生变种输入的方法,理论上所有可行的路径都可以被计算并分析一遍。

动态符号执行相对于静态符号执行的优点是每次都是具体输入的执行,在模拟执行这个过程中,符号化的模拟执行比具体化的模拟执行的花销大很多;并且模拟执行过程中所有的变量都为具体值,而不必使用复杂的数据结构来表达符号值,使得模拟执行的花销进一步减少。但是动态符号执行的结果是对程序的所有路径的一个“下逼近”,即其最后产生路径集合应该比所有路径集合小,但这种情况在软件测试中是允许的。

1.1.2 符号执行的关键技术
路径状态空间爆炸

符号执行技术在理论上面临着路径状态空间的爆炸问题,其主要形成原因是每一个分支条件语句都可能会使当前的路径再分支出一条新的路径,而这是“指数级”增长的。相对于静态符号执行在分支语句时都是符号值的约束,动态符号执行在分支语句时则都是具体值的约束。动态符号执行的当前路径执行过程中不会产生新路径,只在执行结束后再去产生新路径,且一次只产生一条。但是它们都没法从根本上避免路径状态空间爆炸所造成的影响。

在符号执行的具体实现中,可使用限定每个过程内的分析路径数目上限的方法来缓解该问题所产生的影响。也可以使用设置时间上限或者内存空间上限的方法来缓解路径爆炸问题所可能造成分析工具崩溃的影响。另一方面,符号执行工具的实现者总是希望能设计出好的路径遍历策略,以在有限的时间和空间范围内达到最大的代码检测覆盖率。人们通过改进路径调度算法来提高符号执行的分析性能,但是这些路径调度算法都只是局部改进,很难从根本上解决这一问题。

复杂结构语义和操作语义的建模

符号执行实现时需要对被分析代码的结构语义进行建模,然后再对被分析代码的操作语义进行建模,最后构建一个虚拟机模型。由于符号执行是路径敏感的分析方法,因此一般为每条路径都会创建一个专属的虚拟机模型,以保证路径之间的相互独立性。该模型的准确程度将直接影响静态分析结果的精度。由于编程语言中使用的复杂数据结构和复杂操作语句具有较高的灵活性,使得它们的建模变得十分困难。

人们提出了一种“惰性初始化”的方法,其思想是为每个数据结构(特别是复杂数据结构)建模时,在声明或者定义时只为其构建类型信息,直到被使用的时候,才根据使用的需要来初始化该变量的对象信息。

程序全局分析

在程序全局分析过程中,当对一个规模较大、包含很多的过程间调用的程序进行上下文敏感的分析时,每当一个过程调用了另一个过程时都进入子过程进行分析,虽然会很精确,但这种方式可能会造成大量的时间空间花销,而使分析过程异常中止或在用户可接受的时间内无法完成。 一种比较好的全局分析方法叫做“函数摘要”。函数摘要的方法是在过程内分析的基础上对已分析过的函数进行一个摘要记录的操作。在以后的分析中遇到调用其他函数时,如果已存在被调用函数的摘要,则直接调用该函数的摘要并对该摘要行为进行解释;如果不存在被调用函数的摘要信息,则进入被调用函数进行分析,并在分析之后进行摘要保存。函数摘要是一种相对折中的办法,所创建的函数摘要也可能很不准确。另一个问题是对无法获得源代码的第三方库函数的摘要只能通过人工的编写来完成,这也可能是不精确的,而这两点都会影响到最终的分析结果的精度。

对于函数摘要的构建策略也是一个研究内容,使用在函数调用图上的自顶向下的需求驱动策略来为所有子过程创建函数摘要。由于循环语句可能会使被摘要函数的路径空间爆炸,而使得无法对函数的所有路径都进行分析摘要,该文章介绍了使用循环不变量来进行函数摘要以解决循环语句所引入的问题。

1.1.2 符号执行实现概述

初始化指令指针执行程序入口点,初始化的路径函数一般是恒等函数,初始化的路径条件为真。在符号执行中需要更新指令指针,路径函数,路径条件:

  • 指令指针通常从指向当前指令转到指向其后续指令或指向jmp/call的目的地址处的指令
  • 路径函数一般通过复合路径上每条指令的语义函数(如操作语义或指称语义)得到更新
  • 路径函数的更新保证了可获取到路径中每个分支的分支条件,通过合取逻辑操作来合并路径中每条分支的分支条件

比如: 变量a,b执行如下指令语句

1 if(a<10)
2    assert(a<10);
3 else if(b<10)
4    assert(a<10 && b<10);
5 else assert (a>10 && b>10);
6 end

则符号执行的表示如下:

value: (0,0) path:1-2-6 path_condition (a<10)

新的路径条件 !(a<10),用求解器可得一个具体值 a=10

value: (10,0) path:1-3-4-6 path_condition !(a<10)&&(b<10)

新的路径条件 !(a<10)&&!(b<10),用求解器可得一个具体值 a=10, b=10

value: (10,10) path:1-3-5-6 path_condition !(a<10)&&!(b<10)

在执行每条指令时,对于非分支指令,可通过函数的复合操作来更新路径函数。对于分支指令,由于要执行一条新路径,所以需要创建新的进程,并将原路径条件与美国分支目标满足的分支条件执行合取逻辑操作,形成新的路径条件。当某符号执行进程结束时,可调用约束求解器求解路径条件以获取可导致程序执行该路径的具体输入值的集合。当所有符号执行的进程都结束了,则整个符号执行过程结束。

1.2 klee介绍

KLEE是一个符号执行工具,为复杂系统软件生成的测试数据可以达到很高 的覆盖率。同时,KLEE是一个错误检查工具,除了常规的编码错误,也可以检查 功能错误。KLEE使用约束求解器STP,扮演着处理符号进程的操作系统和解释 器的双重角色。KLEE把内存视为多个无类型字节数组,为每一个数据对象生成一 个单独的字节数组。无类型字节数组使得KLEE能够精确的处理类型不安全的内存 访问。KLEE要求数据对象有具体的大小,所以它不能支持大小不确定数据。KLEE 不能直接支持指针,它处理指针的方法是通过一种插桩方法确定指针指向的对 象。对于指针解引用,首先通过一个记录变量到其字节数组的映射表找到指针指 向的对象对应的字节数组,然后计算指针相对这个字节数组的偏移量,最后把偏 移量作为下标访问数组的元素。路径条件是关于字节数组的约束,对数组进行推 理是STP的关键。STP通过对数组推理进行优化保证了它的性能。

1.3 llvm介绍

LLVM(Low-Level Virtual Machine)利用其代码表示和编译器框架设计, 以对程序员透明的方式,支持任意程序的全程(lifelong)分析和转换。LLVM的 全程代码优化包括:连接时执行的过程间优化、软件安装时的目标机器相关优化、 运行时执行的动态优化以及在程序不运行的空闲时间执行的剖面导向的 (profile-guided)优化。全程分析和转换不仅可以用来优化程序,还可以用来做过 程间静态分析,比如静态调试、内存泄漏检测等。另外,全程分析和转换也可以 用来做可靠安全性质检查等。

LLVM的全程分析和转换基于LLVM的两个关键部分:代码表示和LLVM编译 器框架。LLVM编译器框架利用代码表示提供了支持全程分析和转换的五种能 力:

  • 一致的程序信息:LLVM编译器在软件整个生存期一直保存着LLVM汇编 代码,这使得可以在任何时期对代码进行分析和优化。
  • 编译型代码生成:对于性能关键的程序,可以把程序直接编译为本地机器代 码。通过使用复杂的代码生成技术,可以保证生成的本地机器代码的高效 性。
  • 基于用户的剖面(profiling)和优化:LLVM在运行时向用户收集剖面信 息,并把这些信息应用到基于剖面的转换中。这样,软件就能反映用户的 具体使用习惯。
  • 透明的运行时模型:LLVM不指定对象模型、异常语义或者运行时环境, 这使得LLVM能编译支持任何语言的程序的分析和转换。
  • 统一的全程序优化:语言独立性使得LLVM可以以统一的方式编译和优化 程序的所有代码,包括系统库和语言特定的运行时库。

LLVM的代码表示的特点是,提供支持复杂分析和转换的程序高级信息,比如 类型,同时又可以表示任意程序,并支持各种优化。LLVM的指令集抽象了一般处 理器的关键操作,同时避免了机器特定的部分,比如物理寄存器、流水线等。 LLVM指令集比较简单,只有31个操作码,多数指令是三地址码的形式。LLVM 包含无数多个有类型的虚拟寄存器,这些寄存器可以保存基本类型的数据(布尔、 整数、浮点数和指针)。寄存器在LLVM汇编代码中是SSA(Static Single Assignment) 形式。内存位置不是SSA形式,原因是,一个指针可能指向多个内存位置。LLVM 汇编代码中,函数的CFG(Control Flow Graph)已经被构造出来,这一点十分有 用,因为很多静态分析依赖于函数的CFG,包括符号执行。这里CFG的节点是基本 块(basic block),而不是单个语句。LLVM汇编代码中的数据有确定的类型,它 提供一个语言独立的类型系统,包括四个简单类型:void、布尔、整数和浮点数, 以及四个导出类型:指针、数组、结构和函数。这个简单的类型系统可以实现绝 大多数高级语言的类型,比如,C++中的类可以使用结构、函数以及函数指针的数 组的组合实现。指令getelementptr用来实现指针算术,它可以计算组合类型(结 构或者数组)数据的成员的地址。getelementptr使得在LLVM汇编代码中附带类型 信息成为可能。

在LLVM的汇编代码中,所有的堆数据使用malloc指令分配内存,返回有类型 指针。所有栈中的数据使用alloca指令分配内存,并返回有类型指针。全局变量和 函数定义定义了一个执行相应对象的地址。这样,所有的可寻址对象都有指向它 的指针,所有的内存操作,包括函数调用,都是通过指针进行执行。在内存和 寄存器之间进行数据传递是通过load和store指令来做的。LLVM的代码表示中的另 一个创新点是使用invoke和unwind两个指令实现了对高级语言中的异常处理的支 持。这种机制支持一种抽象异常处理模型,机器独立。

2 klee设计与实现

2.1 klee总体结构

2.1.1 总体架构
2.1.2关键功能单元

2.2 klee的核心技术

2.2.1 解释执行

解释器执行代码操作的符号语义,

2.2.2 执行状态
2.2.3 内存模型

内存模型的作用是跟踪符号状态。为了支持一些复杂的语法成分,比如类型不安全的内存访问(比如,C代码中常常出现的类型转换),内存模型需要足够精确。内存模型处理数据的方式与数据的类型和生存期密切相关。与其他静态分析一样,符号执行也需要面对指针和别名问题。同时,符号执行必须处理一些特殊问题,比如函数的参数是包含无界数据的复杂数据结构。当程序规模较大时,需要记录大量的符号状态,数据读写的时间消耗也不可忽视。这时,内存模型的性能(可扩展 性)成为制约其实用性的关键。鉴于这些问题,设计一种精确、具有一定的可扩 展性(calability)的内存模型,支持各种数据类型和语法成分,是推动符号执行深 入、广泛应用的关键。

底层系统软件包括软件开发中使用的编译器、设备驱动程序、操作系统等, 它们是其他软件的基础,在整个软件系统中发挥着重要的作用。系统软件对正确 性有着更高的要求,因此,它们是形式化验证的主要目标。系统代码的一个特点 是代码中经常出现类型不安全的内存访问(type-unsafe memory access)。类型不 安全的内存访问主要包括类型转换和指针算术。比如,把一个数据结构转换为字 节数组。指针类型转换是一个典型情况。系统代码常常把内存看作无类型的字节 序列,通过指针类型转换以不同的方式读写一个内存位置。指针算术通过一个指 针与整数之和计算临近的数据对象的地址。比如,C语言规定的指针算术针对以指 针方式引用数组元素的场景,要求运算前的指针以及运算结果产生的指针指向数 组内的元素或者数组最后一个元素之后的一个对象。

为了能够分析和验证底层系统软件,内存模型必须能够精确描述类型不安全 的内存访问所带来的影响。类型不安全的内存访问的背后原因是系统代码把内存 视为无类型的字节数组,因此内存块在字节粒度可区分是精确的内存模型的关键。

一个极端是把内存空间视为单一的一个字节数组。很明显,这种内存模 型是完全精确的,它能够精确的表示所有的内存访问。但是,它的可扩展性很差。 随着硬件价格的降低,现代计算机系统的可用内存很大(一个普通微机的内存往 往是2G),程序的内存消耗也十分巨大。因此,精确的表示每一个内存位置的分 配和访问代价很高。字节数组内存模型对即使很小的代码段也很难胜任。另一 些内存模型则假设代码没有动态内存分配并且是类型安全的,这种内存模型可 以成功处理数十万行代码,但是不能精确分析系统软件。

单体内存模型

单体内存模型(monolithic memory model)在系统软件的静态分析中应用广 泛,KLEE的内存模型本质上就是单体内存模型。单体内存模型的基本 思想是把内存空间划分为不相交的区域(region),通过相应对象的程序符号引用 识别区域。区域是具有相应数据对象大小的字节数组。指针表示为一个二元组, 包括对象引用和字节偏移量,对象引用表示指针指向的区域,字节偏移量是指针 指向的位置相对于区域开始处的字节偏移量。单体内存模型有三种数据类型:整 数int、对象引用ref以及指针ptr=ref*int。单体内存模型的语义由三个映射组 成。

  • Mem: ptr->{ptr,int}
  • Alloc: ref->{allocated, unallocated}
  • Size: ref->int

Mem是表示内存位置所存储的值,Alloc建模动态内存分配,Size记录对象的大小。可见,通过区域内的字节偏移量,单体内存模型可以表示类型不安全的内存访问。

 Burstall内存模型

Burstall内存模型是在类型安全代码的分析中广泛使用的内存模型。它的基本思想是不但把内存划分为不相交的区域,而且使用数据的类型区分不同的内存位置。Burstall内存模型的语义如下所示。

  • Mem:ptr→value
  • Alloc:ref→{allocated,unallocated}
  • Size:ref→int
  • Type:ptr→type

其中,type类型定义一些类型常量, 类型常量表示某一个内存位置的类型。比如一个整数指针对应的内存位置的类型 值为$intP。映射Men表示内存位置存储的值,通过指针和类型对标识内存位置。 映射Type表示一个内存位置的类型。Alloc和Size同单片内存模型。Burstall内存模 型中的类型信息使得它具有较好的可扩展性,但是,它不够精确,不能精确分析 包含类型不安全的内存访问的系统软件。

数据类型

数据类型定义了一组数据对象以及创建和操纵它们的操作集合。一个数据 类型的基本组成部分包括:

  • 属性:区别于其他数据类型的性质,比如数组的维数等;
  • 值:这个类型的数据对象的所有可能的取值;
  • 操作:作用于这个类型的数据对象的操作,比如以下标访问数组元素等。编程语言实现数据类型需要定义相应数据对象的存储和操作的实现,包括在程序执行过程中相应数据对象在计算机存储器中的存储表示和根据特定于存储表示的算法或过程定义操作的实现。

常见的数据类型有标量数据类型、指针、结构化数据类型和抽象的数据类型等。标量数据类型包括枚举类型、字符型、数字类型和布尔类型。结构化数据对象由其他对象集合而成,这些对象称为成员。典型的结构化数据类型有数组、结构等。抽象的数据类型有面向对象编程中的类等。

数据的生存期

数据的生存期(lifetime)是指程序运行时为数据对象分配存储空间的时期。数据的生存期分三种:静态生存期、局部生存期和动态生存期。静态生存期指生存期从程序开始执行一直持续到程序结束。在C程序中,所有的外部变量以及定义中有static关键字的局部变量都有静态生存期。局部生存期指生存期从块或者函数的进入持续到块或者函数的退出。形式参数、多数局部变量都有局部生存期,这样的局部变量被称为自动变量。动态生存期指生存期由程序员控制。拥有动态生存期的动态数据对象往往通过特定的库函数,比如C中的malloc等产生。

内存模型功能

符号执行系统的解释器执行代码的符号语义,根 据计算结果更新内存模型中的符号状态,并在分支语句处调用约束求解器判定约 束公式。约束求解器是针对某种理论(比如一阶逻辑公式形式的包含等词的整数 线性算术)的定理证明器,作用是判定约束公式的可满足性。内存模型的主要作 用是跟踪符号状态。内存模型的功能包括以下几个方面:

  • 跟踪符号状态;
  • 定义内存操作的语义;
  • 处理内存模型特定的约束公式,以方便约束公式的判定。

根据前面的定义,符号状态是一个三元组,IP跟踪程序的执行, 关于PC要解决的工作是是化简和求解,这是约束求解器的职责。M是变量到其符号值 的映射,是内存模型的基础和关键部分。M需要直接或间接地实现两个映射:Φ 和Ψ,如图3.2所示。Φ表示为可寻址对象分配的内存位置,Ψ表示内存位置上存 储的符号值。其中,Denotation表示可寻址对象的程序符号的集合,Location表示 内存位置的集合,SymbolicValue表示符号表达式的集合。

要在静态分析中使用一个内存模型,必须为编程语言定义针对这个内存模型 的内存操作语义。具体包括内存模型直接支持的数据类型(往往是编程语言的数 据类型的一个很小的子集)、如何进行数据对象的读写操作、如何建模动态内存 分配(比如C语言中的malloc()函数),以及内存操作相关的其他语法成分的语义。 比如,单体内存模型定义的动态内存分配的语义。

另外,内存模型需要处理内存模型特定的约束公式,以方便约束公式的判定。 路径条件来自路径上的分支条件,通过把变量替换为相应的表达式,从分支条件 直接得到的路径条件与内存模型是密切相关的。符号执行系统可把内存模型 和约束求解器紧密耦合在一起,也可把他们分离开来,由内存模型负责路径条 件的预处理。这样,经过预处理的约束公式是某种标准形式, 就可以使用通用的约束求解器来判定。单体内存模型(monolithic memory model)把指针表示为一个二元组, 则两个指针相等的谓词就变成了两个二元组的相等,不能处理向量的约束求解器 就不能判定这样的约束公式。KLEE把内存空间视为多个字节数组,因此,它使 用的约束求解器STP必须能够高效的求解数组约束。

指针和别名

指针分析是指静态地确定 指针的运行时取值,别名分析是指判断同一存储位置的多种访问方式。从指针分 析的结果中可以得到别名信息,同样,从别名分析的结果中也可以得到一些指针 信息。

指针分析算法根据其目不同可以分为两种。第一种的目的是处理一般的、 比较简单的指针。这些算法是域不敏感的,也就是不区分结构化类型数据中的不 同成员——例如结构中的域。此类别的一些早期的算法还限制指针必须为单级指 针。第二种算法则集中于处理递归结构,例如链表、树等,出现了许多处理C 语言程序的此类算法。 别名分析算法可以划分为不同的精确度,精确度的标准有流不敏感算法、流 敏感算法、上下文不敏感算法、上下文敏感算法、路径不敏感算法和路径敏感算 法等。其中,流不敏感算法的精确性最低,而路径敏感算法的精确度最高。一般 而言,随着精确度的提高,算法的计算代价随之升高,可扩展性则随之降低。 流不敏感别名分析算法为整个程序只生成一个别名分析结果,即内存位置之 间的指向图,用以表示所有可能的别名关系。比较著名的流不敏感算法的基本思路是为每个 指针表达式分配一个类型,代表该表达式所有可能指向的内存位置。为表达式分 配类型的过程就是类型推理(type inferencing)过程。对指针表达式赋值语句的处 理方法是,为该语句左端表达式的类型和右端表达式的类型生成一个约束条件, 其含义是语句执行之后,左端表达式与右端表达式指向同一个内存位置。通过求 解约束,即可得到别名分析结果。

流敏感的别名分析算法可以使用传统的数据流分析框架来表示。在每个程序 点,可以计算出多种形式的数据流事实。在处理指针赋值语句时,通常先计算出 该语句消除了哪些别名信息,然后再计算出它生成的新的别名信息。在汇合结点 处,做别名信息的合并操作。上下文敏感指针分析算法同上下文不敏感算法相比,需要消除别名信息在过 程间不可行路径上的传播。主要有两种方法。第一种方法为别名信息标记上下文 信息,上下文标记用来区分在不同调用上下文中生成的别名信息。第二种方法是,在不同的上下文环境下,对过程的每一个调用,采用宏扩展方式进行相互独立的分析。这样,一个过程可能会被分析 多次,导致了性能代价。

指针和别名问题也是符号执行必须面对和解决的问题。指针和别名分析算法 代价较高,尤其是精确性高的算法。多数符号执行系统不支持指针,出于性能方 面的考虑,往往使用十分简单的算法(比如流不敏感算法)处理指针和别名问 题。使用流不敏感的指针和别名分析结果做路径敏感的符号执行,似乎存在 悖论。虽然通过使用各种近似方法可以得出有意义的结果,但这确实严重影响了 符号执行的精确性。

指针和别名分析只关心数据之间的指向关系,而忽略变量的具体值和内存地 址,而符号执行本身就需要精确跟踪内存状态,包括变量的值。所以,符号执 行可以使用更精确的方法处理指针和别名问题,而不仅仅是使用现有的指针和别 名分析算法。

大小不确定的数据

这里的大小不确定数据是指无界复杂数据和大小是符号值的动态分配的数 据。无界复杂数据包括链表(list)、树(tree)等,是Java、C++等现代编程语言 的一个高级结构,在代码中经常出现。符号执行必须面对以这些数据作为输入的 函数所造成的困难。惰性初始化算法可以处理无界复杂数据,但是惰性初 始化算法是一种数据初始化方法,符号执行中的内存模型必须为无界复杂数据和 相应的处理算法提供支持。

一些编程语言(C、C++等)的动态内存分配机制为灵活地进行程序设计提供 了方便。动态内存的分配与使用是在程序代码中按照算法的要求,由程序开发人 员控制的。但是,动态内存分配却给符号执行造成了困难。考虑这么一个C语句: int*p=malloc(n*sizeof(int)),其中n是依赖输入的符号值。这个语句动态分配的n 元整数数组的大小就是不确定的。内存模型必须能够支持大小是符号值的动态分 配的数据。

为了建模无界复杂数据和大小是符号值的动态分配的数据,内存模型必须允 许这些数据可能的动态收缩或扩展,而不能在这些数据生成时对其大小做任何假 设。把内存模型视为一个字节数组的方法不支持大小不确定数据,原因是如 果数据的大小不能确定,就不知道它需要的数组元素的个数,进而就不能确定下 一个数据在数组中的位置(对于大小是符号值的数据,会使得接下来的数据的索引 都是符号值)。单体内存模型也不支持大小不确定数据,因为它的每一个区 域都要求有确定的大小。

2.2.4 约束求解器

约束求解器判定约束公式

KLEE的约束求解器STP的基本方法是E把每个数据视为一个独立的字节数组, STP是一个针对无量词一阶逻辑公式形式的位向量和位向 量数组线性算术的判定过程。STP提供三种数据类型:布尔、位向量和位向量数组。 其中,位向量是固定长度的位序列。同时,STP支持位连接和位提取操作,以支持 KLEE的内存模型。STP支持所有的算术操作、位级布尔操作和关系操作等,代码 中的谓词被转换为对位向量的约束。通过一系列位级转换和针对位向量和数组的 优化,位向量约束被转换为CNF范式形式的命题逻辑公式,再提交给一个标准SAT 求解器判定。

  • 3
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值