英文

KCOV:用于模糊测试的代码覆盖率

KCOV 以适合覆盖率引导的模糊测试的形式收集和公开内核代码覆盖率信息。正在运行的内核的覆盖率数据通过 kcov debugfs 文件导出。覆盖率收集是基于任务启用的,因此 KCOV 可以捕获单个系统调用的精确覆盖率。

请注意,KCOV 的目标不是收集尽可能多的覆盖率。它的目标是收集或多或少稳定的覆盖率,它是系统调用输入的函数。为了实现此目标,它不会在软/硬中断中收集覆盖率(除非启用了远程覆盖率收集,请参见下文)以及来自内核某些固有的不确定性部分(例如,调度程序、锁定)的覆盖率。

除了收集代码覆盖率之外,KCOV 还可以收集比较操作数。有关详细信息,请参见“比较操作数收集”部分。

除了从系统调用处理程序收集覆盖率数据之外,KCOV 还可以为在后台内核任务或软中断中执行的内核的带注释部分收集覆盖率。有关详细信息,请参见“远程覆盖率收集”部分。

先决条件

KCOV 依赖于编译器插桩,需要 GCC 6.1.0 或更高版本,或者内核支持的任何 Clang 版本。

GCC 8+ 或 Clang 支持收集比较操作数。

要启用 KCOV,请使用以下配置内核:

CONFIG_KCOV=y

要启用比较操作数收集,请设置:

CONFIG_KCOV_ENABLE_COMPARISONS=y

只有在挂载 debugfs 后,才能访问覆盖率数据:

mount -t debugfs none /sys/kernel/debug

覆盖率收集

以下程序演示了如何使用 KCOV 从测试程序中收集单个系统调用的覆盖率

#include <stdio.h>
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <fcntl.h>
#include <linux/types.h>

#define KCOV_INIT_TRACE                     _IOR('c', 1, unsigned long)
#define KCOV_ENABLE                 _IO('c', 100)
#define KCOV_DISABLE                        _IO('c', 101)
#define COVER_SIZE                  (64<<10)

#define KCOV_TRACE_PC  0
#define KCOV_TRACE_CMP 1

int main(int argc, char **argv)
{
    int fd;
    unsigned long *cover, n, i;

    /* A single fd descriptor allows coverage collection on a single
     * thread.
     */
    fd = open("/sys/kernel/debug/kcov", O_RDWR);
    if (fd == -1)
            perror("open"), exit(1);
    /* Setup trace mode and trace size. */
    if (ioctl(fd, KCOV_INIT_TRACE, COVER_SIZE))
            perror("ioctl"), exit(1);
    /* Mmap buffer shared between kernel- and user-space. */
    cover = (unsigned long*)mmap(NULL, COVER_SIZE * sizeof(unsigned long),
                                 PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if ((void*)cover == MAP_FAILED)
            perror("mmap"), exit(1);
    /* Enable coverage collection on the current thread. */
    if (ioctl(fd, KCOV_ENABLE, KCOV_TRACE_PC))
            perror("ioctl"), exit(1);
    /* Reset coverage from the tail of the ioctl() call. */
    __atomic_store_n(&cover[0], 0, __ATOMIC_RELAXED);
    /* Call the target syscall call. */
    read(-1, NULL, 0);
    /* Read number of PCs collected. */
    n = __atomic_load_n(&cover[0], __ATOMIC_RELAXED);
    for (i = 0; i < n; i++)
            printf("0x%lx\n", cover[i + 1]);
    /* Disable coverage collection for the current thread. After this call
     * coverage can be enabled for a different thread.
     */
    if (ioctl(fd, KCOV_DISABLE, 0))
            perror("ioctl"), exit(1);
    /* Free resources. */
    if (munmap(cover, COVER_SIZE * sizeof(unsigned long)))
            perror("munmap"), exit(1);
    if (close(fd))
            perror("close"), exit(1);
    return 0;
}

通过管道传输 addr2line 后,程序的输出如下所示

SyS_read
fs/read_write.c:562
__fdget_pos
fs/file.c:774
__fget_light
fs/file.c:746
__fget_light
fs/file.c:750
__fget_light
fs/file.c:760
__fdget_pos
fs/file.c:784
SyS_read
fs/read_write.c:562

如果一个程序需要从多个线程(独立地)收集覆盖率,则需要在每个线程中单独打开 /sys/kernel/debug/kcov

该接口是细粒度的,允许高效地分叉测试进程。也就是说,父进程打开 /sys/kernel/debug/kcov,启用跟踪模式,内存映射覆盖率缓冲区,然后在循环中分叉子进程。子进程只需要启用覆盖率(当线程退出时,它会自动禁用)。

比较操作数收集

比较操作数收集类似于覆盖率收集

/* Same includes and defines as above. */

/* Number of 64-bit words per record. */
#define KCOV_WORDS_PER_CMP 4

/*
 * The format for the types of collected comparisons.
 *
 * Bit 0 shows whether one of the arguments is a compile-time constant.
 * Bits 1 & 2 contain log2 of the argument size, up to 8 bytes.
 */

#define KCOV_CMP_CONST          (1 << 0)
#define KCOV_CMP_SIZE(n)        ((n) << 1)
#define KCOV_CMP_MASK           KCOV_CMP_SIZE(3)

int main(int argc, char **argv)
{
    int fd;
    uint64_t *cover, type, arg1, arg2, is_const, size;
    unsigned long n, i;

    fd = open("/sys/kernel/debug/kcov", O_RDWR);
    if (fd == -1)
            perror("open"), exit(1);
    if (ioctl(fd, KCOV_INIT_TRACE, COVER_SIZE))
            perror("ioctl"), exit(1);
    /*
    * Note that the buffer pointer is of type uint64_t*, because all
    * the comparison operands are promoted to uint64_t.
    */
    cover = (uint64_t *)mmap(NULL, COVER_SIZE * sizeof(unsigned long),
                                 PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if ((void*)cover == MAP_FAILED)
            perror("mmap"), exit(1);
    /* Note KCOV_TRACE_CMP instead of KCOV_TRACE_PC. */
    if (ioctl(fd, KCOV_ENABLE, KCOV_TRACE_CMP))
            perror("ioctl"), exit(1);
    __atomic_store_n(&cover[0], 0, __ATOMIC_RELAXED);
    read(-1, NULL, 0);
    /* Read number of comparisons collected. */
    n = __atomic_load_n(&cover[0], __ATOMIC_RELAXED);
    for (i = 0; i < n; i++) {
            uint64_t ip;

            type = cover[i * KCOV_WORDS_PER_CMP + 1];
            /* arg1 and arg2 - operands of the comparison. */
            arg1 = cover[i * KCOV_WORDS_PER_CMP + 2];
            arg2 = cover[i * KCOV_WORDS_PER_CMP + 3];
            /* ip - caller address. */
            ip = cover[i * KCOV_WORDS_PER_CMP + 4];
            /* size of the operands. */
            size = 1 << ((type & KCOV_CMP_MASK) >> 1);
            /* is_const - true if either operand is a compile-time constant.*/
            is_const = type & KCOV_CMP_CONST;
            printf("ip: 0x%lx type: 0x%lx, arg1: 0x%lx, arg2: 0x%lx, "
                    "size: %lu, %s\n",
                    ip, type, arg1, arg2, size,
            is_const ? "const" : "non-const");
    }
    if (ioctl(fd, KCOV_DISABLE, 0))
            perror("ioctl"), exit(1);
    /* Free resources. */
    if (munmap(cover, COVER_SIZE * sizeof(unsigned long)))
            perror("munmap"), exit(1);
    if (close(fd))
            perror("close"), exit(1);
    return 0;
}

请注意,KCOV 模式(收集代码覆盖率或比较操作数)是互斥的。

远程覆盖率收集

除了从用户空间进程发出的系统调用处理程序收集覆盖率数据之外,KCOV 还可以为在其他上下文中执行的内核部分(所谓的“远程”覆盖率)收集覆盖率。

使用 KCOV 收集远程覆盖率需要:

  1. 修改内核代码以使用 kcov_remote_startkcov_remote_stop 注释应从中收集覆盖率的代码段。

  2. 在收集覆盖率的用户空间进程中使用 KCOV_REMOTE_ENABLE 而不是 KCOV_ENABLE

kcov_remote_startkcov_remote_stop 注释以及 KCOV_REMOTE_ENABLE ioctl 都接受标识特定覆盖率收集段的句柄。句柄的使用方式取决于匹配的代码段执行的上下文。

KCOV 支持从以下上下文中收集远程覆盖率:

  1. 全局内核后台任务。这些是在内核启动期间在有限数量的实例中生成的任务(例如,每个 USB HCD 生成一个 USB hub_event 工作线程)。

  2. 本地内核后台任务。这些是在用户空间进程与某些内核接口交互时生成的,并且通常在进程退出时被杀死(例如,vhost 工作线程)。

  3. 软中断。

对于 #1 和 #3,必须选择一个唯一的全局句柄,并将其传递给相应的 kcov_remote_start 调用。然后,用户空间进程必须将此句柄传递给 kcov_remote_arg 结构的 handles 数组字段中的 KCOV_REMOTE_ENABLE。这将把使用的 KCOV 设备附加到此句柄引用的代码段。可以一次传递多个标识不同代码段的全局句柄。

对于 #2,用户空间进程必须通过 kcov_remote_arg 结构的 common_handle 字段传递一个非零句柄。此公共句柄将被保存到当前 task_struct 中的 kcov_handle 字段,并且需要通过自定义内核代码修改传递给新生成的本地任务。这些任务应该在其 kcov_remote_startkcov_remote_stop 注释中使用传递的句柄。

KCOV 为全局和公共句柄遵循预定义的格式。每个句柄都是一个 u64 整数。目前,仅使用顶部和较低的 4 个字节。字节 4-7 保留,必须为零。

对于全局句柄,句柄的顶字节表示此句柄所属的子系统的 ID。例如,KCOV 使用 1 作为 USB 子系统 ID。全局句柄的较低 4 个字节表示该子系统中任务实例的 ID。例如,每个 hub_event 工作线程都使用 USB 总线号作为任务实例 ID。

对于公共句柄,保留值 0 用作子系统 ID,因为此类句柄不属于特定的子系统。公共句柄的较低 4 个字节标识用户空间进程传递公共句柄给 KCOV_REMOTE_ENABLE 而生成的所有本地任务的集合实例。

实际上,如果仅从系统上的单个用户空间进程收集覆盖率,则可以将任何值用于公共句柄实例 ID。但是,如果多个进程使用公共句柄,则每个进程必须使用唯一的实例 ID。一种选择是使用进程 ID 作为公共句柄实例 ID。

以下程序演示了如何使用 KCOV 从进程生成的本地任务和处理 USB 总线 #1 的全局任务收集覆盖率:

/* Same includes and defines as above. */

struct kcov_remote_arg {
    __u32           trace_mode;
    __u32           area_size;
    __u32           num_handles;
    __aligned_u64   common_handle;
    __aligned_u64   handles[0];
};

#define KCOV_INIT_TRACE                     _IOR('c', 1, unsigned long)
#define KCOV_DISABLE                        _IO('c', 101)
#define KCOV_REMOTE_ENABLE          _IOW('c', 102, struct kcov_remote_arg)

#define COVER_SIZE  (64 << 10)

#define KCOV_TRACE_PC       0

#define KCOV_SUBSYSTEM_COMMON       (0x00ull << 56)
#define KCOV_SUBSYSTEM_USB  (0x01ull << 56)

#define KCOV_SUBSYSTEM_MASK (0xffull << 56)
#define KCOV_INSTANCE_MASK  (0xffffffffull)

static inline __u64 kcov_remote_handle(__u64 subsys, __u64 inst)
{
    if (subsys & ~KCOV_SUBSYSTEM_MASK || inst & ~KCOV_INSTANCE_MASK)
            return 0;
    return subsys | inst;
}

#define KCOV_COMMON_ID      0x42
#define KCOV_USB_BUS_NUM    1

int main(int argc, char **argv)
{
    int fd;
    unsigned long *cover, n, i;
    struct kcov_remote_arg *arg;

    fd = open("/sys/kernel/debug/kcov", O_RDWR);
    if (fd == -1)
            perror("open"), exit(1);
    if (ioctl(fd, KCOV_INIT_TRACE, COVER_SIZE))
            perror("ioctl"), exit(1);
    cover = (unsigned long*)mmap(NULL, COVER_SIZE * sizeof(unsigned long),
                                 PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if ((void*)cover == MAP_FAILED)
            perror("mmap"), exit(1);

    /* Enable coverage collection via common handle and from USB bus #1. */
    arg = calloc(1, sizeof(*arg) + sizeof(uint64_t));
    if (!arg)
            perror("calloc"), exit(1);
    arg->trace_mode = KCOV_TRACE_PC;
    arg->area_size = COVER_SIZE;
    arg->num_handles = 1;
    arg->common_handle = kcov_remote_handle(KCOV_SUBSYSTEM_COMMON,
                                                    KCOV_COMMON_ID);
    arg->handles[0] = kcov_remote_handle(KCOV_SUBSYSTEM_USB,
                                            KCOV_USB_BUS_NUM);
    if (ioctl(fd, KCOV_REMOTE_ENABLE, arg))
            perror("ioctl"), free(arg), exit(1);
    free(arg);

    /*
     * Here the user needs to trigger execution of a kernel code section
     * that is either annotated with the common handle, or to trigger some
     * activity on USB bus #1.
     */
    sleep(2);

    n = __atomic_load_n(&cover[0], __ATOMIC_RELAXED);
    for (i = 0; i < n; i++)
            printf("0x%lx\n", cover[i + 1]);
    if (ioctl(fd, KCOV_DISABLE, 0))
            perror("ioctl"), exit(1);
    if (munmap(cover, COVER_SIZE * sizeof(unsigned long)))
            perror("munmap"), exit(1);
    if (close(fd))
            perror("close"), exit(1);
    return 0;
}