1.目的
我想实现如上图效果,统计随着fuzz的时间增加,统计代码覆盖率的趋势,这样可以反映出我的种子变异策略的好坏,是否能覆盖更多的API。
在titanfuzz中,他们给每一个API一分钟的fuzz时间,然后把所有api循环一边,然后统计最后的代码覆盖率。但是如何得到10、20、30等直接的代码覆盖率。这也是我想要知道的问题。
因此,我对该实验进行了模拟。
2.测试代码
我目录下有一个main.cpp,内容如下:
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
// 手动编写 if-else if 语句,从 0 扩展到 20
if (argc != 2) {
printf("Usage: %s <flag>\n", argv[0]);
return 1;
}
int flag = atoi(argv[1]);
if (flag >= 1) {
printf("flag>=1\n");
if (flag >= 2) {
printf("flag>=2\n");
if (flag >= 3) {
printf("flag>=3\n");
if (flag >= 4) {
printf("flag>=4\n");
if (flag >= 5) {
printf("flag>=5\n");
if (flag >= 6) {
printf("flag>=6\n");
if (flag >= 7) {
printf("flag>=7\n");
if (flag >= 8) {
printf("flag>=8\n");
if (flag >= 9) {
printf("flag>=9\n");
if (flag >= 10) {
printf("flag>=10\n");
}
}
}
}
}
}
}
}
}
}
return 0;
}
代码的逻辑很简单,就是根据flag的值,嵌套判断,并输出结果。为什么要写成这样,这是为了方便使用gcov统计代码覆盖率。 可以直观的看出 代码覆盖率的值。
然后编译代码:
gcc -o main main.cpp
查看代码逻辑是否正常:
3.使用gcov选项编译代码
如何使用gcc编译时开启代码覆盖率统计,推荐大家看这篇文章
https://blog.csdn.net/yanxiangyfg/article/details/80989680
删除上一步编译好的main,重新编译,添加代码覆盖率统计选项。
-ftest-coverage
:在编译的时候产生.gcno文件,它包含了重建基本块图和相应的块的源码的行号的信息。-fprofile-arcs
:在运行编译过的程序的时候,会产生.gcda文件,它包含了弧跳变的次数等信息。
gcc -o main main.cpp -fprofile-arcs -ftest-coverage
此时目录下应该有三个文件,main,main.cpp,和main.gcno。
其中main.gcno,只要源码不变,编译出来永远不改变.
4.统计代码覆盖率
运行下面的命令产生代码覆盖率数据文件。
gcov main.cpp
可以看出代码覆盖率为0,因为我们还没有执行main,所以代码覆盖率还没有统计。
注意:编译一次后,后续只要你调用一次main,代码覆盖率就会累计增加,所以我们的目的是,执行10s后,就应该要统计一次代码覆盖率,不然等1分钟后再统计,就是第60秒的结果。
执行gcov main.cpp 后,文件夹下面会多出一个 main.cpp.gcov文件,代码覆盖率数据,其数据的来源为main.gcda。
而main.gcda,是每次调用main时就会更新。
可以看出,执行main 1后,文件夹下面就有了main.gcda, 后续只要调用了main,main.gcda的数据就会更新。
cat main.cpp.gcov
通过该命令查看代码覆盖率的情况
现在,我们看代码覆盖率,依然还是0,因为虽然我们执行了main,但是想查看执行后的代码覆盖率,还需要手动执行
gcov main.cpp
再次查看代码覆盖率,可以看出,每行最前面有数字1,代码这行代码被执行了1次。而后续的if由于条件不满足,没有被触发,而我们模拟fuzz,想突破这些路径,就是改变flag的值,当然这个例子很简单,只需要让flag的值不断变大就可以实现,真实环境里面的条件判断是很复杂的,所以我们需要一个很好的变异策略,引导种子变异。
可以看出,每次执行main后,想要查看代码覆盖率,都要手动执行gcov命令,而且这样的方式也不太容易观察,所以我们一般使用clov生成可视化的代码覆盖率。
lcov -c -d . -o main.info
现在文件夹下面多了一个main.info,这个就是clov根据.gcno .gcda生成的,用于存放覆盖率数据的。
然后执行下面的代码,去得到web版的代码覆盖率。
genhtml -o html_output main.info
进入html_output,打开index.html即可。或者在当前目录下开一个web服务器。
python -m http.server 8080
5.实现代码覆盖率趋势统计
回到我们最初的目的,我们想统计一分钟内的代码覆盖率趋势,并且是按照10s、20s、30s这样的步长去统计,但是如果我们直接写个脚本,一分钟内不断执行main,给出不同的flag值。那么最后得到的代码覆盖率其实是总体的代码覆盖率,也就是这一分钟内,所有测试用例所触发的代码行数统计。
因此,我们只有设置一个定时器,以10s为步长,暂停fuzz,然后执行下面的命令。记录当前的代码覆盖率。
gcov main.cpp
lcov -c -d . -o main.info
genhtml -o html_output main.info
因此,我写了一个python脚本来实现这个功能。其中flag是随机从1-10生成。统计10s、20s、30s...后的代码覆盖率。
import re
import subprocess
import threading
import time
import random
flag = 0
# 定义输出函数
def print_output():
# 获取当前时间
current_time = time.strftime("%H:%M:%S", time.localtime())
print("Output at", current_time)
# 定义定时任务函数
def fuzzer():
print("Fuzz start")
count = 0
result_list = []
global flag
while True:
time.sleep(1)
count += 1
print("fuzzing...")
my_exec()
if count % 10 == 0:
# 在每个时间点执行代码
print(f"Measuring coverage at {count}s...")
# 统计代码覆盖率
# 这里使用 coverage 工具来统计代码覆盖率
code_lines = save_coverage_info()
result_list.append(code_lines)
flag = generate_random_number(1,10)
if count == 60:
break
print("fuzz end")
print(result_list)
# 创建定时器,每 10 秒执行一次 my_exec 函数
# 定义函数,生成指定范围内的随机整数
def generate_random_number(start, end):
return random.randint(start, end)
def my_exec():
global flag
print("now flag is ", flag)
# 调用 C 程序并传递参数
process = subprocess.Popen(["./main", str(flag)], stdout=subprocess.PIPE)
# 获取输出
output, error = process.communicate()
# 打印输出
print(output.decode("utf-8"))
def save_coverage_info():
# 执行 gcov 命令
lines_covered = 0
gcov_command = "gcov main.cpp"
result = subprocess.run(gcov_command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
if result.returncode == 0:
# 打印命令的输出
print("Command output:")
print(result.stdout)
else:
# 打印错误信息
print("Error occurred:")
print(result.stderr)
# 执行 lcov 命令
lcov_command = "lcov -c -d . -o main.info"
result = subprocess.run(lcov_command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
if result.returncode == 0:
# 打印命令的输出
print("Command output:")
print(result.stdout)
else:
# 打印错误信息
print("Error occurred:")
print(result.stderr)
# 执行 genhtml 命令
genhtml_command = "genhtml -o html_output main.info"
result = subprocess.run(genhtml_command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
if result.returncode == 0:
# 打印命令的输出
print("Command output:")
print(result.stdout)
pattern = r"lines\.{6}: (\d+\.\d+)% \((\d+) of (\d+) lines\)"
matches = re.findall(pattern, result.stdout)
# 打印匹配结果
for match in matches:
# coverage_percentage = float(match[0])
lines_covered = int(match[1])
# total_lines = int(match[2])
# print("Lines covered:", lines_covered)
else:
# 打印错误信息
print("Error occurred:")
print(result.stderr)
return lines_covered
if __name__ == '__main__':
fuzzer()
执行结果如下:
rm -rf html_output/ main main.cpp.g* main.info main.g*