程序分析-klee工具分析

2023-10-31

一.klee介绍

1.1.简单介绍

Klee是一个LLVM IR符号执行工具(OSDI 08 Paper地址),能够自动生成测试,实现对各种复杂且环境密集型程序的高覆盖率

klee有2个目标:

  • 命中目标程序中的每一行代码

  • 检测到每一个危险操作(dereference, assertion等等,如果任何的输入能够触发此危险操作)

当klee检测到错误或路径到达 exit 调用时,klee求解当前路径的约束来生成一个测试用例,当在同样的程序上重新运行时,该测试用例将执行相同的路径。

klee的核心是1个循环,每轮循环中klee会从state列表中选出1个state并执行处于该state上下文中的1个指令(参考ExecutionState.h,klee在每个状态都会记录将要执行的指令和已经执行的指令),这个循环会持续到列表中不再有state或者用户设定的时间上限达到了。循环大致伪代码如下:

initalState = create();
stateList.add(initialState);

while (!stateList.empty() && !haltExecution){
    state = selectState(stateList); // 根据策略从列表选择状态
    executeState(state); // 符号执行,可能会fork新的state
    updateStateList(stateList); // 更新状态列表
}
  • 当状态列表不为空或者终止条件(超时、内存超出等)没达到时循环继续。

  • selectState 的实现根据不同的搜索策略(DFS、BFS、RSS等)有不同的实现方式。

与普通进程不同,状态(寄存器、堆栈和堆对象)的存储位置引用表达式(树)而不是原始数据值。

大部分指令的符号执行是简洁明了的,比如 %dst = add i32 %src0, %src1,klee从寄存器 %src0%src1 中获取加数并将 Add(%src0, %src1 写入寄存器 %dst。为了提高效率,构建表达式的代码检查所有给定操作数是否都是具体的(即常量,符号变量越少效率越高),如果是,则以本机方式执行操作,返回常量表达式。

条件分支是1个布尔表达式(分支条件),并根据条件是true 还是 false 更改状态的指令指针。

1.2.klee架构

1.2.1.基础架构

KLEE查询约束解算器以确定分支条件沿当前路径是可证明为真还是可证明为假;如果是,指令指针将更新到适当的位置。否则,两个分支都是可能的:KLEE克隆(fork)state,以便它可以探索两条路径,适当更新每条路径上的指令指针和路径条件。

潜在危险的操作隐式生成分支,检查是否存在可能导致错误的输入值。例如,除法指令生成检查零除数的分支。这些分支的工作原理与普通分支相同。因此,即使检查成功(即检测到错误),也会在错误路径上继续执行,这会将检查的否定添加为约束(例如,使除数不为零)。如果检测到错误,KLEE会生成一个测试用例来触发错误并终止状态。

与其他危险操作一样,loadstore 指令生成check:检查被测地址是否在有效内存对象的边界内。然而,loadstore 操作带来了额外的复杂性。被测代码使用的内存最直接的表示形式是一个平面字节数组。在这种情况下,loadstore 将分别映射到数组读取和写入表达式。不幸的是,约束解算器STP几乎永远无法求解结果约束(我们所知道的其他约束解算程序也无法解算)。KLEE将被测代码中的每个内存对象映射到不同的STP数组(从某种意义上说,将平面地址空间映射到分段地址空间)。这种表示法极大地提高了性能,因为它允许STP忽略给定表达式未引用的所有数组。

许多操作(如bound checks或object-level copy-on-write)需要特定于对象的信息。如果一个指针可以引用许多对象,那么这些操作就很难执行。为了简单起见,KLEE回避了这个问题,如下所示。当dereferenced pointer p 可以引用 N 个对象时,KLEE会将当前状态克隆 N 次。在每个状态下,它将 p 限制在其各自对象的边界内,然后执行适当的读或写操作。虽然这种方法对于具有大points-to集合的指针来说代价高昂,但我们测试的大多数程序只使用指向单个对象的符号指针,并且KLEE针对这种情况进行了很好的优化。

1.2.2.Query优化

给定复杂的路径约束,在调用求解器(STP, Z3等)之前,klee会对约束进行一些简化,加快约束求解的过程,主要的优化包括:

  • 表达式简化: 最基本的优化反映了编译器中的优化:例如,简单的算术简化(x+0=0)、强度减少(x * pow(2, n)=x<<n)、线性简化(2*x-x=x)。

  • 约束集简化: 符号执行通常涉及向路径条件添加大量约束。程序的自然结构意味着对相同变量的约束会变得更加具体。比如前一个有 x < 10,后面添加了等于约束 x = 5 ,那么 x 的值会具体化为5,约束 x < 10 会被删除。

  • 隐含值具体化:当诸如 x + 1 = 10 这样的等于约束添加到路径约束中,x 的值被具体化(这里为9)并将具体值写入内存。

  • 约束独立性:许多约束在引用的内存方面没有重叠。约束独立性(取自EXE)根据约束集引用的符号变量将其划分为不相交的独立子集。通过显式跟踪这些子集,KLEE可以在向约束解算器发送查询之前经常消除不相关的约束。比如 {i < j, j < 20, k > 0} 中,遇到查询 i < 20,只会需要求解前2个约束。

  • 示例缓存:冗余查询非常频繁,简单的缓存可以有效地消除大量冗余查询。然而,由于约束集的特殊结构,可以构建更复杂的缓存。示例缓存将约束集映射到示例(即变量赋值),并在一组约束没有解决方案时使用一个特殊的哨兵。假设缓存目前含有约束 {i < 10, i = 10}(无解) 和 {i < 10, j = 8}(有解 i = 5, j = 8)。

    • 当约束集的子集没有解时,原始约束集也没有解。向不可满足的约束集添加约束不能使其满足。在上述缓存优化下,路径约束 {i < 10, i = 10, j = 12} 很快被确定为无解。

    • 当约束集的超集有解时,该解也满足原始约束集。从约束集中删除约束不会使该集合的解决方案无效。赋值 i → 5, j → 8 能满足约束 i < 10j = 8

    • 当约束集的子集有解时,这很可能也是该约束集的解。这是因为额外的约束通常不会使子集的解决方案无效。由于检查潜在解决方案的成本很低,KLEE尝试用所有解决方案替换约束集的子集,如果找到,则返回满意的解决方案。i = 5, j = 8 依旧是 {i < 10, j = 8, i != 3} 的一个解。

作者在coreutils上运行了5分钟,独立性优化能减少45%的运行时间,示例缓存能够减少40%的STP查询以及运行时间。当这2项优化开启后,STP查询的数量减小为原来的5%,平均运行时间减少了一个数量级以上。

在原始情况下,STP求解能占据92%的运行时间,开启2项优化后降低为41%。

1.2.3.状态调度

paper发出时klee采用下面2种状态选择策略:

  • Random Path Selection:该策略维护一个二叉树,记录所有活动状态所遵循的程序路径,即树的叶子是当前状态,内部节点是执行分叉的地方。该策略从root结点随机选择方向遍历到叶子结点。

  • 覆盖率优化的策略:尽可能选择能覆盖到新代码的状态,根据与未覆盖指令的最小距离、状态的调用栈深度、该状态是否最近覆盖新指令计算权重,然后根据权重随机选择状态。

执行一个指令所花费的时间可能与多个因素有关(指令本身、是否fork状态、是否调用约束求解器),为了不让一个耗时的state影响klee工作,klee运行每一个状态在1个time slice以内。

1.3.环境建模

当代码从其环境中读取值(命令行参数、环境变量、文件数据和元数据、网络数据包等)时,klee从概念上想返回读取可以合法生成的所有值,而不仅仅是单个具体值。当它写入其环境时,这些更改的影响应反映在后续读取中。

原理上,klee通过将访问环境的库调用重定向到环境模型来处理环境,这些模型能够很好地理解所需操作的语义,从而生成所需的约束。重要的是,这些模型是用普通的C代码编写的,用户可以轻松地定制、扩展甚至替换,而无需了解KLEE的内部原理。klee有大约2500行代码来定义大约40个系统调用的简单模型。(e.g., open, read, write, stat, lseek, ftruncate, ioctl).

对于文件读写,如果文件名是具体值,klee会调用OS提供的对应API进行读写,对于符号文件,读取时klee会从保存数据的符号缓冲区种中读取数据传给用户数组。

二.安装

官方给的安装教程(LLVM 11 + klee-2.3)

首先装klee前,系统得配置好其它的包(包括LLVM等)

  • 官方首先给了命令 sudo apt-get install build-essential cmake curl file g++-multilib gcc-multilib git libcap-dev libgoogle-perftools-dev libncurses5-dev libsqlite3-dev libtcmalloc-minimal4 python3-pip unzip graphviz doxygen ,很明显这么一大长串有包的系统已经装好了,因此可以从命令中移除,对我来说 libgoogle-perftools-devlibsqlite3-dev 包是还没装好的,因此我只装了这2个包,python环境我用的是anaconda的。

  • python包 lit, wllvm, tabulate

    • lit是用来测试用的,wllvm是方便将程序编译成bytecode,直接使用 pip install lit wllvm==1.0.17 安装。

    • tabulate包:官方给的命令是 sudo apt-get install python3-tabulate,我是使用 pip install tabulate 安装,这个包是在klee符号执行完毕后,使用klee-stats查看run.stats文件时使用的。

  • LLVM-11,官方给的安装命令是 sudo apt-get install clang-11 llvm-11 llvm-11-dev llvm-11-tools, 但是我这边ubuntu直接找不到 llvm-11,因此只能尝试其它安装方法

    • 最简单的方法就是从github上下载release(找到clang+llvm-11.0.0-x86_64-linux-gnu-ubuntu-20.04.tar.xz,我系统是ubuntu的) ,但是下载release版的有个小问题,就是LLVM_DIR/bin目录下少了ELF文件 FileChecknot,当然这并不影响LLVM的使用,但是编译klee的时候会报错。(这2个文件源码在目录llvm-project/llvm/utils目录下,分别有个FileChecknot目录)

    • 因此我只能尝试重新编译安装LLVM 11(花费2个小时,为了2个ELF文件这么干真的耗时,但奈何不知道如何只编译这2个文件),下载完LLVM 11源码后,命令行切到源码根目录下,cmake的时候添加 -DLLVM_BUILD_UTILS=ON 参数,编译完成之后可以在build/bin目录下找到FileCheck和not,但是这2个ELF在 make install 的时候不会自动复制到LLVM的安装目录下,需要手动从build目录下复制到安装目录下.

    • 完整的编译命令如下:mkdir build && cd buildcmake -G "Unix Makefiles" -DLLVM_ENABLE_PROJECTS='clang;clang-tools-extra;libcxx;libcxxabi;libunwind;lldb;compiler-rt;lld;polly' -DCMAKE_INSTALL_PREFIX=<LLVM_DIR> -DCMAKE_BUILD_TYPE=Release -DLLVM_ENABLE_ASSERTIONS=On -DLLVM_BUILD_UTILS=On ../llvmcmake --build . -j 4cmake --build . --target install

  • 从github上下载klee-ulibc的源码并编译,直接按github上的教程编译就行,不用设置额外参数。

  • 编译klee

    • mkdir build && cd build

    • cmake -DENABLE_POSIX_RUNTIME=ON -DENABLE_KLEE_UCLIBC=ON -DKLEE_UCLIBC_PATH=<KLEE_UCLIBC_DIR> -DCMAKE_INSTALL_PREFIX=<KLEE_DIR> ..<KLEE_DIR> 是klee安装目录,该目录下只有 bin, lib, include 3个文件夹,比 build 目录简单。<KLEE_UCLIBC_DIR>klee-uclibc 的源码目录,klee-uclibc 只需编译后就行不用install)

    • make -j4

    • make install

最终环境

工具 版本
LLVM 11.0.0
klee 2.3
wllvm 1.0.17

三.klee使用

参考官方示例

3.1.Testing a Small Function

get_sign.c

#include<klee/klee.h>

int get_sign(int x) {
  if (x == 0)
     return 0;
  
  if (x < 0)
     return -1;
  else 
     return 1;
} 

int main() {
  int a;
  klee_make_symbolic(&a, sizeof(a), "a");
  return get_sign(a);
}

用下面命令编译代码 clang -I $KLEE_DIR/include -emit-llvm -c get_sign.c(不要用clang优化)生成bc文件,用 klee get_sign.bc 测试,测试结束后会在当前目录生成klee-out-0文件夹,klee-out-0中包含若干文件:assembly.II(实际执行的LLVM代码),testxxxxx.ktest,info,run.stats,message.txt等

执行的LLVM IR(assembly.II)为:

; ModuleID = 'sample.bc'
source_filename = "sample.c"
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-unknown-linux-gnu"

@.str = private unnamed_addr constant [2 x i8] c"a\00", align 1

; Function Attrs: noinline nounwind optnone uwtable
define dso_local i32 @get_sign(i32 %x) #0 {
entry:
  %retval = alloca i32, align 4
  %x.addr = alloca i32, align 4
  store i32 %x, i32* %x.addr, align 4
  %0 = load i32, i32* %x.addr, align 4
  %cmp = icmp eq i32 %0, 0
  br i1 %cmp, label %if.then, label %if.end

if.then:                                          ; preds = %entry
  store i32 0, i32* %retval, align 4
  br label %return

if.end:                                           ; preds = %entry
  %1 = load i32, i32* %x.addr, align 4
  %cmp1 = icmp slt i32 %1, 0
  br i1 %cmp1, label %if.then2, label %if.else

if.then2:                                         ; preds = %if.end
  store i32 -1, i32* %retval, align 4
  br label %return

if.else:                                          ; preds = %if.end
  store i32 1, i32* %retval, align 4
  br label %return

return:                                           ; preds = %if.else, %if.then2, %if.then
  %2 = load i32, i32* %retval, align 4
  ret i32 %2
}

; Function Attrs: noinline nounwind optnone uwtable
define dso_local i32 @main() #0 {
entry:
  %retval = alloca i32, align 4
  %a = alloca i32, align 4
  store i32 0, i32* %retval, align 4
  %0 = bitcast i32* %a to i8*
  call void @klee_make_symbolic(i8* %0, i64 4, i8* getelementptr inbounds ([2 x i8], [2 x i8]* @.str, i64 0, i64 0))
  %1 = load i32, i32* %a, align 4
  %call = call i32 @get_sign(i32 %1)
  ret i32 %call
}

declare dso_local void @klee_make_symbolic(i8*, i64, i8*) #1

attributes #0 = { noinline nounwind optnone uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "frame-pointer"="all" "less-precise-fpmad"="false" "min-legal-vector-width"="0" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #1 = { "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "frame-pointer"="all" "less-precise-fpmad"="false" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }

!llvm.module.flags = !{!0}
!llvm.ident = !{!1}

!0 = !{i32 1, !"wchar_size", i32 4}
!1 = !{!"clang version 11.0.0"}

在这个示例中,klee生成了3个ktest文件:test000001.ktest, test000002.ktest, test000003.ktest表示生成了3个测试用例,分别对应程序3个分支(x < 0, x = 0, x > 0)

使用 ktest-tool klee-out-0/test000001.ktest 查看其内容:

ktest file : 'klee-last/test000001.ktest'
args       : ['get_sign.bc']
num objects: 1
object 0: name: 'a'
object 0: size: 4
object 0: data: b'\x00\x00\x00\x00'
object 0: hex : 0x00000000
object 0: int : 0
object 0: uint: 0
object 0: text: ....

num objects 表示符号变量数量,size为变量大小(计量单位byte),data为对应的数据

run.stats包含了一些统计信息,使用 klee-stats klee-out-0(不要加文件名,定位到目录就行) 查看

Path Instrs Time(s) ICov(%) BCov(%) ICount TSolver(%)
klee-out-0 31 0.09 100.00 100.00 25 97.82
  • ICov表示LLVM指令的覆盖率

  • BCov表示分支覆盖率

  • ICount表示指令数量

run.istats包含了一些状态信息,可用kcachegrind工具查看 (kcachegrind xxx.istats)

version: 1
creator: klee
pid: 13664
cmd: sample.bc


positions: instr line
event: Icov : CoveredInstructions
event: Forks : Forks
event: Ireal : InstructionRealTimes
event: Itime : InstructionTimes
event: I : Instructions
event: UCdist : MinDistToUncovered
event: Rtime : ResolveTime
event: States : States
event: Iuncov : UncoveredInstructions
event: Q : Queries
event: Qiv : QueriesInvalid
event: Qv : QueriesValid
event: Qtime : QueryTime
events: Icov Forks Ireal Itime I UCdist Rtime States Iuncov Q Qiv Qv Qtime 
ob=assembly.ll
fn=get_sign
11 0 1 0 0 0 1 0 0 0 0 0 0 0 0 
12 0 1 0 0 0 1 0 0 0 0 0 0 0 0 
13 0 1 0 0 0 1 0 0 0 0 0 0 0 0 
14 0 1 0 0 0 1 0 0 0 0 0 0 0 0 
15 0 1 0 0 0 1 0 0 0 0 0 0 0 0 
16 0 1 1 0 0 1 0 0 0 0 2 2 0 73279 
19 0 1 0 0 0 1 0 0 0 0 0 0 0 0 
20 0 1 0 0 0 1 0 0 0 0 0 0 0 0 
23 0 1 0 0 0 1 0 0 0 0 0 0 0 0 
24 0 1 0 0 0 1 0 0 0 0 0 0 0 0 
25 0 1 1 0 0 1 0 0 0 0 1 1 0 18761 
28 0 1 0 0 0 1 0 0 0 0 0 0 0 0 
29 0 1 0 0 0 1 0 0 0 0 0 0 0 0 
32 0 1 0 0 0 1 0 0 0 0 0 0 0 0 
33 0 1 0 0 0 1 0 0 0 0 0 0 0 0 
36 0 1 0 0 0 3 0 0 0 0 0 0 0 0 
37 0 1 0 0 0 3 0 0 0 0 0 0 0 0 
fn=main
43 0 1 0 0 0 1 0 0 0 0 0 0 0 0 
44 0 1 0 0 0 1 0 0 0 0 0 0 0 0 
45 0 1 0 0 0 1 0 0 0 0 0 0 0 0 
46 0 1 0 0 0 1 0 0 0 0 0 0 0 0 
47 0 1 0 0 0 1 0 0 0 0 0 0 0 0 
48 0 1 0 0 0 1 0 0 0 0 0 0 0 0 
49 0 1 0 0 0 1 0 0 0 0 0 0 0 0 
cfn=get_sign
calls=1 8 0
49 0 17 2 0 0 21 0 0 0 0 3 3 0 92040 
50 0 1 0 0 0 3 0 0 0 0 0 0 0 0 

每个状态对应一个int列表

  • 第1列为指令在assenmbly.II中的行号

  • 第2列为指令对应的源代码中的行号,需要在clang编译源码时添加参数 -g 保存debug信息,不然全是0

  • 第3列表示该状态覆盖的指令数

  • 第4列表示该状态Fork的数量

  • 第5,6列表示指令时间

  • 最后一个是求解时间

3.2.Using Symbolic Environment

3.2.1.sym-arg

  • -sym-arg <N> 能够提供1个长度为N 的命令行参数

  • -sym-args <MIN> <MAX> <N> 提供最少 MIN 个,最多 MAX 个长度最长为 N 的命令行参数

#include <stdio.h>

int check_password(char *buf) {
  if (buf[0] == 'h' && buf[1] == 'e' &&
      buf[2] == 'l' && buf[3] == 'l' &&
      buf[4] == 'o')
    return 1;
  return 0;
}

int main(int argc, char **argv) {
  if (argc < 2)
     return 1;
  
  if (check_password(argv[1])) {
    printf("Password found!\n");
    return 0;
  }

  return 1;
}

3.2.2.sym-files

-sym-files <NUM> <N> 参数创建 <NUM> 个,第一个命令为 A,第二个命名 B,依此类推,每个文件大小 N byte,子选项 -sym-stdin-sym-stdout 会使得标准输入和输出符号化

以类似的代码为例

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>

int check_password(int fd) {
  char buf[5];
  if (read(fd, buf, 5) != -1) {
    if (buf[0] == 'h' && buf[1] == 'e' &&
	buf[2] == 'l' && buf[3] == 'l' &&
	buf[4] == 'o')
      return 1;
  }
  return 0;
}

int main(int argc, char **argv) {
  int fd;

  if (argc >= 2) {
    if ((fd = open(argv[1], O_RDONLY)) != -1) {
      if (check_password(fd)) {
        printf("Password found in %s\n", argv[1]);
        close(fd);
        return 0;
      }
      close(fd);
      return 1;
    }
  }

  if (check_password(0)) {
    printf("Password found in standard input\n");
    return 0;
  }

  return 1;
}

编译之后用 klee -posix-runtime password.bc A -sym-files 1 10 跑,生成6个testcase,用ktest-tool查看其中一个,结果为

ktest file : 'klee-last/test000006.ktest'
args       : ['password.bc', 'A', '-sym-files', '1', '10']
num objects: 3
object 0: name: 'A-data'
object 0: size: 10
object 0: data: b'hellohhhhh'
object 0: hex : 0x68656c6c6f6868686868
object 0: text: hellohhhhh
object 1: name: 'A-data-stat'
object 1: size: 144
object 1: data: b'\x01\x08\x00\x00\x00\x00\x00\x00\x01\x01\x01\x01\x01\x01\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x00\x00\xe8\x03\x00\x00\xe8\x03\x00\x00\x01\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x01\x01\x01\x01\x01\x01\x01\x01\x00\x10\x00\x00\x00\x00\x00\x00\x01\x01\x01\x01\x01\x01\x01\x01\xed\xa7Bc\x00\x00\x00\x00\x01\x01\x01\x01\x01\x01\x01\x01\xaa\xa8Bc\x00\x00\x00\x00\x01\x01\x01\x01\x01\x01\x01\x01\xaa\xa8Bc\x00\x00\x00\x00\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01'
object 1: hex : 0x010800000000000001010101010101010100000000000000a4810000e8030000e8030000010101010000000000000000010101010101010100100000000000000101010101010101eda74263000000000101010101010101aaa84263000000000101010101010101aaa84263000000000101010101010101010101010101010101010101010101010101010101010101
object 1: text: ..........................................................................Bc..............Bc..............Bc....................................
object 2: name: 'model_version'
object 2: size: 4
object 2: data: b'\x01\x00\x00\x00'
object 2: hex : 0x01000000
object 2: int : 1
object 2: uint: 1
object 2: text: ....

只关注第一个object,名称为 A-data,text为 hellohhhhh,可以通过测试程序的密码检测。

四.源码简单分析

klee的 main 函数在tools/klee/main.cpp中,main 函数前面一大堆的代码是解析环境、参数啥的,在main.cpp中的1384行,程序新建了一个Interpreter,而之后,程序会调用Interpreter::runFunctionAsMain方法进行符号执行,这个方法是个虚方法。在实际执行中,程序调用的 Interpreter 子类是Executor类,因此符号执行的入口代码在Executor::runFunctionAsMain中(这里我删除了部分解析环境的代码)

void Executor::runFunctionAsMain(Function *f,
				 int argc,
				 char **argv,
				 char **envp) {
  std::vector<ref<Expr> > arguments;

  ExecutionState *state = new ExecutionState(kmodule->functionMap[f]);
  initializeGlobals(*state);

  processTree = std::make_unique<PTree>(state);
  run(*state);
  processTree = nullptr;
}
  • PTree在Random Path Search中用到了,其它搜索策略均没用到。

  • ExecutionState *state = new ExecutionState(kmodule->functionMap[f]);initializeGlobals(*state); 完成了初始状态的初始化。

  • run方法则以是符号执行的主要代码(下面是精简过的代码)

void Executor::run(ExecutionState &initialState) {
  states.insert(&initialState);

  searcher = constructUserSearcher(*this);
  std::vector<ExecutionState *> newStates(states.begin(), states.end());
  searcher->update(0, newStates, std::vector<ExecutionState *>());

  // main interpreter loop
  while (!states.empty() && !haltExecution) {
    ExecutionState &state = searcher->selectState();
    KInstruction *ki = state.pc;
    stepInstruction(state);

    executeInstruction(state, ki);
    updateStates(&state);
  }

  delete searcher;
  searcher = nullptr;

  doDumpStates();
}
  • states.insert(&initialState); 将初始状态添加到状态列表中。需要注意的是 statesExecutor 维护的状态集合(成员变量),每个Searcher类都会有一个自己的状态列表,但是二者是同步的。

  • searcher = constructUserSearcher(*this); 根据命令行参数选择状态选择策略,构造状态选择器Searcher

  • !states.empty() && !haltExecution 表示当状态列表不为空或者没有到终止条件(超市等)时循环继续。

  • ExecutionState &state = searcher->selectState(); 从状态列表中取出1个状态。

  • executeInstruction(state, ki); 在状态state下执行相关指令 ki(参考Executor::executeInstruction),执行指令的时候是可能产生终止状态的(调用Executor::terminateStateOnExit)以及生成新状态(调用Execute::fork),之后我会详细分析这个函数。

  • updateStates(&state); 更新状态列表,ExecutorSearcher 的都会更新。

updateStates 的部分代码如下(精简过):

void Executor::updateStates(ExecutionState *current) {
  if (searcher) {
    searcher->update(current, addedStates, removedStates);
  }
  
  states.insert(addedStates.begin(), addedStates.end());
  addedStates.clear();

  for (std::vector<ExecutionState *>::iterator it = removedStates.begin(), ie = removedStates.end(); it != ie; ++it) {
    ExecutionState *es = *it;
    std::set<ExecutionState*>::iterator it2 = states.find(es);
    assert(it2!=states.end());
    states.erase(it2);
    delete es;
  }
  removedStates.clear();
}

大概意思就是在 SearcherExecutor 的状态列表中删除 removedStates 中的state,添加 addStates 中的state。

五.对于符号执行流程的简单探索

Executor::run 下插入代码(只有 if (state.constraints.size() < 4) 是插入的代码):

// main interpreter loop
  while (!states.empty() && !haltExecution) {
    ExecutionState &state = searcher->selectState();
    KInstruction *ki = state.pc;

      // klee_message("next instruction %s \n", ki->inst->getName().data());
      if (state.constraints.size() < 4) {
          ki->inst->dump();
          klee_message("constraint num %lu \n", state.constraints.size());
          for (ConstraintSet::const_iterator iter = state.constraints.begin();
                iter != state.constraints.end(); ++iter)
                iter->get()->dump();
      klee_message("============================= \n");
      }
    stepInstruction(state);

    executeInstruction(state, ki);
    timers.invoke();
    if (::dumpStates) dumpStates();
    if (::dumpPTree) dumpPTree();

    updateStates(&state);

    if (!checkMemoryUsage()) {
      // update searchers when states were terminated early due to memory pressure
      updateStates(nullptr);
    }
  }

大概就是每次取出1个状态时打印将要执行的指令以及打印当前状态的约束集合。

测试以下代码

5.1.示例1

主要探索循环语句的影响,源码如下:

#include<klee/klee.h>

int main() {
  int a;
  klee_make_symbolic(&a, sizeof(a), "a");
  int i = a;
  while (i < 5){
    i++;
  }
} 

LLVM IR:

; ModuleID = 'sample.c'
source_filename = "sample.c"
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-unknown-linux-gnu"

@.str = private unnamed_addr constant [2 x i8] c"a\00", align 1

; Function Attrs: noinline nounwind optnone uwtable
define dso_local i32 @main() #0 {
entry:
  %retval = alloca i32, align 4
  %a = alloca i32, align 4
  %i = alloca i32, align 4
  store i32 0, i32* %retval, align 4
  %0 = bitcast i32* %a to i8*
  call void @klee_make_symbolic(i8* %0, i64 4, i8* getelementptr inbounds ([2 x i8], [2 x i8]* @.str, i64 0, i64 0))
  %1 = load i32, i32* %a, align 4
  store i32 %1, i32* %i, align 4
  br label %while.cond

while.cond:                                       ; preds = %while.body, %entry
  %2 = load i32, i32* %i, align 4
  %cmp = icmp slt i32 %2, 5
  br i1 %cmp, label %while.body, label %while.end

while.body:                                       ; preds = %while.cond
  %3 = load i32, i32* %i, align 4
  %inc = add nsw i32 %3, 1
  store i32 %inc, i32* %i, align 4
  br label %while.cond

while.end:                                        ; preds = %while.cond
  %4 = load i32, i32* %retval, align 4
  ret i32 %4
}

declare dso_local void @klee_make_symbolic(i8*, i64, i8*) #1

attributes #0 = { noinline nounwind optnone uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "frame-pointer"="all" "less-precise-fpmad"="false" "min-legal-vector-width"="0" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #1 = { "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "frame-pointer"="all" "less-precise-fpmad"="false" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }

!llvm.module.flags = !{!0}
!llvm.ident = !{!1}

!0 = !{i32 1, !"wchar_size", i32 4}
!1 = !{!"clang version 11.0.0"}

在klee执行的过程中可以发现:

  • 当第1次执行完 br i1 %cmp, label %while.body, label %while.end 后,状态的约束集合添加了1个。

    • 如果下一个执行是 %4 = load i32, i32* %retval, align 4,也就是在false branch,添加的约束为 (Eq false (Slt (ReadLSB w32 0 a) 5)),表示 ~(a < 5)(这里 a 是符号值)。

    • 如果是 while.body: 中的指令,添加的约束为 (Slt (ReadLSB w32 0 a) 5),表示 a < 5

  • 第2次执行完 br i1 %cmp, label %while.body, label %while.end,状态的约束集合又添加了1个。

    • 对于false branch(%4 = load i32, i32* %retval, align 4),2个约束分别为 Slt (ReadLSB w32 0 a) 5, (Eq false (Slt (Add w32 1 (ReadLSB w32 0 a)) 5))。分别表示 a < 5, ~((a + 1) < 5)

    • 对于true branch(while.body: 中的),则为 Slt (ReadLSB w32 0 a) 5, (Slt (Add w32 1 (ReadLSB w32 0 a)) 5) 。分别表示 a < 5, (a + 1) < 5

  • 符号执行会不断进行下去,循环不会停止,除非外部中断。

5.2.示例2

主要探索函数调用的影响,源码如下:

#include <klee/klee.h>

int f(int x)
{
    if (x % 2 == 0)
        return x / 2;
    else
        return 3 * x + 1;
}

int main(void)
{
    int x;
    klee_make_symbolic(&x, sizeof(x), "x");
    int a = 0;
    if (x < 5)
        a += f(x);
    else
        a += f(x);
    return 0;
}

LLVM IR

; ModuleID = 'sample.c'
source_filename = "sample.c"
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-unknown-linux-gnu"

@.str = private unnamed_addr constant [2 x i8] c"x\00", align 1

; Function Attrs: noinline nounwind optnone uwtable
define dso_local i32 @f(i32 %x) #0 {
entry:
  %retval = alloca i32, align 4
  %x.addr = alloca i32, align 4
  store i32 %x, i32* %x.addr, align 4
  %0 = load i32, i32* %x.addr, align 4
  %rem = srem i32 %0, 2
  %cmp = icmp eq i32 %rem, 0
  br i1 %cmp, label %if.then, label %if.else

if.then:                                          ; preds = %entry
  %1 = load i32, i32* %x.addr, align 4
  %div = sdiv i32 %1, 2
  store i32 %div, i32* %retval, align 4
  br label %return

if.else:                                          ; preds = %entry
  %2 = load i32, i32* %x.addr, align 4
  %mul = mul nsw i32 3, %2
  %add = add nsw i32 %mul, 1
  store i32 %add, i32* %retval, align 4
  br label %return

return:                                           ; preds = %if.else, %if.then
  %3 = load i32, i32* %retval, align 4
  ret i32 %3
}

; Function Attrs: noinline nounwind optnone uwtable
define dso_local i32 @main() #0 {
entry:
  %retval = alloca i32, align 4
  %x = alloca i32, align 4
  %a = alloca i32, align 4
  store i32 0, i32* %retval, align 4
  %0 = bitcast i32* %x to i8*
  call void @klee_make_symbolic(i8* %0, i64 4, i8* getelementptr inbounds ([2 x i8], [2 x i8]* @.str, i64 0, i64 0))
  store i32 0, i32* %a, align 4
  %1 = load i32, i32* %x, align 4
  %cmp = icmp slt i32 %1, 5
  br i1 %cmp, label %if.then, label %if.else

if.then:                                          ; preds = %entry
  %2 = load i32, i32* %x, align 4
  %call = call i32 @f(i32 %2)
  %3 = load i32, i32* %a, align 4
  %add = add nsw i32 %3, %call
  store i32 %add, i32* %a, align 4
  br label %if.end

if.else:                                          ; preds = %entry
  %4 = load i32, i32* %x, align 4
  %call1 = call i32 @f(i32 %4)
  %5 = load i32, i32* %a, align 4
  %add2 = add nsw i32 %5, %call1
  store i32 %add2, i32* %a, align 4
  br label %if.end

if.end:                                           ; preds = %if.else, %if.then
  ret i32 0
}

declare dso_local void @klee_make_symbolic(i8*, i64, i8*) #1

attributes #0 = { noinline nounwind optnone uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "frame-pointer"="all" "less-precise-fpmad"="false" "min-legal-vector-width"="0" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #1 = { "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "frame-pointer"="all" "less-precise-fpmad"="false" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }

!llvm.module.flags = !{!0}
!llvm.ident = !{!1}

!0 = !{i32 1, !"wchar_size", i32 4}
!1 = !{!"clang version 11.0.0"}

这个示例klee探索了4条路径,其中主程序中fork了1次,2次调用f各fork了1次。

接下来换示例3

5.3.示例3

#include <klee/klee.h>

int f(int x){
    if (x % 2 == 0)
        return x / 2;
    else
        return 3 * x + 1;
}

int main(void){
    int x;
    klee_make_symbolic(&x, sizeof(x), "x");
    int a = f(x);

    if (x < 5)
        a++;
    else
        a--;
    return 0;
}

IR就不放了,这次运行klee依旧产生了4个path,首先在 f 调用返回时已经fork成2个状态了,在 f fork的2个状态返回主程序之后又分别在 if (x < 5) 又fork了一次,就这样最后fork成了4个状态,对应4个path。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

程序分析-klee工具分析 的相关文章

  • 具有子列表属性映射问题的自动映射器

    我有以下型号 Models public class Dish Required public Int64 ID get set Required public string Name get set Required public str
  • 查找哪些页面不再与写入时复制共享

    假设我在 Linux 中有一个进程 我从中fork 另一个相同的过程 后forking 因为原始进程将开始写入内存 Linux写时复制机制将为进程提供与分叉进程使用的不同的唯一物理内存页 在执行的某个时刻 我如何知道原始进程的哪些页面已被写
  • 进程何时获得 SIGABRT(信号 6)?

    C 中进程获得 SIGABRT 的场景有哪些 该信号是否始终来自进程内部 或者该信号可以从一个进程发送到另一个进程吗 有没有办法识别哪个进程正在发送该信号 abort 向调用进程发送SIGABRT信号 就是这样abort 基本上有效 abo
  • C#动态支持吗?

    看完之后这个帖子 https stackoverflow com questions 2674906 when should one use dynamic keyword in c sharp 4 0k和链接 我还有 2 个问题 问题 1
  • 向 ExpandoObject 添加方法时,“关键字 'this' 在静态属性、静态方法或静态字段初始值设定项中无效”

    我尝试向 ExpandoObject 添加一个动态方法 该方法将返回属性 动态添加 给它 但它总是给我错误 我在这里做错了什么吗 using System using System Collections Generic using Sys
  • 如何创建可以像 UserControl 一样编辑的 TabPage 子类?

    我想创建一个包含一些控件的 TabPage 子类 并且我想通过设计器来控制这些控件的布局和属性 但是 如果我在设计器中打开子类 我将无法像在 UserControl 上那样定位它们 我不想创建一个带有 UserControl 实例的 Tab
  • 如何在 Android NDK 中创建新的 NativeWindow 而无需 Android 操作系统源代码?

    我想编译一个 Android OpenGL 控制台应用程序 您可以直接从控制台启动 Android x86 运行 或者从 Android x86 GUI 内的 Android 终端应用程序运行 这个帖子 如何在 Android NDK 中创
  • 为什么要序列化对象需要 Serialized 属性

    根据我的理解 SerializedAttribute 不提供编译时检查 因为它都是在运行时完成的 如果是这样 那么为什么需要将类标记为可序列化呢 难道序列化器不能尝试序列化一个对象然后失败吗 这不就是它现在所做的吗 当某些东西被标记时 它会
  • C# 中的接口继承

    我试图解决我在编写应用程序时遇到的相当大的 对我来说 问题 请看这个 为了简单起见 我将尝试缩短代码 我有一个名为的根接口IRepository
  • POCO HTTPSClientSession 发送请求时遇到问题 - 证书验证失败

    我正在尝试使用 POCO 库编写一个向服务器发出 HTTPS 请求的程序 出于测试目的 我正在连接到具有自签名证书的服务器 并且我希望允许客户端进行连接 为了允许这种情况发生 我尝试安装InvalidCertificateHandler这是
  • 访问者和模板化虚拟方法

    在一个典型的实现中Visitor模式 该类必须考虑基类的所有变体 后代 在许多情况下 访问者中的相同方法内容应用于不同的方法 在这种情况下 模板化的虚拟方法是理想的选择 但目前这是不允许的 那么 模板化方法可以用来解析父类的虚方法吗 鉴于
  • 如何从网站下载 .EXE 文件?

    我正在编写一个应用程序 需要从网站下载 exe 文件 我正在使用 Visual Studio Express 2008 我正在使用以下代码 private void button1 Click object sender EventArgs
  • Qt 创建布局并动态添加小部件到布局

    我正在尝试在 MainWindow 类中动态创建布局 我有四个框架 它们是用网格布局对象放置的 每个框架都包含一个自定义的 ClockWidget 我希望 ClockWidget 对象在调整主窗口大小时相应地调整大小 因此我需要将它们添加到
  • 基于xsd模式生成xml(使用.NET)

    我想根据我的 xsd 架构 cap xsd 生成 xml 文件 我找到了这篇文章并按照说明进行操作 使用 XSD 文件生成 XML 文件 https stackoverflow com questions 6530424 generatin
  • C# 中条件编译符号的编译时检查(参见示例)?

    在 C C 中你可以这样做 define IN USE 1 define NOT IN USE 1 define USING system 1 system 1 IN USE 进而 define MY SYSTEM IN USE if US
  • 为什么拆箱枚举会产生奇怪的结果?

    考虑以下 Object box 5 int int int box int 5 int nullableInt box as int nullableInt 5 StringComparison enum StringComparison
  • 剪贴板在 .NET 3.5 和 4 中的行为有所不同,但为什么呢?

    我们最近将一个非常大的项目从 NET Framework 3 5 升级到 4 最初一切似乎都工作正常 但现在复制粘贴操作开始出现错误 我已经成功制作了一个小型的可复制应用程序 它显示了 NET 3 5 和 4 中的不同行为 我还找到了一种解
  • WinRT 定时注销

    我正在开发一个 WinRT 应用程序 要求之一是应用程序应具有 定时注销 功能 这意味着在任何屏幕上 如果应用程序空闲了 10 分钟 应用程序应该注销并导航回主屏幕 显然 执行此操作的强力方法是在每个页面的每个网格上连接指针按下事件 并在触
  • Googletest:如何异步运行测试?

    考虑到一个包含数千个测试的大型项目 其中一些测试需要几分钟才能完成 如果按顺序执行 整套测试需要一个多小时才能完成 通过并行执行测试可以减少测试时间 据我所知 没有办法直接从 googletest mock 做到这一点 就像 async选项
  • 匿名结构体作为返回类型

    下面的代码编译得很好VC 19 00 23506 http rextester com GMUP11493 标志 Wall WX Za 与VC 19 10 25109 0 标志 Wall WX Za permissive 这可以在以下位置检

随机推荐

  • Source Insight 4.0首次安装提示unable to open or create...解决方案

    打开注册表编辑器 windows r 打开运行窗口 输入 regedit enter确认 编辑注册表 一 找到这个文件夹计算机 HKEY CURRENT USER Software Source Dynamics Source Insigh
  • 「长沙 · 中国1024程序员节」来了!

    从资深院士到行业元老再到领域大拿 重磅嘉宾 掌门云集 5 代技术代表人物岳麓对话 9 大操作系统掌门人星城聚首 10 场热门技术分论坛 峰会 覆盖开源技术 操作系统 区块链 黑客松等 100 开源技术英雄年度会面 还有众多创新互动体验 体验
  • 前端 字体样式

    字体样式
  • U3D批处理的静态与动态

    在屏幕上渲染物体 引擎需要发出一个绘制调用来访问图形API 每个绘制调用需要进行大量的工作来访问图形API 从而导致了CPU方面显著的性能开销 Unity在运行时可以将一些物体进行合并 从而用一个绘制调用来渲染他们 这一操作 我们称之为 批
  • TCP快速重传为什么是三次冗余ack

    先理解ACK的基本工作原理 当发送端发送第N 1个包后 接收端答复的ACK序列号实际上跟发送端发送下一个包 也就是第N个包的序列号一致 重复ACK是指在接收方收到乱序报文时 所发出的一类TCP报文 TCP使用报文头的序列号和确认号以有效保证
  • python绘制正弦函数和余弦函数

    题目 根据如下绘图写出相应代码 1 绘图函数 y sin x y cos x x np linspace np pi np pi 256 endpoint True 2 绘制填充区域 紫色区域 2 5
  • PARL与强化学习笔记

    PARL与强化学习笔记 1 预习 1 1MNIST手写识别 1 2 python基础知识 1 3paddle基础知识 1 3 1计算常量的加法 1 1 1 3 2计算变量的加法 1 1 1 3 3使用PaddlePaddle做线性回归 满足
  • Python 基础合集12:os库文件操作

    一 前言 本小节梳理了os常用的一些方法 并介绍一个小案例 环境说明 Python 3 6 windows11 64位 二 os常用方法 注意 使用方法前需要先调用os库 即import os os getcwd 获取当前目录 os chd
  • apache-commons目录大全

    主页推荐 BCEL 字节码工程库 分析 创建和操作Java类文件 BeanUtils 围绕Java反射和自省api的易于使用的包装器 BSF Bean脚本框架 脚本语言的接口 包括JSR 223 Chain 责任链模式的实施 CLI 命令行
  • 基于单片机的照明灯智能控制器系统(设计报告+电路原理图+程序)

    摘要 本文设计了一种基于单片机的照明灯智能控制器系统 该系统通过使用单片机与光照传感器和人体感应器进行通信 实时感知环境光照和人的存在情况 并根据预设的控制策略控制灯光亮度和开关 通过适当的算法和控制逻辑 实现了对照明灯的智能控制 实验结果
  • C++卷积神经网络实例:tiny_cnn代码详解(8)——partial_connected_layer层结构类分析(上)

    在之前的博文中我们已经将顶层的网络结构都介绍完毕 包括卷积层 下采样层 全连接层 在这篇博文中主要有两个任务 一是整体贯通一下卷积神经网络在对图像进行卷积处理的整个流程 二是继续我们的类分析 这次需要进行分析的是卷积层和下采样层的公共基类
  • CSDN如何解决复制后代码格式错乱问题?

    不要直接用鼠标选择代码复制粘贴 要用代码块右侧的复制按钮进行复制
  • Cannot prepare internal mirrorlist: No URLs in mirrorlist

    我是在执行 yum install dnf plugins core 这个命令报的错误 问题 在CentOS 8中 使用yum时出现错误 镜像列表中没有url 类似如下 Error Failed to download metadata f
  • canvas 刻度尺

    参考链接 https codepen io luren pen yEagYO 画布
  • 拆书领读

    来源于课程学习笔记 一 为什么拆书 赚钱 自我提升 看书 二 如何拆书 1 平台及类型 听书 用一篇文章的长度 5000 8000字 告诉你一本书的精华内容 拆书 用5 10篇文章的长 2 听书 采用总分总的套路 第一部分 总领全文 看书的
  • 锂离子电池保护板你懂多少呢?

    锂离子电池保护板你懂多少呢 电子设备通常用的是聚合物电池和锂电池 但是聚合物电池容易鼓包 随着锂离子电池的出现 由于其能量密度高 充电效率高 而且对环境 友好 故得到制造商的垂爱 但是用锂电池必须对过压和过流进行检测 以保护锂离子电池 不然
  • 静态方法访问非静态变量

    使用态方法需要访问非静态变量会出现图中的问题 其解决方法有两种 1 将要访问的非静态变量改成静态的 2 使用类对象来访问 public class Main String string 1111111123456 public static
  • LocalDateTime和Date的比较(JDK8新特性:时间日期API)

    最近在项目升级框架查资料会涉及到LocalDateTime 当时看到这个觉得为什么大家都在用这个 为什么 说到这里我们要知道这个LocalDateTime来自哪里 实际上这个LocalDateTime是JDK8的新特性之一 JDK8发布了新
  • opencv 手势识别 【附源代码】

    我使用OpenCV2 4 4的windows版本 Qt4 8 3 VS2010的编译器做了一个手势识别的小程序 本程序主要使到了Opencv的特征训练库和最基本的图像处理的知识 包括肤色检测等等 废话不多 先看一下基本的界面设计 以及主要功
  • 程序分析-klee工具分析

    一 klee介绍 1 1 简单介绍 Klee是一个LLVM IR符号执行工具 OSDI 08 Paper地址 能够自动生成测试 实现对各种复杂且环境密集型程序的高覆盖率 klee有2个目标 命中目标程序中的每一行代码 检测到每一个危险操作