说明:
- 这个例子展示了如何使用KLEE来寻找迷宫游戏的所有解决方案,是对如何使用符号执行来生成输入的一个很好的说明。示例官网原文见:Solving a maze with KLEE
-实例中的迷宫大小为11X7,玩家通过’w’,‘s’,‘d’,'f’操作上下左右,从X出发,要找到路径达到出口#。如图所示:
源代码在:https://pastebin.com/6wG5stht
1.首先我们尝试手动寻找答案
假设我们的代码存放在***maze.c***中,我们将其编译:
$ gcc maze.c -o maze
得到可执行文件 maze.exe,然后执行并输入路径:
ssssddddwwaawwddddssssddwwww
最终得到我们wining的结果,即我们成功的找到了迷宫出口。
2.现在我们尝试用KLEE来找我们的结果
补充:这里由于我是在docker中使用的klee,首先我需要将maze.c拷贝到我的docker下:
$ docker cp C:\Users\Lichao\Desktop\maze.c my_first_klee_container:/home/klee/klee_src/examples/maze
这里,
- docker cp 是拷贝命令
- C:\Users\Lichao\Desktop\maze.c 是我的拷贝源
- my_first_klee_container:/home/klee/klee_src/examples/maze 是目的拷贝地址
和之前例子一样,首先我们要在代码中符号化输入,因此我们还要修改源代码:
read(0,program,ITERS);
修改为下面:
klee_make_symbolic(program,ITERS,"program");
并添加头文件:
#include<klee/klee/h>
然后,我们将 maze.c编译为maze.bc,并在/examples/maze下执行命令:
$ clang -c -I …/…/include -emit-llvm maze.c -o maze.bc
$ klee maze.bc
然后在控制台可以看到一系列执行的过程,并最终生成结果:
KLEE: done: total instructions = 134318
KLEE: done: completed paths = 309
KLEE: done: generated tests = 309
以及结果报告,可以看到对于309个测试用例每个都单独有一个报告:
klee@ddf2bf456634:~/klee_src/examples/maze$ ls klee-last
assembly.ll test000059.ktest test000122.ktest test000185.ktest test000248.ktest
info test000060.ktest test000123.ktest test000186.ktest test000249.ktest
messages.txt test000061.ktest test000124.ktest test000187.ktest test000250.ktest
run.istats test000062.ktest test000125.ktest test000188.ktest test000251.ktest
run.stats test000063.ktest test000126.ktest test000189.ktest …
test000001.ktest test000064.ktest test000127.ktest test000190.ktest test000309.ktest … … … … warnings.txt
我们随便打开一个查看里面的内容:
klee@ddf2bf456634:~/klee_src/examples/maze$ ktest-tool klee-last/test000309.ktest
\ktest file : ‘klee-last/test000309.ktest’
args : [‘maze.bc’]
num objects: 1
object 0: name: ‘program’
object 0: size: 28
object 0: data: b’ssssddddwwaawwddddsddsssaaww’
object 0: hex : 0x73737373646464647777616177776464646473646473737361617777
object 0: text: ssssddddwwaawwddddsddsssaaww
里面给出了执行的路径,我们可以自己将这个路径带回到迷宫中去试试到底是不是结果,能不能找到出口。但是,这么多个报告,我们不可能每一个都去打开自己尝试(不然用KLEE有什么意义?),因此我们需要KLEE能够给出我们那些路径是正确的,即能找到出口。
3.如何标记我们感兴趣的部分代码?
这里有一个类似于C语言里面的断言的函数klee_assert(),它强制条件为真,否则就会中止执行。我们可以用断言来标记我们感兴趣的部分代码,一旦KLEE执行到这个地方就会给出提醒。(更多的KLEE C接口可以在klee.h头文件里面查看:https://llvm.org/svn/llvmproject/klee/trunk/include/klee/klee.h
因此,我们可以在源代码中进行修改:
//找到代码
printf("You win!\n");
//替换为:
printf ("You win!\n");
klee_assert(0); //Signal The solution!!
因为当我们源代码执行了printf(“You win!\n”)时表明我们找到了正确的答案,因此我们在这行代码后面添加klee_assert(0),即一个条件永远为0的断言,这样只要执行到了这里断言就会报错,这样我们就有了一个标识告诉我们找到了答案。
修改后,我们重新编译、执行:
$ clang -c -I …/…/include -emit-llvm maze.c -o maze.bc
$ klee maze.bc
最终结果和报告分别是:
KLEE: done: total instructions = 134310
KLEE: done: completed paths = 309
KLEE: done: generated tests = 306klee@ddf2bf456634:~/klee_src/examples/maze$ ls klee-last assembly.ll
test000059.ktest test000122.ktest test000183.ktest
test000246.ktest info test000060.ktest test000123.ktest
test000184.ktest test000247.ktest messages.txt test000061.ktest
test000124.ktest test000185.ktest test000248.ktest run.istats
test000062.ktest test000125.ktest test000186.ktest
test000249.ktest run.stats test000063.ktest test000126.ktest
test000187.ktest test000250.ktest … …
… … … test000012.ktest
test000075.ktest test000138.assert.err test000199.ktest
test000262.ktest test000013.ktest test000076.ktest test000138.kquery
test000200.ktest test000263.ktest … …
… … …
可以看到,我们多出了一个.assert.err的错误报告文件,这应该就是找到了正确答案的测试用例了,我们打开看看:
klee@ddf2bf456634:~/klee_src/examples/maze/klee-last$ ktest-tool test000138.ktest
ktest file : ‘test000138.ktest’
args : [‘maze.bc’]
num objects: 1
object 0: name: ‘program’
object 0: size: 28
object 0: data: b’sddwddddsddw\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff’
object 0: hex : 0x736464776464646473646477ffffffffffffffffffffffffffffffff
object 0: text: sddwddddsddw…
得到我们的路径顺序是: sddwddddsddw,但是这个长度有点奇怪,很明显应该是到不了出口的,我们用它作为输入重新执行下我们的maze.exe:
呃呃呃…穿墙术? 可以看到这个地方,我们设置的两个墙被直接传过去了,为什么?这里我们回到源代码:
1. //If something is wrong do not advance
2. if (maze[y][x] != ' '
3. &&
4. !((y == 2 && maze[y][x] == '|' && x > 0 && x < W)))
5. {
6. x = ox;
7. y = oy;
8. }
原本应该是当前位置如果不为空格都应该回退,但是这里多了一个判定条件,等于说将第二列的‘|’都设置成了虚假墙壁,是可以穿过去的!!!因此得到了我们上面的答案。
OK,这个问题解决了。但是这样的话我们仍然只有一个答案,照理说应该会有很多条不同的路径都可以到达我们的出口才对。回顾我们上一个例子中说过,klee对于由同一位置到达的错误只会报告一次。因此,这里我们需要用到参数:-emit-all-errors (更多参数详情见:https://pastebin.com/tDPGNn9D
$ klee -emit-all-errors maze.bc
结果如图:
KLEE: done: total instructions = 134310
KLEE: done: completed paths = 309
KLEE: done: generated tests = 309
可以看到,想比之前的306个测试用例,这里多了3个,对应每一条路径都有一个测试报告。而那3个就是由于上述原因导致的没有生成的测试用例。
查看报告之后也可以看到一共有4个.assert.err的文件了:
… … … …
… test000008.ktest test000073.ktest test000138.assert.err
test000201.ktest test000262.ktest test000009.ktest
test000074.ktest test000138.kquery test000202.ktest
test000263.ktest test000010.ktest test000075.ktest test000138.ktest
test000203.ktest test000264.ktest test000019.ktest
test000084.ktest test000147.ktest test000212.assert.err
test000273.ktest test000020.ktest test000085.ktest test000148.ktest
test000212.kquery test000274.ktest test000021.ktest
test000086.ktest test000149.ktest test000212.ktest
test000275.ktest test000047.ktest test000112.ktest test000175.ktest
test000238.ktest test000301.assert.err test000048.ktest
test000113.ktest test000176.ktest test000239.ktest
test000301.kquery test000049.ktest test000114.ktest test000177.ktest
test000240.ktest test000301.ktest test000058.ktest
test000123.ktest test000186.ktest test000249.ktest
warnings.txt test000059.ktest test000124.ktest test000187.ktest
test000250.assert.err test000060.ktest test000125.ktest
test000188.ktest test000250.kquery …
我们分别打开这4个.ktest文件,最终得到4个路径:
1.sddwddddsddw
2.ssssddddwwaawwddddsddw
3.sddwddddssssddwwww
4.ssssddddwwaawwddddssssddwwww
总结:对于这个例子,我们通过符号执行比手动去寻找答案肯定方便多了,甚至比自己编写代码去搜索路径也方便(毕竟写代码也容易出错)。 同时,我们学习了如何通过增加断言的方式来获得我们需要的信息。