在 Linux 内核中使用 gcov

Linux 内核的 gcov 性能分析(profiling)支持允许在 Linux 内核中使用 GCC 的覆盖率测试工具 gcov。运行中的内核的覆盖率数据通过“gcov” debugfs 目录以 gcov 兼容的格式导出。要获取特定文件的覆盖率数据,请切换到内核构建目录,并使用带 -o 选项的 gcov,如下所示(需要 root 权限):

# cd /tmp/linux-out
# gcov -o /sys/kernel/debug/gcov/tmp/linux-out/kernel spinlock.c

这将在当前目录中创建带有执行计数注释的源代码文件。此外,还可以使用图形化的 gcov 前端(例如 lcov)来自动化收集整个内核数据的过程,并以 HTML 格式提供覆盖率概览。

可能的用途

  • 调试(这一行到底有没有被执行到?)

  • 测试改进(我该如何修改测试以覆盖这些行?)

  • 精简内核配置(如果相关代码从不运行,我还需要这个选项吗?)

准备工作

通过以下配置内核

CONFIG_DEBUG_FS=y
CONFIG_GCOV_KERNEL=y

并且要获取整个内核的覆盖率数据

CONFIG_GCOV_PROFILE_ALL=y

请注意,使用性能分析标志编译的内核会显著增大且运行速度变慢。此外,并非所有架构都支持 CONFIG_GCOV_PROFILE_ALL。

只有在挂载 debugfs 后,性能分析数据才会变为可用状态

mount -t debugfs none /sys/kernel/debug

自定义

要为特定文件或目录启用性能分析,请在相应的内核 Makefile 中添加类似于以下内容的行

  • 对于单个文件(例如 main.o)

    GCOV_PROFILE_main.o := y
    
  • 对于一个目录中的所有文件

    GCOV_PROFILE := y
    

要排除某些文件不进行性能分析(即使指定了 CONFIG_GCOV_PROFILE_ALL),请使用

GCOV_PROFILE_main.o := n

GCOV_PROFILE := n

此机制仅支持链接到主内核镜像或编译为内核模块的文件。

模块特定配置

下面描述了针对特定模块的 Gcov 内核配置

CONFIG_GCOV_PROFILE_RDS

在 RDS 上启用 GCOV 性能分析,以检查执行了哪些函数或行。此配置由 rds 自测用于生成覆盖率报告。如果未设置,则会省略该报告。

文件

gcov 内核支持在 debugfs 中创建以下文件

/sys/kernel/debug/gcov

所有与 gcov 相关文件的父目录。

/sys/kernel/debug/gcov/reset

全局重置文件:写入时将所有覆盖率数据重置为零。

/sys/kernel/debug/gcov/path/to/compile/dir/file.gcda

gcov 工具可识别的实际 gcov 数据文件。写入时将文件的覆盖率数据重置为零。

/sys/kernel/debug/gcov/path/to/compile/dir/file.gcno

指向 gcov 工具所需的静态数据文件的符号链接。该文件由 gcc 在使用 -ftest-coverage 选项编译时生成。

模块 (Modules)

内核模块可能包含仅在模块卸载时运行的清理代码。gcov 机制通过保留与已卸载模块相关的数据副本,提供了一种收集此类代码覆盖率数据的方法。这些数据通过 debugfs 保持可用。一旦模块再次加载,相关的覆盖率计数器就会用其先前实例化的数据进行初始化。

可以通过指定 gcov_persist 内核参数来停用此行为

gcov_persist=0

在运行时,用户还可以选择通过写入其数据文件或全局重置文件来丢弃已卸载模块的数据。

分离的构建和测试机器

gcov 内核性能分析基础架构旨在开箱即用于内核在同一台机器上构建和运行的环境。在内核运行在独立机器上的情况下,必须根据使用 gcov 工具的位置做出特殊的准备工作

  1. 在测试机器上运行 gcov

    测试机器上的 gcov 工具版本必须与用于构建内核的 gcc 版本兼容。此外,需要将以下文件从构建机器复制到测试机器

    来自源码树
    • 所有 C 源代码文件 + 头文件

    来自构建树
    • 所有 C 源代码文件 + 头文件

    • 所有 .gcda 和 .gcno 文件

    • 指向目录的所有链接

    重要提示:这些文件必须放置在测试机器上与构建机器上完全相同的文件系统位置。如果任何路径组件是符号链接,则必须使用实际的目录(由于 make 的 CURDIR 处理方式)。

  1. 在构建机器上运行 gcov

    在每个测试用例之后,需要将以下文件从测试机器复制到构建机器

    来自 sysfs 中的 gcov 目录
    • 所有 .gcda 文件

    • 指向 .gcno 文件的所有链接

    这些文件可以复制到构建机器上的任意位置。然后必须使用指向该目录的 -o 选项来调用 gcov。

    构建机器上的目录设置示例

    /tmp/linux:    kernel source tree
    /tmp/out:      kernel build directory as specified by make O=
    /tmp/coverage: location of the files copied from the test machine
    
    [user@build] cd /tmp/out
    [user@build] gcov -o /tmp/coverage/tmp/out/init main.c
    

关于编译器的说明

GCC 和 LLVM gcov 工具不一定兼容。请使用 gcov 处理 GCC 生成的 .gcno 和 .gcda 文件,并使用 llvm-cov 处理 Clang 生成的文件。

GCC 和 Clang gcov 之间的构建差异由 Kconfig 处理。它会根据检测到的工具链自动选择合适的 gcov 格式。

故障排查

问题 (Problem)

编译在链接器步骤中中止。

原因

为未链接到主内核或通过自定义链接器过程链接的源文件指定了性能分析标志。

解决方案

通过在相应的 Makefile 中指定 GCOV_PROFILE := nGCOV_PROFILE_basename.o := n,将受影响的源文件排除在性能分析之外。

问题 (Problem)

从 sysfs 复制的文件显得为空或不完整。

原因

由于 seq_file 的工作方式,某些工具(如 cp 或 tar)可能无法正确从 sysfs 复制文件。

解决方案

使用 cat 读取 .gcda 文件,并使用 cp -d 复制链接。或者使用附录 B 中所示的机制。

附录 A:gather_on_build.sh

用于在构建机器上收集覆盖率元文件的示例脚本(参见 分离的构建和测试机器 a.

#!/bin/bash

KSRC=$1
KOBJ=$2
DEST=$3

if [ -z "$KSRC" ] || [ -z "$KOBJ" ] || [ -z "$DEST" ]; then
  echo "Usage: $0 <ksrc directory> <kobj directory> <output.tar.gz>" >&2
  exit 1
fi

KSRC=$(cd $KSRC; printf "all:\n\t@echo \${CURDIR}\n" | make -f -)
KOBJ=$(cd $KOBJ; printf "all:\n\t@echo \${CURDIR}\n" | make -f -)

find $KSRC $KOBJ \( -name '*.gcno' -o -name '*.[ch]' -o -type l \) -a \
                 -perm /u+r,g+r | tar cfz $DEST -P -T -

if [ $? -eq 0 ] ; then
  echo "$DEST successfully created, copy to test system and unpack with:"
  echo "  tar xfz $DEST -P"
else
  echo "Could not create file $DEST"
fi

附录 B:gather_on_test.sh

用于在测试机器上收集覆盖率数据文件的示例脚本(参见 分离的构建和测试机器 b.

#!/bin/bash -e

DEST=$1
GCDA=/sys/kernel/debug/gcov

if [ -z "$DEST" ] ; then
  echo "Usage: $0 <output.tar.gz>" >&2
  exit 1
fi

TEMPDIR=$(mktemp -d)
echo Collecting data..
find $GCDA -type d -exec mkdir -p $TEMPDIR/\{\} \;
find $GCDA -name '*.gcda' -exec sh -c 'cat < $0 > '$TEMPDIR'/$0' {} \;
find $GCDA -name '*.gcno' -exec sh -c 'cp -d $0 '$TEMPDIR'/$0' {} \;
tar czf $DEST -C $TEMPDIR sys
rm -rf $TEMPDIR

echo "$DEST successfully created, copy to build system and unpack with:"
echo "  tar xfz $DEST"