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 8 KCOV: 用于模糊测试的代码覆盖率 9 ============================== 10 11 KCOV 以一种适用于覆盖率引导的模糊测试的形式收集和暴露内核代码覆盖率信息。 12 一个正在运行的内核的覆盖率数据可以通过 ``kcov`` 调试文件导出。覆盖率的收集是基 13 于任务启用的,因此 KCOV 可以精确捕获单个系统调用的覆盖率。 14 15 要注意的是 KCOV 不是为了收集尽可能多的覆盖率数据。而是为了收集相对稳定的覆盖率 16 ,这是系统调用输入的函数。为了完成这个目标,它不收集软硬中断的覆盖率(除非移除 17 覆盖率收集被启用,见下文)以及内核中固有的不确定部分的覆盖率(如调度器,锁定) 18 19 除了收集代码覆盖率,KCOV 还收集操作数比较的覆盖率。见 "操作数比较收集" 一节 20 查看详细信息。 21 22 除了从系统调用处理器收集覆盖率数据,KCOV 还从后台内核或软中断任务中执行的内核 23 被标注的部分收集覆盖率。见 "远程覆盖率收集" 一节查看详细信息。 24 25 先决条件 26 -------- 27 28 KCOV 依赖编译器插桩,要求 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 226 1. 修改内核源码并使用 ``kcov_remote_start`` 和 ``kcov_remote_stop`` 来标注要收集 227 覆盖率的代码片段。 228 229 2. 在用户空间的收集覆盖率的进程应使用 ``KCOV_REMOTE_ENABLE`` 而不是 ``KCOV_ENABLE``。 230 231 ``kcov_remote_start`` 和 ``kcov_remote_stop`` 的标注以及 ``KCOV_REMOTE_ENABLE`` 232 ioctl 都接受可以识别特定覆盖率收集片段的句柄。句柄的使用方式取决于匹配代码片段执 233 行的上下文。 234 235 KCOV 支持在如下上下文中收集远程覆盖率: 236 237 1. 全局内核后台任务。这些任务是内核启动时创建的数量有限的实例(如,每一个 238 USB HCD 产生一个 USB ``hub_event`` 工作器)。 239 240 2. 局部内核后台任务。这些任务通常是由于用户空间进程与某些内核接口进行交互时产 241 生的,并且通常在进程退出时会被停止(如,vhost 工作器)。 242 243 3. 软中断。 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 257 KCOV 对全局句柄和通用句柄均遵循一个预定义的格式。每一个句柄都是一个 ``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 }
Linux® is a registered trademark of Linus Torvalds in the United States and other countries.
TOMOYO® is a registered trademark of NTT DATA CORPORATION.