xref: /linux/Documentation/translations/zh_CN/dev-tools/kcov.rst (revision ea04ef19ebdcd22e8a21054a19c2c8fefae011ce)
1.. SPDX-License-Identifier: GPL-2.0
2
3.. include:: ../disclaimer-zh_CN.rst
4
5:Original: Documentation/dev-tools/kcov.rst
6:Translator: 刘浩阳 Haoyang Liu <tttturtleruss@hust.edu.cn>
7
8KCOV: 用于模糊测试的代码覆盖率
9==============================
10
11KCOV 以一种适用于覆盖率引导的模糊测试的形式收集和暴露内核代码覆盖率信息。
12一个正在运行的内核的覆盖率数据可以通过 ``kcov`` 调试文件导出。覆盖率的收集是基
13于任务启用的,因此 KCOV 可以精确捕获单个系统调用的覆盖率。
14
15要注意的是 KCOV 不是为了收集尽可能多的覆盖率数据。而是为了收集相对稳定的覆盖率
16,这是系统调用输入的函数。为了完成这个目标,它不收集软硬中断的覆盖率(除非移除
17覆盖率收集被启用,见下文)以及内核中固有的不确定部分的覆盖率(如调度器,锁定)
18
19除了收集代码覆盖率,KCOV 还收集操作数比较的覆盖率。见 "操作数比较收集" 一节
20查看详细信息。
21
22除了从系统调用处理器收集覆盖率数据,KCOV 还从后台内核或软中断任务中执行的内核
23被标注的部分收集覆盖率。见 "远程覆盖率收集" 一节查看详细信息。
24
25先决条件
26--------
27
28KCOV 依赖编译器插桩,要求 GCC 6.1.0 及更高版本或者内核支持的任意版本的 Clang。
29
30收集操作数比较的覆盖率需要 GCC 8+ 或者 Clang。
31
32为了启用 KCOV,需要使用如下参数配置内核::
33
34        CONFIG_KCOV=y
35
36为了启用操作数比较覆盖率的收集,使用如下参数::
37
38    CONFIG_KCOV_ENABLE_COMPARISONS=y
39
40覆盖率数据只会在调试文件系统被挂载后才可以获取::
41
42        mount -t debugfs none /sys/kernel/debug
43
44覆盖率收集
45----------
46
47下面的程序演示了如何使用 KCOV 在一个测试程序中收集单个系统调用的覆盖率:
48
49.. code-block:: c
50
51    #include <stdio.h>
52    #include <stddef.h>
53    #include <stdint.h>
54    #include <stdlib.h>
55    #include <sys/types.h>
56    #include <sys/stat.h>
57    #include <sys/ioctl.h>
58    #include <sys/mman.h>
59    #include <unistd.h>
60    #include <fcntl.h>
61    #include <linux/types.h>
62
63    #define KCOV_INIT_TRACE			_IOR('c', 1, unsigned long)
64    #define KCOV_ENABLE			_IO('c', 100)
65    #define KCOV_DISABLE			_IO('c', 101)
66    #define COVER_SIZE			(64<<10)
67
68    #define KCOV_TRACE_PC  0
69    #define KCOV_TRACE_CMP 1
70
71    int main(int argc, char **argv)
72    {
73	int fd;
74	unsigned long *cover, n, i;
75
76	/* 单个文件描述符允许
77	 * 在单线程上收集覆盖率。
78	 */
79	fd = open("/sys/kernel/debug/kcov", O_RDWR);
80	if (fd == -1)
81		perror("open"), exit(1);
82	/* 设置跟踪模式和跟踪大小。 */
83	if (ioctl(fd, KCOV_INIT_TRACE, COVER_SIZE))
84		perror("ioctl"), exit(1);
85	/* 映射内核空间和用户空间共享的缓冲区。 */
86	cover = (unsigned long*)mmap(NULL, COVER_SIZE * sizeof(unsigned long),
87				     PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
88	if ((void*)cover == MAP_FAILED)
89		perror("mmap"), exit(1);
90	/* 在当前线程中启用覆盖率收集。 */
91	if (ioctl(fd, KCOV_ENABLE, KCOV_TRACE_PC))
92		perror("ioctl"), exit(1);
93	/* 在调用 ioctl() 之后重置覆盖率。 */
94	__atomic_store_n(&cover[0], 0, __ATOMIC_RELAXED);
95	/* 调用目标系统调用。 */
96	read(-1, NULL, 0);
97	/* 读取收集到的 PC 的数目。 */
98	n = __atomic_load_n(&cover[0], __ATOMIC_RELAXED);
99	for (i = 0; i < n; i++)
100		printf("0x%lx\n", cover[i + 1]);
101	/* 在当前线程上禁用覆盖率收集。在这之后
102	 * 可以在其他线程上收集覆盖率
103	 */
104	if (ioctl(fd, KCOV_DISABLE, 0))
105		perror("ioctl"), exit(1);
106	/* 释放资源 */
107	if (munmap(cover, COVER_SIZE * sizeof(unsigned long)))
108		perror("munmap"), exit(1);
109	if (close(fd))
110		perror("close"), exit(1);
111	return 0;
112    }
113
114在使用 ``addr2line`` 传输后,程序输出应该如下所示::
115
116    SyS_read
117    fs/read_write.c:562
118    __fdget_pos
119    fs/file.c:774
120    __fget_light
121    fs/file.c:746
122    __fget_light
123    fs/file.c:750
124    __fget_light
125    fs/file.c:760
126    __fdget_pos
127    fs/file.c:784
128    SyS_read
129    fs/read_write.c:562
130
131如果一个程序需要从多个线程收集覆盖率(独立地)。那么每个线程都需要单独打开
132``/sys/kernel/debug/kcov``。
133
134接口的细粒度允许高效的创建测试进程。即,一个父进程打开了
135``/sys/kernel/debug/kcov``,启用了追踪模式,映射了覆盖率缓冲区,然后在一个循
136环中创建了子进程。这个子进程只需要启用覆盖率收集即可(当一个线程退出时将自动禁
137用覆盖率收集)。
138
139操作数比较收集
140--------------
141
142操作数比较收集和覆盖率收集类似:
143
144.. code-block:: c
145
146    /* 包含和上文一样的头文件和宏定义。 */
147
148    /* 每次记录的 64 位字的数量。 */
149    #define KCOV_WORDS_PER_CMP 4
150
151    /*
152     * 收集的比较种类的格式。
153     *
154     * 0 比特表示是否是一个编译时常量。
155     * 1 & 2 比特包含参数大小的 log2 值,最大 8 字节。
156     */
157
158    #define KCOV_CMP_CONST          (1 << 0)
159    #define KCOV_CMP_SIZE(n)        ((n) << 1)
160    #define KCOV_CMP_MASK           KCOV_CMP_SIZE(3)
161
162    int main(int argc, char **argv)
163    {
164	int fd;
165	uint64_t *cover, type, arg1, arg2, is_const, size;
166	unsigned long n, i;
167
168	fd = open("/sys/kernel/debug/kcov", O_RDWR);
169	if (fd == -1)
170		perror("open"), exit(1);
171	if (ioctl(fd, KCOV_INIT_TRACE, COVER_SIZE))
172		perror("ioctl"), exit(1);
173	/*
174	* 注意缓冲区指针的类型是 uint64_t*,因为所有的
175	* 比较操作数都被提升为 uint64_t 类型。
176	*/
177	cover = (uint64_t *)mmap(NULL, COVER_SIZE * sizeof(unsigned long),
178				     PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
179	if ((void*)cover == MAP_FAILED)
180		perror("mmap"), exit(1);
181	/* 注意这里是 KCOV_TRACE_CMP 而不是 KCOV_TRACE_PC。 */
182	if (ioctl(fd, KCOV_ENABLE, KCOV_TRACE_CMP))
183		perror("ioctl"), exit(1);
184	__atomic_store_n(&cover[0], 0, __ATOMIC_RELAXED);
185	read(-1, NULL, 0);
186	/* 读取收集到的比较操作数的数量。 */
187	n = __atomic_load_n(&cover[0], __ATOMIC_RELAXED);
188	for (i = 0; i < n; i++) {
189		uint64_t ip;
190
191		type = cover[i * KCOV_WORDS_PER_CMP + 1];
192		/* arg1 和 arg2 - 比较的两个操作数。 */
193		arg1 = cover[i * KCOV_WORDS_PER_CMP + 2];
194		arg2 = cover[i * KCOV_WORDS_PER_CMP + 3];
195		/* ip - 调用者的地址。 */
196		ip = cover[i * KCOV_WORDS_PER_CMP + 4];
197		/* 操作数的大小。 */
198		size = 1 << ((type & KCOV_CMP_MASK) >> 1);
199		/* is_const - 当操作数是一个编译时常量时为真。*/
200		is_const = type & KCOV_CMP_CONST;
201		printf("ip: 0x%lx type: 0x%lx, arg1: 0x%lx, arg2: 0x%lx, "
202			"size: %lu, %s\n",
203			ip, type, arg1, arg2, size,
204		is_const ? "const" : "non-const");
205	}
206	if (ioctl(fd, KCOV_DISABLE, 0))
207		perror("ioctl"), exit(1);
208	/* 释放资源。 */
209	if (munmap(cover, COVER_SIZE * sizeof(unsigned long)))
210		perror("munmap"), exit(1);
211	if (close(fd))
212		perror("close"), exit(1);
213	return 0;
214    }
215
216注意 KCOV 的模式(代码覆盖率收集或操作数比较收集)是互斥的。
217
218远程覆盖率收集
219--------------
220
221除了从用户空间进程发布的系统调用句柄收集覆盖率数据以外,KCOV 也可以从部分在其
222他上下文中执行的内核中收集覆盖率 - 称为“远程”覆盖率。
223
224使用 KCOV 收集远程覆盖率要求:
225
2261. 修改内核源码并使用 ``kcov_remote_start`` 和 ``kcov_remote_stop`` 来标注要收集
227   覆盖率的代码片段。
228
2292. 在用户空间的收集覆盖率的进程应使用 ``KCOV_REMOTE_ENABLE`` 而不是 ``KCOV_ENABLE``。
230
231``kcov_remote_start`` 和 ``kcov_remote_stop`` 的标注以及 ``KCOV_REMOTE_ENABLE``
232ioctl 都接受可以识别特定覆盖率收集片段的句柄。句柄的使用方式取决于匹配代码片段执
233行的上下文。
234
235KCOV 支持在如下上下文中收集远程覆盖率:
236
2371. 全局内核后台任务。这些任务是内核启动时创建的数量有限的实例(如,每一个
238   USB HCD 产生一个 USB ``hub_event`` 工作器)。
239
2402. 局部内核后台任务。这些任务通常是由于用户空间进程与某些内核接口进行交互时产
241   生的,并且通常在进程退出时会被停止(如,vhost 工作器)。
242
2433. 软中断。
244
245对于 #1 和 #3,必须选择一个独特的全局句柄并将其传递给对应的
246``kcov_remote_start`` 调用。一个用户空间进程必须将该句柄存储在
247``kcov_remote_arg`` 结构体的 ``handle`` 数组字段中并将其传递给
248``KCOV_REMOTE_ENABLE``。这会将使用的 KCOV 设备附加到由此句柄引用的代码片段。多个全局
249句柄标识的不同代码片段可以一次性传递。
250
251对于 #2,用户空间进程必须通过 ``kcov_remote_arg`` 结构体的 ``common_handle`` 字段
252传递一个非零句柄。这个通用句柄将会被保存在当前 ``task_struct`` 结构体的
253``kcov_handle`` 字段中并且需要通过自定义内核代码的修改来传递给新创建的本地任务
254。这些任务需要在 ``kcov_remote_start`` 和 ``kcov_remote_stop`` 标注中依次使用传递过来的
255句柄。
256
257KCOV 对全局句柄和通用句柄均遵循一个预定义的格式。每一个句柄都是一个 ``u64`` 整形
258。当前,只有最高位和低四位字节被使用。第 4-7 字节是保留位并且值必须为 0。
259
260对于全局句柄,最高位的字节表示该句柄属于的子系统的标识。比如,KCOV 使用 ``1``
261表示 USB 子系统类型。全局句柄的低 4 字节表示子系统中任务实例的标识。比如,每一
262个 ``hub_event`` 工作器使用 USB 总线号作为任务实例的标识。
263
264对于通用句柄,使用一个保留值 ``0`` 作为子系统标识,因为这些句柄不属于一个特定
265的子系统。通用句柄的低 4 字节用于识别有用户进程生成的所有本地句柄的集合实例,
266该进程将通用句柄传递给 ``KCOV_REMOTE_ENABLE``。
267
268实际上,如果只从系统中的单个用户空间进程收集覆盖率,那么可以使用任意值作为通用
269句柄的实例标识。然而,如果通用句柄被多个用户空间进程使用,每个进程必须使用唯一
270的实例标识。一个选择是使用进程标识作为通用句柄实例的标识。
271
272下面的程序演示了如何使用 KCOV 从一个由进程产生的本地任务和处理 USB 总线的全局
273任务 #1 收集覆盖率:
274
275.. code-block:: c
276
277    /* 包含和上文一样的头文件和宏定义。 */
278
279    struct kcov_remote_arg {
280	__u32		trace_mode;
281	__u32		area_size;
282	__u32		num_handles;
283	__aligned_u64	common_handle;
284	__aligned_u64	handles[0];
285    };
286
287    #define KCOV_INIT_TRACE			_IOR('c', 1, unsigned long)
288    #define KCOV_DISABLE			_IO('c', 101)
289    #define KCOV_REMOTE_ENABLE		_IOW('c', 102, struct kcov_remote_arg)
290
291    #define COVER_SIZE	(64 << 10)
292
293    #define KCOV_TRACE_PC	0
294
295    #define KCOV_SUBSYSTEM_COMMON	(0x00ull << 56)
296    #define KCOV_SUBSYSTEM_USB	(0x01ull << 56)
297
298    #define KCOV_SUBSYSTEM_MASK	(0xffull << 56)
299    #define KCOV_INSTANCE_MASK	(0xffffffffull)
300
301    static inline __u64 kcov_remote_handle(__u64 subsys, __u64 inst)
302    {
303	if (subsys & ~KCOV_SUBSYSTEM_MASK || inst & ~KCOV_INSTANCE_MASK)
304		return 0;
305	return subsys | inst;
306    }
307
308    #define KCOV_COMMON_ID	0x42
309    #define KCOV_USB_BUS_NUM	1
310
311    int main(int argc, char **argv)
312    {
313	int fd;
314	unsigned long *cover, n, i;
315	struct kcov_remote_arg *arg;
316
317	fd = open("/sys/kernel/debug/kcov", O_RDWR);
318	if (fd == -1)
319		perror("open"), exit(1);
320	if (ioctl(fd, KCOV_INIT_TRACE, COVER_SIZE))
321		perror("ioctl"), exit(1);
322	cover = (unsigned long*)mmap(NULL, COVER_SIZE * sizeof(unsigned long),
323				     PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
324	if ((void*)cover == MAP_FAILED)
325		perror("mmap"), exit(1);
326
327	/* 通过通用句柄和 USB 总线 #1 启用代码覆盖率收集。 */
328	arg = calloc(1, sizeof(*arg) + sizeof(uint64_t));
329	if (!arg)
330		perror("calloc"), exit(1);
331	arg->trace_mode = KCOV_TRACE_PC;
332	arg->area_size = COVER_SIZE;
333	arg->num_handles = 1;
334	arg->common_handle = kcov_remote_handle(KCOV_SUBSYSTEM_COMMON,
335							KCOV_COMMON_ID);
336	arg->handles[0] = kcov_remote_handle(KCOV_SUBSYSTEM_USB,
337						KCOV_USB_BUS_NUM);
338	if (ioctl(fd, KCOV_REMOTE_ENABLE, arg))
339		perror("ioctl"), free(arg), exit(1);
340	free(arg);
341
342	/*
343	 * 在这里用户需要触发执行一个内核代码段
344	 * 该代码段要么使用通用句柄标识
345	 * 要么触发了一些 USB 总线 #1 上的一些活动。
346	 */
347	sleep(2);
348
349	n = __atomic_load_n(&cover[0], __ATOMIC_RELAXED);
350	for (i = 0; i < n; i++)
351		printf("0x%lx\n", cover[i + 1]);
352	if (ioctl(fd, KCOV_DISABLE, 0))
353		perror("ioctl"), exit(1);
354	if (munmap(cover, COVER_SIZE * sizeof(unsigned long)))
355		perror("munmap"), exit(1);
356	if (close(fd))
357		perror("close"), exit(1);
358	return 0;
359    }
360