Resguardo inicial

This commit is contained in:
lastdragon
2026-09-06 12:53:21 -06:00
parent 662efe3c2a
commit ad2088d178
94853 changed files with 43200404 additions and 0 deletions
@@ -0,0 +1,264 @@
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/dev-tools/gcov.rst
:Translator: 赵军奎 Bernard Zhao <bernard@vivo.com>
在Linux内核里使用gcov做代码覆盖率检查
=====================================
gcov分析核心支持在Linux内核中启用GCC的覆盖率测试工具 gcov_ ,Linux内核
运行时的代码覆盖率数据会以gcov兼容的格式导出到“gcov”debugfs目录中,可
以通过gcov的 ``-o`` 选项(如下示例)获得指定文件的代码运行覆盖率统计数据
(需要跳转到内核编译路径下并且要有root权限)::
# cd /tmp/linux-out
# gcov -o /sys/kernel/debug/gcov/tmp/linux-out/kernel spinlock.c
这将在当前目录中创建带有执行计数注释的源代码文件。
在获得这些统计文件后,可以使用图形化的gcov前端工具(比如 lcov_ ),来实现
自动化处理Linux内核的覆盖率运行数据,同时生成易于阅读的HTML格式文件。
可能的用途:
* 调试(用来判断每一行的代码是否已经运行过)
* 测试改进(如何修改测试代码,尽可能地覆盖到没有运行过的代码)
* 内核最小化配置(对于某一个选项配置,如果关联的代码从来没有运行过,
是否还需要这个配置)
.. _gcov: https://gcc.gnu.org/onlinedocs/gcc/Gcov.html
.. _lcov: http://ltp.sourceforge.net/coverage/lcov.php
准备
----
内核打开如下配置::
CONFIG_DEBUG_FS=y
CONFIG_GCOV_KERNEL=y
获取整个内核的覆盖率数据,还需要打开::
CONFIG_GCOV_PROFILE_ALL=y
需要注意的是,整个内核开启覆盖率统计会造成内核镜像文件尺寸的增大,
同时内核运行也会变慢一些。
另外,并不是所有的架构都支持整个内核开启覆盖率统计。
代码运行覆盖率数据只在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功能需要在debugfs中创建如下文件:
``/sys/kernel/debug/gcov``
gcov相关功能的根路径
``/sys/kernel/debug/gcov/reset``
全局复位文件:向该文件写入数据后会将所有的gcov统计数据清0
``/sys/kernel/debug/gcov/path/to/compile/dir/file.gcda``
gcov工具可以识别的覆盖率统计数据文件,向该文件写入数据后
会将本文件的gcov统计数据清0
``/sys/kernel/debug/gcov/path/to/compile/dir/file.gcno``
gcov工具需要的软连接文件(指向编译时生成的信息统计文件),这个文件是
在gcc编译时如果配置了选项 ``-ftest-coverage`` 时生成的。
针对模块的统计
--------------
内核中的模块会动态的加载和卸载,模块卸载时对应的数据会被清除掉。
gcov提供了一种机制,通过保留相关数据的副本来收集这部分卸载模块的覆盖率数据。
模块卸载后这些备份数据在debugfs中会继续存在。
一旦这个模块重新加载,模块关联的运行统计会被初始化成debugfs中备份的数据。
可以通过对内核参数gcov_persist的修改来停用gcov对模块的备份机制::
gcov_persist = 0
在运行时,用户还可以通过写入模块的数据文件或者写入gcov复位文件来丢弃已卸
载模块的数据。
编译机和测试机分离
------------------
gcov的内核分析插桩支持内核的编译和运行是在同一台机器上,也可以编译和运
行是在不同的机器上。
如果内核编译和运行是不同的机器,那么需要额外的准备工作,这取决于gcov工具
是在哪里使用的:
.. _gcov-test_zh_CN:
a) 若gcov运行在测试机上
测试机上面gcov工具的版本必须要跟内核编译机器使用的gcc版本相兼容,
同时下面的文件要从编译机拷贝到测试机上:
从源代码中:
- 所有的C文件和头文件
从编译目录中:
- 所有的C文件和头文件
- 所有的.gcda文件和.gcno文件
- 所有目录的链接
特别需要注意,测试机器上面的目录结构跟编译机器上面的目录机构必须
完全一致。
如果文件是软链接,需要替换成真正的目录文件(这是由make的当前工作
目录变量CURDIR引起的)。
.. _gcov-build_zh_CN:
b) 若gcov运行在编译机上
测试用例运行结束后,如下的文件需要从测试机中拷贝到编译机上:
从sysfs中的gcov目录中:
- 所有的.gcda文件
- 所有的.gcno文件软链接
这些文件可以拷贝到编译机的任意目录下,gcov使用-o选项指定拷贝的
目录。
比如一个是示例的目录结构如下::
/tmp/linux: 内核源码目录
/tmp/out: 内核编译文件路径(make O=指定)
/tmp/coverage: 从测试机器上面拷贝的数据文件路径
[user@build] cd /tmp/out
[user@build] gcov -o /tmp/coverage/tmp/out/init main.c
关于编译器的注意事项
--------------------
GCC和LLVM gcov工具不一定兼容。
如果编译器是GCC,使用 gcov_ 来处理.gcno和.gcda文件,如果是Clang编译器,
则使用 llvm-cov_ 。
.. _gcov: https://gcc.gnu.org/onlinedocs/gcc/Gcov.html
.. _llvm-cov: https://llvm.org/docs/CommandGuide/llvm-cov.html
GCC和Clang gcov之间的版本差异由Kconfig处理的。
kconfig会根据编译工具链的检查自动选择合适的gcov格式。
问题定位
--------
可能出现的问题1
编译到链接阶段报错终止
问题原因
分析标志指定在了源文件但是没有链接到主内核,或者客制化了链接程序
解决方法
通过在相应的Makefile中使用 ``GCOV_PROFILE := n``
或者 ``GCOV_PROFILE_basename.o := n`` 来将链接报错的文件排除掉
可能出现的问题2
从sysfs复制的文件显示为空或不完整
问题原因
由于seq_file的工作方式,某些工具(例如cp或tar)可能无法正确地从
sysfs复制文件。
解决方法
使用 ``cat`` 读取 ``.gcda`` 文件,使用 ``cp -d`` 复制链接,或者使用附录B
中所示的机制。
附录A:collect_on_build.sh
--------------------------
用于在编译机上收集覆盖率元文件的示例脚本
(见 :ref:`编译机和测试机分离 a. <gcov-test_zh_CN>` )
.. code-block:: sh
#!/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:collect_on_test.sh
-------------------------
用于在测试机上收集覆盖率数据文件的示例脚本
(见 :ref:`编译机和测试机分离 b. <gcov-build_zh_CN>` )
.. code-block:: sh
#!/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"
@@ -0,0 +1,167 @@
.. highlight:: none
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/process/debugging/gdb-kernel-debugging.rst
:Translator: 高超 gao chao <gaochao49@huawei.com>
通过gdb调试内核和模块
=====================
Kgdb内核调试器、QEMU等虚拟机管理程序或基于JTAG的硬件接口,支持在运行时使用gdb
调试Linux内核及其模块。Gdb提供了一个强大的python脚本接口,内核也提供了一套
辅助脚本以简化典型的内核调试步骤。本文档为如何启用和使用这些脚本提供了一个简要的教程。
此教程基于QEMU/KVM虚拟机,但文中示例也适用于其他gdb stub。
环境配置要求
------------
- gdb 7.2+ (推荐版本: 7.4+) 且开启python支持 (通常发行版上都已支持)
设置
----
- 创建一个QEMU/KVM的linux虚拟机(详情请参考 www.linux-kvm.org 和 www.qemu.org )。
对于交叉开发,https://landley.net/aboriginal/bin 提供了一些镜像和工具链,
可以帮助搭建交叉开发环境。
- 编译内核时开启CONFIG_GDB_SCRIPTS,关闭CONFIG_DEBUG_INFO_REDUCED。
如果架构支持CONFIG_FRAME_POINTER,请保持开启。
- 在guest环境上安装该内核。如有必要,通过在内核command line中添加“nokaslr”来关闭KASLR。
此外,QEMU允许通过-kernel、-append、-initrd这些命令行选项直接启动内核。
但这通常仅在不依赖内核模块时才有效。有关此模式的更多详细信息,请参阅QEMU文档。
在这种情况下,如果架构支持KASLR,应该在禁用CONFIG_RANDOMIZE_BASE的情况下构建内核。
- 构建gdb脚本(适用于内核v5.1版本及以上)
make scripts_gdb
- 启用QEMU/KVM的gdb stub,可以通过如下方式实现
- 在VM启动时,通过在QEMU命令行中添加“-s”参数
或
- 在运行时通过从QEMU监视控制台发送“gdbserver”
- 切换到/path/to/linux-build(内核源码编译)目录
- 启动gdb:gdb vmlinux
注意:某些发行版可能会将gdb脚本的自动加载限制在已知的安全目录中。
如果gdb报告拒绝加载vmlinux-gdb.py(相关命令找不到),请将::
add-auto-load-safe-path /path/to/linux-build
添加到~/.gdbinit。更多详细信息,请参阅gdb帮助信息。
- 连接到已启动的guest环境::
(gdb) target remote :1234
使用Linux提供的gdb脚本的示例
----------------------------
- 加载模块(以及主内核)符号::
(gdb) lx-symbols
loading vmlinux
scanning for modules in /home/user/linux/build
loading @0xffffffffa0020000: /home/user/linux/build/net/netfilter/xt_tcpudp.ko
loading @0xffffffffa0016000: /home/user/linux/build/net/netfilter/xt_pkttype.ko
loading @0xffffffffa0002000: /home/user/linux/build/net/netfilter/xt_limit.ko
loading @0xffffffffa00ca000: /home/user/linux/build/net/packet/af_packet.ko
loading @0xffffffffa003c000: /home/user/linux/build/fs/fuse/fuse.ko
...
loading @0xffffffffa0000000: /home/user/linux/build/drivers/ata/ata_generic.ko
- 对一些尚未加载的模块中的函数函数设置断点,例如::
(gdb) b btrfs_init_sysfs
Function "btrfs_init_sysfs" not defined.
Make breakpoint pending on future shared library load? (y or [n]) y
Breakpoint 1 (btrfs_init_sysfs) pending.
- 继续执行::
(gdb) c
- 加载模块并且能观察到正在加载的符号以及断点命中::
loading @0xffffffffa0034000: /home/user/linux/build/lib/libcrc32c.ko
loading @0xffffffffa0050000: /home/user/linux/build/lib/lzo/lzo_compress.ko
loading @0xffffffffa006e000: /home/user/linux/build/lib/zlib_deflate/zlib_deflate.ko
loading @0xffffffffa01b1000: /home/user/linux/build/fs/btrfs/btrfs.ko
Breakpoint 1, btrfs_init_sysfs () at /home/user/linux/fs/btrfs/sysfs.c:36
36 btrfs_kset = kset_create_and_add("btrfs", NULL, fs_kobj);
- 查看内核的日志缓冲区::
(gdb) lx-dmesg
[ 0.000000] Initializing cgroup subsys cpuset
[ 0.000000] Initializing cgroup subsys cpu
[ 0.000000] Linux version 3.8.0-rc4-dbg+ (...
[ 0.000000] Command line: root=/dev/sda2 resume=/dev/sda1 vga=0x314
[ 0.000000] e820: BIOS-provided physical RAM map:
[ 0.000000] BIOS-e820: [mem 0x0000000000000000-0x000000000009fbff] usable
[ 0.000000] BIOS-e820: [mem 0x000000000009fc00-0x000000000009ffff] reserved
....
- 查看当前task struct结构体的字段(仅x86和arm64支持)::
(gdb) p $lx_current().pid
$1 = 4998
(gdb) p $lx_current().comm
$2 = "modprobe\000\000\000\000\000\000\000"
- 对当前或指定的CPU使用per-cpu函数::
(gdb) p $lx_per_cpu(runqueues).nr_running
$3 = 1
(gdb) p $lx_per_cpu(runqueues, 2).nr_running
$4 = 0
- 使用container_of查看更多hrtimers信息::
(gdb) set $leftmost = $lx_per_cpu(hrtimer_bases).clock_base[0].active.rb_root.rb_leftmost
(gdb) p *$container_of($leftmost, "struct hrtimer", "node")
$5 = {
node = {
node = {
__rb_parent_color = 18446612686384860673,
rb_right = 0xffff888231da8b00,
rb_left = 0x0
},
expires = 1228461000000
},
_softexpires = 1228461000000,
function = 0xffffffff8137ab20 <tick_nohz_handler>,
base = 0xffff888231d9b4c0,
state = 1 '\001',
is_rel = 0 '\000',
is_soft = 0 '\000',
is_hard = 1 '\001'
}
命令和辅助调试功能列表
----------------------
命令和辅助调试功能可能会随着时间的推移而改进,此文显示的是初始版本的部分示例::
(gdb) apropos lx
function lx_current -- Return current task
function lx_module -- Find module by name and return the module variable
function lx_per_cpu -- Return per-cpu variable
function lx_task_by_pid -- Find Linux task by PID and return the task_struct variable
function lx_thread_info -- Calculate Linux thread_info from task variable
lx-dmesg -- Print Linux kernel log buffer
lx-lsmod -- List currently loaded modules
lx-symbols -- (Re-)load symbols of Linux kernel and currently loaded modules
可以通过“help <command-name>”或“help function <function-name>”命令
获取指定命令或指定调试功能的更多详细信息。
@@ -0,0 +1,41 @@
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/dev-tools/index.rst
:Translator: 赵军奎 Bernard Zhao <bernard@vivo.com>
============
内核开发工具
============
本文档是有关内核开发工具文档的合集。
目前这些文档已经整理在一起,不需要再花费额外的精力。
欢迎任何补丁。
有关测试专用工具的简要概述,参见
Documentation/translations/zh_CN/dev-tools/testing-overview.rst
.. toctree::
:caption: 目录
:maxdepth: 2
testing-overview
sparse
kcov
kcsan
kmsan
gcov
kasan
ubsan
kmemleak
gdb-kernel-debugging
Todolist:
- checkpatch
- coccinelle
- kfence
- kgdb
- kselftest
- kunit/index
- ktap
- checkuapi
@@ -0,0 +1,476 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/dev-tools/kasan.rst
:Translator: 万家兵 Wan Jiabing <wanjiabing@vivo.com>
内核地址消毒剂(KASAN)
=====================
概述
----
Kernel Address SANitizer(KASAN)是一种动态内存安全错误检测工具,主要功能是
检查内存越界访问和使用已释放内存的问题。
KASAN有三种模式:
1. 通用KASAN
2. 基于软件标签的KASAN
3. 基于硬件标签的KASAN
用CONFIG_KASAN_GENERIC启用的通用KASAN,是用于调试的模式,类似于用户空
间的ASan。这种模式在许多CPU架构上都被支持,但它有明显的性能和内存开销。
基于软件标签的KASAN或SW_TAGS KASAN,通过CONFIG_KASAN_SW_TAGS启用,
可以用于调试和自我测试,类似于用户空间HWASan。这种模式只支持arm64,但其
适度的内存开销允许在内存受限的设备上用真实的工作负载进行测试。
基于硬件标签的KASAN或HW_TAGS KASAN,用CONFIG_KASAN_HW_TAGS启用,被
用作现场内存错误检测器或作为安全缓解的模式。这种模式只在支持MTE(内存标签
扩展)的arm64 CPU上工作,但它的内存和性能开销很低,因此可以在生产中使用。
关于每种KASAN模式的内存和性能影响的细节,请参见相应的Kconfig选项的描述。
通用模式和基于软件标签的模式通常被称为软件模式。基于软件标签的模式和基于
硬件标签的模式被称为基于标签的模式。
支持
----
体系架构
~~~~~~~~
在x86_64、arm、arm64、powerpc、riscv、s390、xtensa和loongarch上支持通用KASAN,
而基于标签的KASAN模式只在arm64上支持。
编译器
~~~~~~
软件KASAN模式使用编译时工具在每个内存访问之前插入有效性检查,因此需要一个
提供支持的编译器版本。基于硬件标签的模式依靠硬件来执行这些检查,但仍然需要
一个支持内存标签指令的编译器版本。
通用KASAN需要GCC 8.3.0版本或更高版本,或者内核支持的任何Clang版本。
基于软件标签的KASAN需要GCC 11+或者内核支持的任何Clang版本。
基于硬件标签的KASAN需要GCC 10+或Clang 12+。
内存类型
~~~~~~~~
通用KASAN支持在所有的slab、page_alloc、vmap、vmalloc、堆栈和全局内存
中查找错误。
基于软件标签的KASAN支持slab、page_alloc、vmalloc和堆栈内存。
基于硬件标签的KASAN支持slab、page_alloc和不可执行的vmalloc内存。
对于slab,两种软件KASAN模式都支持SLUB和SLAB分配器,而基于硬件标签的
KASAN只支持SLUB。
用法
----
要启用KASAN,请使用以下命令配置内核::
CONFIG_KASAN=y
同时在 ``CONFIG_KASAN_GENERIC`` (启用通用KASAN模式), ``CONFIG_KASAN_SW_TAGS``
(启用基于硬件标签的KASAN模式),和 ``CONFIG_KASAN_HW_TAGS`` (启用基于硬件标签
的KASAN模式)之间进行选择。
对于软件模式,还可以在 ``CONFIG_KASAN_OUTLINE`` 和 ``CONFIG_KASAN_INLINE``
之间进行选择。outline和inline是编译器插桩类型。前者产生较小的二进制文件,
而后者快2倍。
要将受影响的slab对象的alloc和free堆栈跟踪包含到报告中,请启用
``CONFIG_STACKTRACE`` 。要包括受影响物理页面的分配和释放堆栈跟踪的话,
请启用 ``CONFIG_PAGE_OWNER`` 并使用 ``page_owner=on`` 进行引导。
启动参数
~~~~~~~~
KASAN受到通用 ``panic_on_warn`` 命令行参数的影响。当它被启用时,KASAN
在打印出错误报告后会使内核恐慌。
默认情况下,KASAN只对第一个无效的内存访问打印错误报告。使用
``kasan_multi_shot``,KASAN对每一个无效的访问都打印一份报告。这会禁用
了KASAN报告的 ``panic_on_warn``。
另外,独立于 ``panic_on_warn`` 、 ``kasan.fault=`` boot参数可以用
来控制恐慌和报告行为。
- ``kasan.fault=report`` 或 ``=panic`` 控制是否只打印KASAN report或
同时使内核恐慌(默认: ``report`` )。即使 ``kasan_multi_shot`` 被
启用,恐慌也会发生。
基于软件和硬件标签的KASAN模式(见下面关于各种模式的部分)支持改变堆栈跟
踪收集行为:
- ``kasan.stacktrace=off`` 或 ``=on`` 禁用或启用分配和释放堆栈痕
迹的收集(默认: ``on`` )。
- ``kasan.stack_ring_size=<number of entries>`` 指定堆栈环的条
目数(默认: ``32768`` )。
基于硬件标签的KASAN模式是为了在生产中作为一种安全缓解措施使用。因此,它
支持额外的启动参数,允许完全禁用KASAN或控制其功能。
- ``kasan=off`` 或 ``=on`` 控制KASAN是否被启用(默认: ``on`` )。
- ``kasan.mode=sync``, ``=async`` or ``=asymm`` 控制KASAN是否
被配置为同步、异步或非对称的执行模式(默认: ``同步`` )。
同步模式:当标签检查异常发生时,会立即检测到不良访问。
异步模式:不良访问的检测是延迟的。当标签检查异常发生时,信息被存储在硬
件中(对于arm64来说是在TFSR_EL1寄存器中)。内核周期性地检查硬件,并\
且只在这些检查中报告标签异常。
非对称模式:读取时同步检测不良访问,写入时异步检测。
- ``kasan.vmalloc=off`` or ``=on`` 禁用或启用vmalloc分配的标记(默认: ``on`` )。
错误报告
~~~~~~~~
典型的KASAN报告如下所示::
==================================================================
BUG: KASAN: slab-out-of-bounds in kmalloc_oob_right+0xa8/0xbc [kasan_test]
Write of size 1 at addr ffff8801f44ec37b by task insmod/2760
CPU: 1 PID: 2760 Comm: insmod Not tainted 4.19.0-rc3+ #698
Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.10.2-1 04/01/2014
Call Trace:
dump_stack+0x94/0xd8
print_address_description+0x73/0x280
kasan_report+0x144/0x187
__asan_report_store1_noabort+0x17/0x20
kmalloc_oob_right+0xa8/0xbc [kasan_test]
kmalloc_tests_init+0x16/0x700 [kasan_test]
do_one_initcall+0xa5/0x3ae
do_init_module+0x1b6/0x547
load_module+0x75df/0x8070
__do_sys_init_module+0x1c6/0x200
__x64_sys_init_module+0x6e/0xb0
do_syscall_64+0x9f/0x2c0
entry_SYSCALL_64_after_hwframe+0x44/0xa9
RIP: 0033:0x7f96443109da
RSP: 002b:00007ffcf0b51b08 EFLAGS: 00000202 ORIG_RAX: 00000000000000af
RAX: ffffffffffffffda RBX: 000055dc3ee521a0 RCX: 00007f96443109da
RDX: 00007f96445cff88 RSI: 0000000000057a50 RDI: 00007f9644992000
RBP: 000055dc3ee510b0 R08: 0000000000000003 R09: 0000000000000000
R10: 00007f964430cd0a R11: 0000000000000202 R12: 00007f96445cff88
R13: 000055dc3ee51090 R14: 0000000000000000 R15: 0000000000000000
Allocated by task 2760:
save_stack+0x43/0xd0
kasan_kmalloc+0xa7/0xd0
kmem_cache_alloc_trace+0xe1/0x1b0
kmalloc_oob_right+0x56/0xbc [kasan_test]
kmalloc_tests_init+0x16/0x700 [kasan_test]
do_one_initcall+0xa5/0x3ae
do_init_module+0x1b6/0x547
load_module+0x75df/0x8070
__do_sys_init_module+0x1c6/0x200
__x64_sys_init_module+0x6e/0xb0
do_syscall_64+0x9f/0x2c0
entry_SYSCALL_64_after_hwframe+0x44/0xa9
Freed by task 815:
save_stack+0x43/0xd0
__kasan_slab_free+0x135/0x190
kasan_slab_free+0xe/0x10
kfree+0x93/0x1a0
umh_complete+0x6a/0xa0
call_usermodehelper_exec_async+0x4c3/0x640
ret_from_fork+0x35/0x40
The buggy address belongs to the object at ffff8801f44ec300
which belongs to the cache kmalloc-128 of size 128
The buggy address is located 123 bytes inside of
128-byte region [ffff8801f44ec300, ffff8801f44ec380)
The buggy address belongs to the page:
page:ffffea0007d13b00 count:1 mapcount:0 mapping:ffff8801f7001640 index:0x0
flags: 0x200000000000100(slab)
raw: 0200000000000100 ffffea0007d11dc0 0000001a0000001a ffff8801f7001640
raw: 0000000000000000 0000000080150015 00000001ffffffff 0000000000000000
page dumped because: kasan: bad access detected
Memory state around the buggy address:
ffff8801f44ec200: fc fc fc fc fc fc fc fc fb fb fb fb fb fb fb fb
ffff8801f44ec280: fb fb fb fb fb fb fb fb fc fc fc fc fc fc fc fc
>ffff8801f44ec300: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 03
^
ffff8801f44ec380: fc fc fc fc fc fc fc fc fb fb fb fb fb fb fb fb
ffff8801f44ec400: fb fb fb fb fb fb fb fb fc fc fc fc fc fc fc fc
==================================================================
报告标题总结了发生的错误类型以及导致该错误的访问类型。紧随其后的是错误访问的
堆栈跟踪、所访问内存分配位置的堆栈跟踪(对于访问了slab对象的情况)以及对象
被释放的位置的堆栈跟踪(对于访问已释放内存的问题报告)。接下来是对访问的
slab对象的描述以及关于访问的内存页的信息。
最后,报告展示了访问地址周围的内存状态。在内部,KASAN单独跟踪每个内存颗粒的
内存状态,根据KASAN模式分为8或16个对齐字节。报告的内存状态部分中的每个数字
都显示了围绕访问地址的其中一个内存颗粒的状态。
对于通用KASAN,每个内存颗粒的大小为8个字节。每个颗粒的状态被编码在一个影子字节
中。这8个字节可以是可访问的,部分访问的,已释放的或成为Redzone的一部分。KASAN
对每个影子字节使用以下编码:00表示对应内存区域的所有8个字节都可以访问;数字N
(1 <= N <= 7)表示前N个字节可访问,其他(8 - N)个字节不可访问;任何负值都表示
无法访问整个8字节。KASAN使用不同的负值来区分不同类型的不可访问内存,如redzones
或已释放的内存(参见 mm/kasan/kasan.h)。
在上面的报告中,箭头指向影子字节 ``03`` ,表示访问的地址是部分可访问的。
对于基于标签的KASAN模式,报告最后的部分显示了访问地址周围的内存标签
(参考 `实施细则`_ 章节)。
请注意,KASAN错误标题(如 ``slab-out-of-bounds`` 或 ``use-after-free`` )
是尽量接近的:KASAN根据其拥有的有限信息打印出最可能的错误类型。错误的实际类型
可能会有所不同。
通用KASAN还报告两个辅助调用堆栈跟踪。这些堆栈跟踪指向代码中与对象交互但不直接
出现在错误访问堆栈跟踪中的位置。目前,这包括 call_rcu() 和排队的工作队列。
CONFIG_KASAN_EXTRA_INFO
~~~~~~~~~~~~~~~~~~~~~~~
启用 CONFIG_KASAN_EXTRA_INFO 选项允许 KASAN 记录和报告更多信息。目前支持的
额外信息包括分配和释放时的 CPU 编号和时间戳。更多的信息可以帮助找到内核错误的原因,
并将错误与其他系统事件关联起来,但代价是用额外的内存来记录更多信息(有关更多
开销的细节,请参见 CONFIG_KASAN_EXTRA_INFO 选项的帮助文本)。
以下为 CONFIG_KASAN_EXTRA_INFO 开启后的报告(仅显示不同部分)::
==================================================================
...
Allocated by task 134 on cpu 5 at 229.133855s:
...
Freed by task 136 on cpu 3 at 230.199335s:
...
==================================================================
实施细则
--------
通用KASAN
~~~~~~~~~
软件KASAN模式使用影子内存来记录每个内存字节是否可以安全访问,并使用编译时工具
在每次内存访问之前插入影子内存检查。
通用KASAN将1/8的内核内存专用于其影子内存(16TB以覆盖x86_64上的128TB),并使用
具有比例和偏移量的直接映射将内存地址转换为其相应的影子地址。
这是将地址转换为其相应影子地址的函数::
static inline void *kasan_mem_to_shadow(const void *addr)
{
return (void *)((unsigned long)addr >> KASAN_SHADOW_SCALE_SHIFT)
+ KASAN_SHADOW_OFFSET;
}
在这里 ``KASAN_SHADOW_SCALE_SHIFT = 3`` 。
编译时工具用于插入内存访问检查。编译器在每次访问大小为1、2、4、8或16的内存之前
插入函数调用( ``__asan_load*(addr)`` , ``__asan_store*(addr)``)。这些函数通过
检查相应的影子内存来检查内存访问是否有效。
使用inline插桩,编译器不进行函数调用,而是直接插入代码来检查影子内存。此选项
显著地增大了内核体积,但与outline插桩内核相比,它提供了x1.1-x2的性能提升。
通用KASAN是唯一一种通过隔离延迟重新使用已释放对象的模式
(参见 mm/kasan/quarantine.c 以了解实现)。
基于软件标签的KASAN模式
~~~~~~~~~~~~~~~~~~~~~~~
基于软件标签的KASAN使用软件内存标签方法来检查访问有效性。目前仅针对arm64架构实现。
基于软件标签的KASAN使用arm64 CPU的顶部字节忽略(TBI)特性在内核指针的顶部字节中
存储一个指针标签。它使用影子内存来存储与每个16字节内存单元相关的内存标签(因此,
它将内核内存的1/16专用于影子内存)。
在每次内存分配时,基于软件标签的KASAN都会生成一个随机标签,用这个标签标记分配
的内存,并将相同的标签嵌入到返回的指针中。
基于软件标签的KASAN使用编译时工具在每次内存访问之前插入检查。这些检查确保正在
访问的内存的标签等于用于访问该内存的指针的标签。如果标签不匹配,基于软件标签
的KASAN会打印错误报告。
基于软件标签的KASAN也有两种插桩模式(outline,发出回调来检查内存访问;inline,
执行内联的影子内存检查)。使用outline插桩模式,会从执行访问检查的函数打印错误
报告。使用inline插桩,编译器会发出 ``brk`` 指令,并使用专用的 ``brk`` 处理程序
来打印错误报告。
基于软件标签的KASAN使用0xFF作为匹配所有指针标签(不检查通过带有0xFF指针标签
的指针进行的访问)。值0xFE当前保留用于标记已释放的内存区域。
基于硬件标签的KASAN模式
~~~~~~~~~~~~~~~~~~~~~~~
基于硬件标签的KASAN在概念上类似于软件模式,但它是使用硬件内存标签作为支持而
不是编译器插桩和影子内存。
基于硬件标签的KASAN目前仅针对arm64架构实现,并且基于ARMv8.5指令集架构中引入
的arm64内存标记扩展(MTE)和最高字节忽略(TBI)。
特殊的arm64指令用于为每次内存分配指定内存标签。相同的标签被指定给指向这些分配
的指针。在每次内存访问时,硬件确保正在访问的内存的标签等于用于访问该内存的指针
的标签。如果标签不匹配,则会生成故障并打印报告。
基于硬件标签的KASAN使用0xFF作为匹配所有指针标签(不检查通过带有0xFF指针标签的
指针进行的访问)。值0xFE当前保留用于标记已释放的内存区域。
如果硬件不支持MTE(ARMv8.5之前),则不会启用基于硬件标签的KASAN。在这种情况下,
所有KASAN引导参数都将被忽略。
请注意,启用CONFIG_KASAN_HW_TAGS始终会导致启用内核中的TBI。即使提供了
``kasan.mode=off`` 或硬件不支持MTE(但支持TBI)。
基于硬件标签的KASAN只报告第一个发现的错误。之后,MTE标签检查将被禁用。
影子内存
--------
本节的内容只适用于软件KASAN模式。
内核将内存映射到地址空间的几个不同部分。内核虚拟地址的范围很大:没有足够的真实
内存来支持内核可以访问的每个地址的真实影子区域。因此,KASAN只为地址空间的某些
部分映射真实的影子。
默认行为
~~~~~~~~
默认情况下,体系结构仅将实际内存映射到用于线性映射的阴影区域(以及可能的其他
小区域)。对于所有其他区域 —— 例如vmalloc和vmemmap空间 —— 一个只读页面被映射
到阴影区域上。这个只读的影子页面声明所有内存访问都是允许的。
这给模块带来了一个问题:它们不存在于线性映射中,而是存在于专用的模块空间中。
通过连接模块分配器,KASAN临时映射真实的影子内存以覆盖它们。例如,这允许检测
对模块全局变量的无效访问。
这也造成了与 ``VMAP_STACK`` 的不兼容:如果堆栈位于vmalloc空间中,它将被分配
只读页面的影子内存,并且内核在尝试为堆栈变量设置影子数据时会出错。
CONFIG_KASAN_VMALLOC
~~~~~~~~~~~~~~~~~~~~
使用 ``CONFIG_KASAN_VMALLOC`` ,KASAN可以以更大的内存使用为代价覆盖vmalloc
空间。目前,这在arm64、x86、riscv、s390和powerpc上受支持。
这通过连接到vmalloc和vmap并动态分配真实的影子内存来支持映射。
vmalloc空间中的大多数映射都很小,需要不到一整页的阴影空间。因此,为每个映射
分配一个完整的影子页面将是一种浪费。此外,为了确保不同的映射使用不同的影子
页面,映射必须与 ``KASAN_GRANULE_SIZE * PAGE_SIZE`` 对齐。
相反,KASAN跨多个映射共享后备空间。当vmalloc空间中的映射使用影子区域的特定
页面时,它会分配一个后备页面。此页面稍后可以由其他vmalloc映射共享。
KASAN连接到vmap基础架构以懒清理未使用的影子内存。
为了避免交换映射的困难,KASAN预测覆盖vmalloc空间的阴影区域部分将不会被早期
的阴影页面覆盖,但是将不会被映射。这将需要更改特定于arch的代码。
这允许在x86上支持 ``VMAP_STACK`` ,并且可以简化对没有固定模块区域的架构的支持。
对于开发者
----------
忽略访问
~~~~~~~~
软件KASAN模式使用编译器插桩来插入有效性检查。此类检测可能与内核的某些部分
不兼容,因此需要禁用。
内核的其他部分可能会访问已分配对象的元数据。通常,KASAN会检测并报告此类访问,
但在某些情况下(例如,在内存分配器中),这些访问是有效的。
对于软件KASAN模式,要禁用特定文件或目录的检测,请将 ``KASAN_SANITIZE`` 添加
到相应的内核Makefile中:
- 对于单个文件(例如,main.o)::
KASAN_SANITIZE_main.o := n
- 对于一个目录下的所有文件::
KASAN_SANITIZE := n
对于软件KASAN模式,要在每个函数的基础上禁用检测,请使用KASAN特定的
``__no_sanitize_address`` 函数属性或通用的 ``noinstr`` 。
请注意,禁用编译器插桩(基于每个文件或每个函数)会使KASAN忽略在软件KASAN模式
的代码中直接发生的访问。当访问是间接发生的(通过调用检测函数)或使用没有编译器
插桩的基于硬件标签的模式时,它没有帮助。
对于软件KASAN模式,要在当前任务的一部分内核代码中禁用KASAN报告,请使用
``kasan_disable_current()``/``kasan_enable_current()`` 部分注释这部分代码。
这也会禁用通过函数调用发生的间接访问的报告。
对于基于标签的KASAN模式,要禁用访问检查,请使用 ``kasan_reset_tag()`` 或
``page_kasan_tag_reset()`` 。请注意,通过 ``page_kasan_tag_reset()``
临时禁用访问检查需要通过 ``page_kasan_tag`` / ``page_kasan_tag_set`` 保
存和恢复每页KASAN标签。
测试
~~~~
有一些KASAN测试可以验证KASAN是否正常工作并可以检测某些类型的内存损坏。
所有 KASAN 测试都与 KUnit 测试框架集成,可通过 ``CONFIG_KASAN_KUNIT_TEST`` 启用。
测试可以通过几种不同的方式自动运行和部分验证;请参阅以下说明。
如果检测到错误,每个 KASAN 测试都会打印多份 KASAN 报告中的一份。
然后测试会打印其编号和状态。
当测试通过::
ok 28 - kmalloc_double_kzfree
当由于 ``kmalloc`` 失败而导致测试失败时::
# kmalloc_large_oob_right: ASSERTION FAILED at mm/kasan/kasan_test.c:245
Expected ptr is not null, but is
not ok 5 - kmalloc_large_oob_right
当由于缺少KASAN报告而导致测试失败时::
# kmalloc_double_kzfree: EXPECTATION FAILED at mm/kasan/kasan_test.c:709
KASAN failure expected in "kfree_sensitive(ptr)", but none occurred
not ok 28 - kmalloc_double_kzfree
最后打印所有KASAN测试的累积状态。成功::
ok 1 - kasan
或者,如果其中一项测试失败::
not ok 1 - kasan
有几种方法可以运行 KASAN 测试。
1. 可加载模块
启用 ``CONFIG_KUNIT`` 后,可以将测试构建为可加载模块
并通过使用 ``insmod`` 或 ``modprobe`` 加载 ``kasan_test.ko`` 来运行。
2. 内置
通过内置 ``CONFIG_KUNIT``,测试也可以内置。
测试将在启动时作为后期初始化调用运行。
3. 使用kunit_tool
通过内置 ``CONFIG_KUNIT`` 和 ``CONFIG_KASAN_KUNIT_TEST`` ,还可以使用
``kunit_tool`` 以更易读的方式查看KUnit测试结果。这不会打印通过测试
的KASAN报告。有关 ``kunit_tool`` 更多最新信息,请参阅
`KUnit文档 <https://www.kernel.org/doc/html/latest/dev-tools/kunit/index.html>`_ 。
.. _KUnit: https://www.kernel.org/doc/html/latest/dev-tools/kunit/index.html
@@ -0,0 +1,359 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/dev-tools/kcov.rst
:Translator: 刘浩阳 Haoyang Liu <tttturtleruss@hust.edu.cn>
KCOV: 用于模糊测试的代码覆盖率
==============================
KCOV 以一种适用于覆盖率引导的模糊测试的形式收集和暴露内核代码覆盖率信息。
一个正在运行的内核的覆盖率数据可以通过 ``kcov`` 调试文件导出。覆盖率的收集是基
于任务启用的,因此 KCOV 可以精确捕获单个系统调用的覆盖率。
要注意的是 KCOV 不是为了收集尽可能多的覆盖率数据。而是为了收集相对稳定的覆盖率
,这是系统调用输入的函数。为了完成这个目标,它不收集软硬中断的覆盖率(除非移除
覆盖率收集被启用,见下文)以及内核中固有的不确定部分的覆盖率(如调度器,锁定)
除了收集代码覆盖率,KCOV 还收集操作数比较的覆盖率。见 "操作数比较收集" 一节
查看详细信息。
除了从系统调用处理器收集覆盖率数据,KCOV 还从后台内核或软中断任务中执行的内核
被标注的部分收集覆盖率。见 "远程覆盖率收集" 一节查看详细信息。
先决条件
--------
KCOV 依赖编译器插桩,要求 GCC 6.1.0 及更高版本或者内核支持的任意版本的 Clang。
收集操作数比较的覆盖率需要 GCC 8+ 或者 Clang。
为了启用 KCOV,需要使用如下参数配置内核::
CONFIG_KCOV=y
为了启用操作数比较覆盖率的收集,使用如下参数::
CONFIG_KCOV_ENABLE_COMPARISONS=y
覆盖率数据只会在调试文件系统被挂载后才可以获取::
mount -t debugfs none /sys/kernel/debug
覆盖率收集
----------
下面的程序演示了如何使用 KCOV 在一个测试程序中收集单个系统调用的覆盖率:
.. code-block:: c
#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;
/* 单个文件描述符允许
* 在单线程上收集覆盖率。
*/
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);
/* 在当前线程中启用覆盖率收集。 */
if (ioctl(fd, KCOV_ENABLE, KCOV_TRACE_PC))
perror("ioctl"), exit(1);
/* 在调用 ioctl() 之后重置覆盖率。 */
__atomic_store_n(&cover[0], 0, __ATOMIC_RELAXED);
/* 调用目标系统调用。 */
read(-1, NULL, 0);
/* 读取收集到的 PC 的数目。 */
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;
}
在使用 ``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``,启用了追踪模式,映射了覆盖率缓冲区,然后在一个循
环中创建了子进程。这个子进程只需要启用覆盖率收集即可(当一个线程退出时将自动禁
用覆盖率收集)。
操作数比较收集
--------------
操作数比较收集和覆盖率收集类似:
.. code-block:: c
/* 包含和上文一样的头文件和宏定义。 */
/* 每次记录的 64 位字的数量。 */
#define KCOV_WORDS_PER_CMP 4
/*
* 收集的比较种类的格式。
*
* 0 比特表示是否是一个编译时常量。
* 1 & 2 比特包含参数大小的 log2 值,最大 8 字节。
*/
#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);
/*
* 注意缓冲区指针的类型是 uint64_t*,因为所有的
* 比较操作数都被提升为 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);
/* 注意这里是 KCOV_TRACE_CMP 而不是 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);
/* 读取收集到的比较操作数的数量。 */
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 和 arg2 - 比较的两个操作数。 */
arg1 = cover[i * KCOV_WORDS_PER_CMP + 2];
arg2 = cover[i * KCOV_WORDS_PER_CMP + 3];
/* ip - 调用者的地址。 */
ip = cover[i * KCOV_WORDS_PER_CMP + 4];
/* 操作数的大小。 */
size = 1 << ((type & KCOV_CMP_MASK) >> 1);
/* is_const - 当操作数是一个编译时常量时为真。*/
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);
/* 释放资源。 */
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_start`` 和 ``kcov_remote_stop`` 来标注要收集
覆盖率的代码片段。
2. 在用户空间的收集覆盖率的进程应使用 ``KCOV_REMOTE_ENABLE`` 而不是 ``KCOV_ENABLE``。
``kcov_remote_start`` 和 ``kcov_remote_stop`` 的标注以及 ``KCOV_REMOTE_ENABLE``
ioctl 都接受可以识别特定覆盖率收集片段的句柄。句柄的使用方式取决于匹配代码片段执
行的上下文。
KCOV 支持在如下上下文中收集远程覆盖率:
1. 全局内核后台任务。这些任务是内核启动时创建的数量有限的实例(如,每一个
USB HCD 产生一个 USB ``hub_event`` 工作器)。
2. 局部内核后台任务。这些任务通常是由于用户空间进程与某些内核接口进行交互时产
生的,并且通常在进程退出时会被停止(如,vhost 工作器)。
3. 软中断。
对于 #1 和 #3,必须选择一个独特的全局句柄并将其传递给对应的
``kcov_remote_start`` 调用。一个用户空间进程必须将该句柄存储在
``kcov_remote_arg`` 结构体的 ``handle`` 数组字段中并将其传递给
``KCOV_REMOTE_ENABLE``。这会将使用的 KCOV 设备附加到由此句柄引用的代码片段。多个全局
句柄标识的不同代码片段可以一次性传递。
对于 #2,用户空间进程必须通过 ``kcov_remote_arg`` 结构体的 ``common_handle`` 字段
传递一个非零句柄。这个通用句柄将会被保存在当前 ``task_struct`` 结构体的
``kcov_handle`` 字段中并且需要通过自定义内核代码的修改来传递给新创建的本地任务
。这些任务需要在 ``kcov_remote_start`` 和 ``kcov_remote_stop`` 标注中依次使用传递过来的
句柄。
KCOV 对全局句柄和通用句柄均遵循一个预定义的格式。每一个句柄都是一个 ``u64`` 整形
。当前,只有最高位和低四位字节被使用。第 4-7 字节是保留位并且值必须为 0。
对于全局句柄,最高位的字节表示该句柄属于的子系统的标识。比如,KCOV 使用 ``1``
表示 USB 子系统类型。全局句柄的低 4 字节表示子系统中任务实例的标识。比如,每一
个 ``hub_event`` 工作器使用 USB 总线号作为任务实例的标识。
对于通用句柄,使用一个保留值 ``0`` 作为子系统标识,因为这些句柄不属于一个特定
的子系统。通用句柄的低 4 字节用于识别有用户进程生成的所有本地句柄的集合实例,
该进程将通用句柄传递给 ``KCOV_REMOTE_ENABLE``。
实际上,如果只从系统中的单个用户空间进程收集覆盖率,那么可以使用任意值作为通用
句柄的实例标识。然而,如果通用句柄被多个用户空间进程使用,每个进程必须使用唯一
的实例标识。一个选择是使用进程标识作为通用句柄实例的标识。
下面的程序演示了如何使用 KCOV 从一个由进程产生的本地任务和处理 USB 总线的全局
任务 #1 收集覆盖率:
.. code-block:: c
/* 包含和上文一样的头文件和宏定义。 */
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);
/* 通过通用句柄和 USB 总线 #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);
/*
* 在这里用户需要触发执行一个内核代码段
* 该代码段要么使用通用句柄标识
* 要么触发了一些 USB 总线 #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;
}
@@ -0,0 +1,320 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/dev-tools/kcsan.rst
:Translator: 刘浩阳 Haoyang Liu <tttturtleruss@hust.edu.cn>
内核并发消毒剂(KCSAN)
=====================
内核并发消毒剂(KCSAN)是一个动态竞争检测器,依赖编译时插桩,并且使用基于观察
点的采样方法来检测竞争。KCSAN 的主要目的是检测 `数据竞争`_。
使用
----
KCSAN 受 GCC 和 Clang 支持。使用 GCC 需要版本 11 或更高,使用 Clang 也需要
版本 11 或更高。
为了启用 KCSAN,用如下参数配置内核::
CONFIG_KCSAN = y
KCSAN 提供了几个其他的配置选项来自定义行为(见 ``lib/Kconfig.kcsan`` 中的各自的
帮助文档以获取更多信息)。
错误报告
~~~~~~~~
一个典型数据竞争的报告如下所示::
==================================================================
BUG: KCSAN: data-race in test_kernel_read / test_kernel_write
write to 0xffffffffc009a628 of 8 bytes by task 487 on cpu 0:
test_kernel_write+0x1d/0x30
access_thread+0x89/0xd0
kthread+0x23e/0x260
ret_from_fork+0x22/0x30
read to 0xffffffffc009a628 of 8 bytes by task 488 on cpu 6:
test_kernel_read+0x10/0x20
access_thread+0x89/0xd0
kthread+0x23e/0x260
ret_from_fork+0x22/0x30
value changed: 0x00000000000009a6 -> 0x00000000000009b2
Reported by Kernel Concurrency Sanitizer on:
CPU: 6 PID: 488 Comm: access_thread Not tainted 5.12.0-rc2+ #1
Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.14.0-2 04/01/2014
==================================================================
报告的头部提供了一个关于竞争中涉及到的函数的简短总结。随后是竞争中的两个线程的
访问类型和堆栈信息。如果 KCSAN 发现了一个值的变化,那么那个值的旧值和新值会在
“value changed”这一行单独显示。
另一个不太常见的数据竞争类型的报告如下所示::
==================================================================
BUG: KCSAN: data-race in test_kernel_rmw_array+0x71/0xd0
race at unknown origin, with read to 0xffffffffc009bdb0 of 8 bytes by task 515 on cpu 2:
test_kernel_rmw_array+0x71/0xd0
access_thread+0x89/0xd0
kthread+0x23e/0x260
ret_from_fork+0x22/0x30
value changed: 0x0000000000002328 -> 0x0000000000002329
Reported by Kernel Concurrency Sanitizer on:
CPU: 2 PID: 515 Comm: access_thread Not tainted 5.12.0-rc2+ #1
Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.14.0-2 04/01/2014
==================================================================
这个报告是当另一个竞争线程不可能被发现,但是可以从观测的内存地址的值改变而推断
出来的时候生成的。这类报告总是会带有“value changed”行。这类报告的出现通常是因
为在竞争线程中缺少插桩,也可能是因为其他原因,比如 DMA 访问。这类报告只会在
设置了内核参数 ``CONFIG_KCSAN_REPORT_RACE_UNKNOWN_ORIGIN=y`` 时才会出现,而这
个参数是默认启用的。
选择性分析
~~~~~~~~~~
对于一些特定的访问,函数,编译单元或者整个子系统,可能需要禁用数据竞争检测。
对于静态黑名单,有如下可用的参数:
* KCSAN 支持使用 ``data_race(expr)`` 注解,这个注解告诉 KCSAN 任何由访问
``expr`` 所引起的数据竞争都应该被忽略,其产生的行为后果被认为是安全的。请查阅
`在 LKMM 中 "标记共享内存访问"`_ 获得更多信息。
* 与 ``data_race(...)`` 相似,可以使用类型限定符 ``__data_racy`` 来标记一个变量
,所有访问该变量而导致的数据竞争都是故意为之并且应该被 KCSAN 忽略::
struct foo {
...
int __data_racy stats_counter;
...
};
* 使用函数属性 ``__no_kcsan`` 可以对整个函数禁用数据竞争检测::
__no_kcsan
void foo(void) {
...
为了动态限制该为哪些函数生成报告,查阅 `Debug 文件系统接口`_ 黑名单/白名单特性。
* 为特定的编译单元禁用数据竞争检测,将下列参数加入到 ``Makefile`` 中::
KCSAN_SANITIZE_file.o := n
* 为 ``Makefile`` 中的所有编译单元禁用数据竞争检测,将下列参数添加到相应的
``Makefile`` 中::
KCSAN_SANITIZE := n
.. _在 LKMM 中 "标记共享内存访问": https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/tools/memory-model/Documentation/access-marking.txt
此外,KCSAN 可以根据偏好设置显示或隐藏整个类别的数据竞争。可以使用如下
Kconfig 参数进行更改:
* ``CONFIG_KCSAN_REPORT_VALUE_CHANGE_ONLY``: 如果启用了该参数并且通过观测点
(watchpoint) 观测到一个有冲突的写操作,但是对应的内存地址中存储的值没有改变,
则不会报告这起数据竞争。
* ``CONFIG_KCSAN_ASSUME_PLAIN_WRITES_ATOMIC``: 假设默认情况下,不超过字大小的简
单对齐写入操作是原子的。假设这些写入操作不会受到不安全的编译器优化影响,从而导
致数据竞争。该选项使 KCSAN 不报告仅由不超过字大小的简单对齐写入操作引起
的冲突所导致的数据竞争。
* ``CONFIG_KCSAN_PERMISSIVE``: 启用额外的宽松规则来忽略某些常见类型的数据竞争。
与上面的规则不同,这条规则更加复杂,涉及到值改变模式,访问类型和地址。这个
选项依赖编译选项 ``CONFIG_KCSAN_REPORT_VALUE_CHANGE_ONLY=y``。请查看
``kernel/kcsan/permissive.h`` 获取更多细节。对于只侧重于特定子系统而不是整个
内核报告的测试者和维护者,建议禁用该选项。
要使用尽可能严格的规则,选择 ``CONFIG_KCSAN_STRICT=y``,这将配置 KCSAN 尽可
能紧密地遵循 Linux 内核内存一致性模型(LKMM)。
Debug 文件系统接口
~~~~~~~~~~~~~~~~~~
文件 ``/sys/kernel/debug/kcsan`` 提供了如下接口:
* 读 ``/sys/kernel/debug/kcsan`` 返回不同的运行时统计数据。
* 将 ``on`` 或 ``off`` 写入 ``/sys/kernel/debug/kcsan`` 允许打开或关闭 KCSAN。
* 将 ``!some_func_name`` 写入 ``/sys/kernel/debug/kcsan`` 会将
``some_func_name`` 添加到报告过滤列表中,该列表(默认)会将数据竞争报告中的顶
层堆栈帧是列表中函数的情况列入黑名单。
* 将 ``blacklist`` 或 ``whitelist`` 写入 ``/sys/kernel/debug/kcsan`` 会改变报告
过滤行为。例如,黑名单的特性可以用来过滤掉经常发生的数据竞争。白名单特性可以帮
助复现和修复测试。
性能调优
~~~~~~~~
影响 KCSAN 整体的性能和 bug 检测能力的核心参数是作为内核命令行参数公开的,其默认
值也可以通过相应的 Kconfig 选项更改。
* ``kcsan.skip_watch`` (``CONFIG_KCSAN_SKIP_WATCH``): 在另一个观测点设置之前每
个 CPU 要跳过的内存操作次数。更加频繁的设置观测点将增加观察到竞争情况的可能性
。这个参数对系统整体的性能和竞争检测能力影响最显著。
* ``kcsan.udelay_task`` (``CONFIG_KCSAN_UDELAY_TASK``): 对于任务,观测点设置之
后暂停执行的微秒延迟。值越大,检测到竞争情况的可能性越高。
* ``kcsan.udelay_interrupt`` (``CONFIG_KCSAN_UDELAY_INTERRUPT``): 对于中断,
观测点设置之后暂停执行的微秒延迟。中断对于延迟的要求更加严格,其延迟通常应该小
于为任务选择的延迟。
它们可以通过 ``/sys/module/kcsan/parameters/`` 在运行时进行调整。
数据竞争
--------
在一次执行中,如果两个内存访问存在 *冲突*,在不同的线程中并发执行,并且至少
有一个访问是 *简单访问*,则它们就形成了 *数据竞争*。如果它们访问了同一个内存地址并且
至少有一个是写操作,则称它们存在 *冲突*。有关更详细的讨论和定义,见
`LKMM 中的 "简单访问和数据竞争"`_。
.. _LKMM 中的 "简单访问和数据竞争": https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/tools/memory-model/Documentation/explanation.txt#n1922
与 Linux 内核内存一致性模型(LKMM)的关系
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
LKMM 定义了各种内存操作的传播和排序规则,让开发者可以推理并发代码。最终这允许确
定并发代码可能的执行情况并判断这些代码是否存在数据竞争。
KCSAN 可以识别 *被标记的原子操作* ( ``READ_ONCE``, ``WRITE_ONCE`` , ``atomic_*``
等),以及内存屏障所隐含的一部分顺序保证。启用 ``CONFIG_KCSAN_WEAK_MEMORY=y``
配置,KCSAN 会对加载或存储缓冲区进行建模,并可以检测遗漏的
``smp_mb()``, ``smp_wmb()``, ``smp_rmb()``, ``smp_store_release()``,以及所有的
具有等效隐含内存屏障的 ``atomic_*`` 操作。
请注意,KCSAN 不会报告所有由于缺失内存顺序而导致的数据竞争,特别是在需要内存屏障
来禁止后续内存操作在屏障之前重新排序的情况下。因此,开发人员应该仔细考虑那些未
被检查的内存顺序要求。
数据竞争以外的竞争检测
---------------------------
对于有着复杂并发设计的代码,竞争状况不总是表现为数据竞争。如果并发操作引起了意
料之外的系统行为,则认为发生了竞争状况。另一方面,数据竞争是在 C 语言层面定义
的。内核定义了一些宏定义用来检测非数据竞争的漏洞并发代码的属性。
.. note::
为了不引入新的文档编译警告,这里不展示宏定义的具体内容,如果想查看具体
宏定义可以结合原文(Documentation/dev-tools/kcsan.rst)阅读。
实现细节
--------
KCSAN 需要观测两个并发访问。特别重要的是,我们想要(a)增加观测到竞争的机会(尤
其是很少发生的竞争),以及(b)能够实际观测到这些竞争。我们可以通过(a)注入
不同的延迟,以及(b)使用地址观测点(或断点)来实现。
如果我们在设置了地址观察点的情况下故意延迟一个内存访问,然后观察到观察点被触发
,那么两个对同一地址的访问就发生了竞争。使用硬件观察点,这是 `DataCollider
<http://usenix.org/legacy/events/osdi10/tech/full_papers/Erickson.pdf>`_ 中采用
的方法。与 DataCollider 不同,KCSAN 不使用硬件观察点,而是依赖于编译器插桩和“软
观测点”。
在 KCSAN 中,观察点是通过一种高效的编码实现的,该编码将访问类型、大小和地址存储
在一个长整型变量中;使用“软观察点”的好处是具有可移植性和更大的灵活性。然后,
KCSAN依赖于编译器对普通访问的插桩。对于每个插桩的普通访问:
1. 检测是否存在一个符合的观测点,如果存在,并且至少有一个操作是写操作,则我们发
现了一个竞争访问。
2. 如果不存在匹配的观察点,则定期的设置一个观测点并随机延迟一小段时间。
3. 在延迟前检查数据值,并在延迟后重新检查数据值;如果值不匹配,我们推测存在一个
未知来源的竞争状况。
为了检测普通访问和标记访问之间的数据竞争,KCSAN 也对标记访问进行标记,但仅用于
检查是否存在观察点;即 KCSAN 不会在标记访问上设置观察点。通过不在标记操作上设
置观察点,如果对一个变量的所有并发访问都被正确标记,KCSAN 将永远不会触发观察点
,因此也不会报告这些访问。
弱内存建模
~~~~~~~~~~
KCSAN 通过建模访问重新排序(使用 ``CONFIG_KCSAN_WEAK_MEMORY=y``)来检测由于缺少
内存屏障而导致的数据竞争。每个设置了观察点的普通内存访问也会被选择在其函数范围
内进行模拟重新排序(最多一个正在进行的访问)。
一旦某个访问被选择用于重新排序,它将在函数范围内与每个其他访问进行检查。如果遇
到适当的内存屏障,该访问将不再被考虑进行模拟重新排序。
当内存操作的结果应该由屏障排序时,KCSAN 可以检测到仅由于缺失屏障而导致的冲突的
数据竞争。考虑下面的例子::
int x, flag;
void T1(void)
{
x = 1; // data race!
WRITE_ONCE(flag, 1); // correct: smp_store_release(&flag, 1)
}
void T2(void)
{
while (!READ_ONCE(flag)); // correct: smp_load_acquire(&flag)
... = x; // data race!
}
当启用了弱内存建模,KCSAN 将考虑对 ``T1`` 中的 ``x`` 进行模拟重新排序。在写入
``flag`` 之后,x再次被检查是否有并发访问:因为 ``T2`` 可以在写入
``flag`` 之后继续进行,因此检测到数据竞争。如果遇到了正确的屏障, ``x`` 在正确
释放 ``flag`` 后将不会被考虑重新排序,因此不会检测到数据竞争。
在复杂性上的权衡以及实际的限制意味着只能检测到一部分由于缺失内存屏障而导致的数
据竞争。由于当前可用的编译器支持,KCSAN 的实现仅限于建模“缓冲”(延迟访问)的
效果,因为运行时不能“预取”访问。同时要注意,观测点只设置在普通访问上,这是唯
一一个 KCSAN 会模拟重新排序的访问类型。这意味着标记访问的重新排序不会被建模。
上述情况的一个后果是获取 (acquire) 操作不需要屏障插桩(不需要预取)。此外,引
入地址或控制依赖的标记访问不需要特殊处理(标记访问不能重新排序,后续依赖的访问
不能被预取)。
关键属性
~~~~~~~~
1. **内存开销**:整体的内存开销只有几 MiB,取决于配置。当前的实现是使用一个小长
整型数组来编码观测点信息,几乎可以忽略不计。
2. **性能开销**:KCSAN 的运行时旨在性能开销最小化,使用一个高效的观测点编码,在
快速路径中不需要获取任何锁。在拥有 8 个 CPU 的系统上的内核启动来说:
- 使用默认 KCSAN 配置时,性能下降 5 倍;
- 仅因运行时快速路径开销导致性能下降 2.8 倍(设置非常大的
``KCSAN_SKIP_WATCH`` 并取消设置 ``KCSAN_SKIP_WATCH_RANDOMIZE``)。
3. **注解开销**:KCSAN 运行时之外需要的注释很少。因此,随着内核的发展维护的开
销也很小。
4. **检测设备的竞争写入**:由于设置观测点时会检查数据值,设备的竞争写入也可以
被检测到。
5. **内存排序**:KCSAN 只了解一部分 LKMM 排序规则;这可能会导致漏报数据竞争(
假阴性)。
6. **分析准确率**: 对于观察到的执行,由于使用采样策略,分析是 *不健全* 的
(可能有假阴性),但期望得到完整的分析(没有假阳性)。
考虑的替代方案
--------------
一个内核数据竞争检测的替代方法是 `Kernel Thread Sanitizer (KTSAN)
<https://github.com/google/kernel-sanitizers/blob/master/KTSAN.md>`_。KTSAN 是一
个基于先行发生关系(happens-before)的数据竞争检测器,它显式建立内存操作之间的先
后发生顺序,这可以用来确定 `数据竞争`_ 中定义的数据竞争。
为了建立正确的先行发生关系,KTSAN 必须了解 LKMM 的所有排序规则和同步原语。不幸
的是,任何遗漏都会导致大量的假阳性,这在包含众多自定义同步机制的内核上下文中特
别有害。为了跟踪前因后果关系,KTSAN 的实现需要为每个内存位置提供元数据(影子内
存),这意味着每页内存对应 4 页影子内存,在大型系统上可能会带来数十 GiB 的开销
。
@@ -0,0 +1,229 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/dev-tools/kmemleak.rst
:Translator: 刘浩阳 Haoyang Liu <tttturtleruss@hust.edu.cn>
内核内存泄露检测器
==================
Kmemleak 提供了一个类似 `可追踪的垃圾收集器 <https://en.wikipedia.org/wiki/Tra
cing_garbage_collection>`_ 的方法来检测可能的内核内存泄漏,不同的是孤立对象不会
被释放,而是仅通过 /sys/kernel/debug/kmemleak 报告。Valgrind 工具
(``memcheck --leak-check``)使用了一种相似的方法来检测用户空间应用中的内存泄
露。
用法
----
"Kernel hacking" 中的 CONFIG_DEBUG_KMEMLEAK 必须被启用。一个内核线程每10分钟
(默认情况下)扫描一次内存,并且打印出新发现的未被引用的对象个数。
如果 ``debugfs`` 没有挂载,则执行::
# mount -t debugfs nodev /sys/kernel/debug/
显示所有扫描出的可能的内存泄漏的细节信息::
# cat /sys/kernel/debug/kmemleak
启动一次中等程度的内存扫描::
# echo scan > /sys/kernel/debug/kmemleak
清空当前所有可能的内存泄露列表::
# echo clear > /sys/kernel/debug/kmemleak
当再次读取 ``/sys/kernel/debug/kmemleak`` 文件时,将会输出自上次扫描以来检测到的
新的内存泄露。
注意,孤立目标是通过被分配时间来排序的,列表开始的对象可能会导致后续的对象都被
识别为孤立对象。
可以通过写入 ``/sys/kernel/debug/kmemleak`` 文件在运行时修改内存扫描参数。下面是
支持的参数:
* off
禁用 kmemleak(不可逆)
* stack=on
开启任务栈扫描(默认)
* stack=off
禁用任务栈扫描
* scan=on
开启自动内存扫描线程(默认)
* scan=off
关闭自动内存扫描线程
* scan=<secs>;
设定自动内存扫描间隔,以秒为单位(默认值为 600,设置为 0 表示停
止自动扫描)
* scan
触发一次内存扫描
* clear
通过标记所有当前已报告的未被引用对象为灰,从而清空当前可能的内存泄露列
表;如果 kmemleak 被禁用,则释放所有 kmemleak 对象,。
* dump=<addr>
输出存储在 <addr> 中的对象信息
可以通过在内核命令行中传递 ``kmemleak=off`` 参数从而在启动时禁用 Kmemleak。
在 kmemleak 初始化之前就可能会有内存分配或释放,这些操作被存储在一个早期日志缓
冲区中。缓冲区的大小通过 CONFIG_DEBUG_KMEMLEAK_MEM_POOL_SIZE 选项配置。
如果 CONFIG_DEBUG_KMEMLEAK_DEFAULT_OFF 被启用,则 kmemleak 默认被禁用。在内核命
令行中传递 ``kmemleak=on`` 参数来开启这个功能。
如果出现 "Error while writing to stdout" 或 "write_loop: Invalid argument" 这样
的错误,请确认 kmemleak 被正确启用。
基础算法
--------
通过 :c:func:`kmalloc`, :c:func:`vmalloc`, :c:func:`kmem_cache_alloc` 以及同类
函数均被跟踪,指针,包括一些额外的信息如大小和栈追踪等,都被存储在红黑树中。
对应的释放函数调用也被追踪,并从 kmemleak 数据结构中移除相应指针。
对于一个已分配的内存块,如果通过扫描内存(包括保存寄存器)没有发现任何指针指向
它的起始地址或者其中的任何位置,则认为这块内存是孤立的。这意味着内核无法将该内
存块的地址传递给一个释放内存函数,这块内存便被认为泄露了。
扫描算法步骤:
1. 标记所有对象为白色(最后剩下的白色对象被认为是孤立的)
2. 从数据节和栈开始扫描内存,检测每个值是否是红黑树中存储的地址。如果一个指向
白色对象的指针被检测到,则将该对象标记为灰色。
3. 扫描灰色对象引用的其他对象(有些白色对象可能会变为灰色并被添加到灰名单末尾
)直到灰名单为空。
4. 剩余的白色对象就被认为是孤立的并通过 /sys/kernel/debug/kmemleak 报告。
有些指向已分配的内存块的指针存储在内核内部的数据结构中,它们不能被检测为孤立。
为了避免这种情况,kmemleak 也存储了指向需要被查找的内存块范围内的任意地址的地址
数量,如此一来这些内存便不会被认为泄露。一个例子是 __vmalloc()。
用 kmemleak 测试特定部分
------------------------
在初始化启动阶段 /sys/kernel/debug/kmemleak 的输出可能会很多,这也可能是你在开发
时编写的漏洞百出的代码导致的。为了解决这种情况你可以使用 'clear' 命令来清除
/sys/kernel/debug/kmemleak 输出的所有的未引用对象。在执行 'clear' 后执行 'scan'
可以发现新的未引用对象,这将会有利你测试代码的特定部分。
为了用一个空的 kmemleak 测试一个特定部分,执行::
# echo clear > /sys/kernel/debug/kmemleak
... 测试你的内核或者模块 ...
# echo scan > /sys/kernel/debug/kmemleak
然后像平常一样获得报告::
# cat /sys/kernel/debug/kmemleak
释放 kmemleak 内核对象
----------------------
为了允许访问先前发现的内存泄露,当用户禁用或发生致命错误导致 kmemleak
被禁用时,内核中的 kmemleak 对象不会被释放。这些对象可能会占用很大
一部分物理内存。
在这种情况下,你可以用如下命令回收这些内存::
# echo clear > /sys/kernel/debug/kmemleak
Kmemleak API
------------
在 include/linux/kmemleak.h 头文件中查看函数原型:
- ``kmemleak_init`` - 初始化 kmemleak
- ``kmemleak_alloc`` - 通知一个内存块的分配
- ``kmemleak_alloc_percpu`` - 通知一个 percpu 类型的内存分配
- ``kmemleak_vmalloc`` - 通知一个使用 vmalloc() 的内存分配
- ``kmemleak_free`` - 通知一个内存块的释放
- ``kmemleak_free_part`` - 通知一个部分的内存释放
- ``kmemleak_free_percpu`` - 通知一个 percpu 类型的内存释放
- ``kmemleak_update_trace`` - 更新分配对象过程的栈追踪
- ``kmemleak_not_leak`` - 标记一个对象内存为未泄露的
- ``kmemleak_ignore`` - 不要扫描或报告某个对象未泄露的
- ``kmemleak_scan_area`` - 在内存块中添加扫描区域
- ``kmemleak_no_scan`` - 不扫描某个内存块
- ``kmemleak_erase`` - 在指针变量中移除某个旧的值
- ``kmemleak_alloc_recursive`` - 和 kmemleak_alloc 效果相同但会检查是否有递归的
内存分配
- ``kmemleak_free_recursive`` - 和 kmemleak_free 效果相同但会检查是否有递归的
内存释放
下列函数使用一个物理地址作为对象指针并且只在地址有一个 lowmem 映射时做出相应的
行为:
- ``kmemleak_alloc_phys``
- ``kmemleak_free_part_phys``
- ``kmemleak_ignore_phys``
解决假阳性/假阴性
-----------------
假阴性是指由于在内存扫描中有值指向该对象导致 kmemleak 没有报告的实际存在的内存
泄露(孤立对象)。为了减少假阴性的出现次数,kmemleak 提供了 kmemleak_ignore,
kmemleak_scan_area,kmemleak_no_scan 和 kmemleak_erase 函数(见上)。
任务栈也会增加假阴性的数量并且默认不开启对它们的扫描。
假阳性是对象被误报为内存泄露(孤立对象)。对于已知未泄露的对象,kmemleak
提供了 kmemleak_not_leak 函数。同时 kmemleak_ignore 可以用于标记已知不包含任何
其他指针的内存块,标记后该内存块不会再被扫描。
一些被报告的泄露仅仅是暂时的,尤其是在 SMP(对称多处理)系统中,因为其指针
暂存在 CPU 寄存器或栈中。Kmemleak 定义了 MSECS_MIN_AGE(默认值为 1000)
来表示一个被报告为内存泄露的对象的最小存活时间。
限制和缺点
----------
主要的缺点是内存分配和释放的性能下降。为了避免其他的损失,只有当
/sys/kernel/debug/kmemleak 文件被读取时才会进行内存扫描。无论如何,这个工具是出于
调试的目标,性能表现可能不是最重要的。
为了保持算法简单,kmemleak 寻找指向某个内存块范围中的任何值。这可能会引发假阴性
现象的出现。但是,最后一个真正的内存泄露也会变得明显。
非指针值的数据是假阴性的另一个来源。在将来的版本中,kmemleak 仅仅会扫
描已分配结构体中的指针成员。这个特性会解决上述很多的假阴性情况。
Kmemleak 会报告假阳性。这可能发生在某些被分配的内存块不需要被释放的情况下
(某些 init_call 函数中),指针的计算是通过其他方法而不是常规的 container_of 宏
或是指针被存储在 kmemleak 没有扫描的地方。
页分配和 ioremap 不会被追踪。
使用 kmemleak-test 测试
-----------------------
为了检测是否成功启用了 kmemleak,你可以使用一个故意制造内存泄露的模块
kmemleak-test。设置 CONFIG_SAMPLE_KMEMLEAK 为模块(不能作为内建模块使用)
并且启动启用了 kmemleak 的内核。加载模块并执行一次扫描::
# modprobe kmemleak-test
# echo scan > /sys/kernel/debug/kmemleak
注意你可能无法立刻或在第一次扫描后得到结果。当 kmemleak 得到结果,将会输出日
志 ``kmemleak: <count of leaks> new suspected memory leaks`` 。然后通过读取文件
获取信息::
# cat /sys/kernel/debug/kmemleak
unreferenced object 0xffff89862ca702e8 (size 32):
comm "modprobe", pid 2088, jiffies 4294680594 (age 375.486s)
hex dump (first 32 bytes):
6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b kkkkkkkkkkkkkkkk
6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b a5 kkkkkkkkkkkkkkk.
backtrace:
[<00000000e0a73ec7>] 0xffffffffc01d2036
[<000000000c5d2a46>] do_one_initcall+0x41/0x1df
[<0000000046db7e0a>] do_init_module+0x55/0x200
[<00000000542b9814>] load_module+0x203c/0x2480
[<00000000c2850256>] __do_sys_finit_module+0xba/0xe0
[<000000006564e7ef>] do_syscall_64+0x43/0x110
[<000000007c873fa6>] entry_SYSCALL_64_after_hwframe+0x44/0xa9
...
用 ``rmmod kmemleak_test`` 移除模块时也会触发
kmemleak 的结果输出。
@@ -0,0 +1,392 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/dev-tools/kmsan.rst
:Translator: 刘浩阳 Haoyang Liu <tttturtleruss@hust.edu.cn>
=======================
内核内存消毒剂(KMSAN)
=======================
KMSAN 是一个动态错误检测器,旨在查找未初始化值的使用。它基于编译器插桩,类似于用
户空间的 `MemorySanitizer tool`_。
需要注意的是 KMSAN 并不适合生产环境,因为它会大幅增加内核内存占用并降低系统运行速度。
使用方法
========
构建内核
--------
要构建带有 KMSAN 的内核,你需要一个较新的 Clang (14.0.6+)。
请参阅 `LLVM documentation`_ 了解如何构建 Clang。
现在配置并构建一个启用 CONFIG_KMSAN 的内核。
示例报告
--------
以下是一个 KMSAN 报告的示例::
=====================================================
BUG: KMSAN: uninit-value in test_uninit_kmsan_check_memory+0x1be/0x380 [kmsan_test]
test_uninit_kmsan_check_memory+0x1be/0x380 mm/kmsan/kmsan_test.c:273
kunit_run_case_internal lib/kunit/test.c:333
kunit_try_run_case+0x206/0x420 lib/kunit/test.c:374
kunit_generic_run_threadfn_adapter+0x6d/0xc0 lib/kunit/try-catch.c:28
kthread+0x721/0x850 kernel/kthread.c:327
ret_from_fork+0x1f/0x30 ??:?
Uninit was stored to memory at:
do_uninit_local_array+0xfa/0x110 mm/kmsan/kmsan_test.c:260
test_uninit_kmsan_check_memory+0x1a2/0x380 mm/kmsan/kmsan_test.c:271
kunit_run_case_internal lib/kunit/test.c:333
kunit_try_run_case+0x206/0x420 lib/kunit/test.c:374
kunit_generic_run_threadfn_adapter+0x6d/0xc0 lib/kunit/try-catch.c:28
kthread+0x721/0x850 kernel/kthread.c:327
ret_from_fork+0x1f/0x30 ??:?
Local variable uninit created at:
do_uninit_local_array+0x4a/0x110 mm/kmsan/kmsan_test.c:256
test_uninit_kmsan_check_memory+0x1a2/0x380 mm/kmsan/kmsan_test.c:271
Bytes 4-7 of 8 are uninitialized
Memory access of size 8 starts at ffff888083fe3da0
CPU: 0 PID: 6731 Comm: kunit_try_catch Tainted: G B E 5.16.0-rc3+ #104
Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.14.0-2 04/01/2014
=====================================================
报告指出本地变量 ``uninit`` 在 ``do_uninit_local_array()`` 中未初始化。
第三个堆栈跟踪对应于该变量创建的位置。
第一个堆栈跟踪显示了未初始化值的使用位置(在
``test_uninit_kmsan_check_memory()``)。
工具显示了局部变量中未初始化的字节及其被复制到其他内存位置前的堆栈。
KMSAN 会在以下情况下报告未初始化的值 ``v``:
- 在条件判断中,例如 ``if (v) { ... }``;
- 在索引或指针解引用中,例如 ``array[v]`` 或 ``*v``;
- 当它被复制到用户空间或硬件时,例如 ``copy_to_user(..., &v, ...)``;
- 当它作为函数参数传递,并且启用 ``CONFIG_KMSAN_CHECK_PARAM_RETVAL`` 时(见下文)。
这些情况(除了复制数据到用户空间或硬件外,这是一个安全问题)被视为 C11 标准下的未定义行为。
禁用插桩
--------
可以用 ``__no_kmsan_checks`` 标记函数。这样,KMSAN 会忽略该函数中的未初始化值,
并将其输出标记为已初始化。如此,用户不会收到与该函数相关的 KMSAN 报告。
KMSAN 还支持 ``__no_sanitize_memory`` 函数属性。KMSAN 不会对拥有该属性的函数进行
插桩,这在我们不希望编译器干扰某些底层代码(例如标记为 ``noinstr`` 的代码,该
代码隐式添加了 ``__no_sanitize_memory``)时可能很有用。
然而,这会有代价:此类函数的栈分配将具有不正确的影子/初始值,可能导致误报。来
自非插桩代码的函数也可能接收到不正确的元数据。
作为经验之谈,避免显式使用 ``__no_sanitize_memory``。
也可以通过 Makefile 禁用 KMSAN 对某个文件(例如 main.o)的作用::
KMSAN_SANITIZE_main.o := n
或者对整个目录::
KMSAN_SANITIZE := n
将其应用到文件或目录中的每个函数。大多数用户不会需要 KMSAN_SANITIZE,
除非他们的代码被 KMSAN 破坏(例如在早期启动时运行的代码)。
还可以通过调用 ``kmsan_disable_current()`` 和 ``kmsan_enable_current()``
暂时对当前任务禁用 KMSAN 检查。每个 ``kmsan_enable_current()`` 必须在
``kmsan_disable_current()`` 之后调用;这些调用对可以嵌套。在调用时需要注意保持
嵌套区域简短,并且尽可能使用其他方法禁用插桩。
支持
====
为了使用 KMSAN,内核必须使用 Clang 构建,到目前为止,Clang 是唯一支持 KMSAN
的编译器。内核插桩过程基于用户空间的 `MemorySanitizer tool`_。
目前运行时库仅支持 x86_64 架构。
KMSAN 的工作原理
================
KMSAN 阴影内存
--------------
KMSAN 将一个元数据字节(也称为阴影字节)与每个内核内存字节关联。仅当内核内存字节
的相应位未初始化时,阴影字节中的一个比特位才会被设置。将内存标记为未初始化(即
将其阴影字节设置为 ``0xff``)称为中毒,将其标记为已初始化(将阴影字节设置为
``0x00``)称为解毒。
当在栈上分配新变量时,默认情况下它会中毒,这由编译器插入的插桩代码完成(除非它
是立即初始化的栈变量)。任何未使用 ``__GFP_ZERO`` 的堆分配也会中毒。
编译器插桩还跟踪阴影值在代码中的使用。当需要时,插桩代码会调用 ``mm/kmsan/`` 中
的运行时库以持久化阴影值。
基本或复合类型的阴影值是长度相同的字节数组。当常量值写入内存时,该内存会被解毒
。当从内存读取值时,其阴影内存也会被获取,并传递到所有使用该值的操作中。对于每
个需要一个或多个值的指令,编译器会生成代码根据这些值及其阴影来计算结果的阴影。
示例::
int a = 0xff; // i.e. 0x000000ff
int b;
int c = a | b;
在这种情况下, ``a`` 的阴影为 ``0``, ``b`` 的阴影为 ``0xffffffff``,
``c`` 的阴影为 ``0xffffff00``。这意味着 ``c`` 的高三个字节未初始化,而低字节已
初始化。
起源跟踪
--------
每四字节的内核内存都有一个所谓的源点与之映射。这个源点描述了在程序执行中,未初
始化值的创建点。每个源点都与完整的分配栈(对于堆分配的内存)或包含未初始化变
量的函数(对于局部变量)相关联。
当一个未初始化的变量在栈或堆上分配时,会创建一个新的源点值,并将该变量的初始值
填充为这个值。当从内存中读取一个值时,其初始值也会被读取并与阴影一起保留。对于
每个接受一个或多个值的指令,结果的源点是与任何未初始化输入相对应的源点之一。如
果一个污染值被写入内存,其起源也会被写入相应的存储中。
示例 1::
int a = 42;
int b;
int c = a + b;
在这种情况下, ``b`` 的源点是在函数入口时生成的,并在加法结果写入内存之前存储到
``c`` 的源点中。
如果几个变量共享相同的源点地址,则它们被存储在同一个四字节块中。在这种情况下,
对任何变量的每次写入都会更新所有变量的源点。在这种情况下我们必须牺牲精度,因
为为单独的位(甚至字节)存储源点成本过高。
示例 2::
int combine(short a, short b) {
union ret_t {
int i;
short s[2];
} ret;
ret.s[0] = a;
ret.s[1] = b;
return ret.i;
}
如果 ``a`` 已初始化而 ``b`` 未初始化,则结果的阴影为 0xffff0000,结果的源点为
``b`` 的源点。 ``ret.s[0]`` 会有相同的起源,但它不会被使用,因为该变量已初始化。
如果两个函数参数都未初始化,则只保留第二个参数的源点。
源点链
~~~~~~
为了便于调试,KMSAN 在每次将未初始化值存储到内存时都会创建一个新的源点。新的源点
引用了其创建栈以及值的前一个起源。这可能导致内存消耗增加,因此我们在运行时限制
了源点链的长度。
Clang 插桩 API
--------------
Clang 插桩通过在内核代码中插入定义在 ``mm/kmsan/instrumentation.c`` 中的函数调用
来实现。
阴影操作
~~~~~~~~
对于每次内存访问,编译器都会发出一个函数调用,该函数返回一对指针,指向给定内存
的阴影和原始地址::
typedef struct {
void *shadow, *origin;
} shadow_origin_ptr_t
shadow_origin_ptr_t __msan_metadata_ptr_for_load_{1,2,4,8}(void *addr)
shadow_origin_ptr_t __msan_metadata_ptr_for_store_{1,2,4,8}(void *addr)
shadow_origin_ptr_t __msan_metadata_ptr_for_load_n(void *addr, uintptr_t size)
shadow_origin_ptr_t __msan_metadata_ptr_for_store_n(void *addr, uintptr_t size)
函数名依赖于内存访问的大小。
编译器确保对于每个加载的值,其阴影和原始值都从内存中读取。当一个值存储到内存时
,其阴影和原始值也会通过元数据指针进行存储。
处理局部变量
~~~~~~~~~~~~
一个特殊的函数用于为局部变量创建一个新的原始值,并将该变量的原始值设置为该值::
void __msan_poison_alloca(void *addr, uintptr_t size, char *descr)
访问每个任务数据
~~~~~~~~~~~~~~~~
在每个插桩函数的开始处,KMSAN 插入一个对 ``__msan_get_context_state()`` 的调用
::
kmsan_context_state *__msan_get_context_state(void)
``kmsan_context_state`` 在 ``include/linux/kmsan.h`` 中声明::
struct kmsan_context_state {
char param_tls[KMSAN_PARAM_SIZE];
char retval_tls[KMSAN_RETVAL_SIZE];
char va_arg_tls[KMSAN_PARAM_SIZE];
char va_arg_origin_tls[KMSAN_PARAM_SIZE];
u64 va_arg_overflow_size_tls;
char param_origin_tls[KMSAN_PARAM_SIZE];
depot_stack_handle_t retval_origin_tls;
};
KMSAN 使用此结构体在插桩函数之间传递参数阴影和原始值(除非立刻通过
``CONFIG_KMSAN_CHECK_PARAM_RETVAL`` 检查参数)。
将未初始化的值传递给函数
~~~~~~~~~~~~~~~~~~~~~~~~
Clang 的 MemorySanitizer 插桩有一个选项 ``-fsanitize-memory-param-retval``,该
选项使编译器检查按值传递的函数参数,以及函数返回值。
该选项由 ``CONFIG_KMSAN_CHECK_PARAM_RETVAL`` 控制,默认启用以便 KMSAN 更早报告
未初始化的值。有关更多细节,请参考 `LKML discussion`_。
由于 LLVM 中的实现检查的方式(它们仅应用于标记为 ``noundef`` 的参数),并不是所
有参数都能保证被检查,因此我们不能放弃 ``kmsan_context_state`` 中的元数据存储
。
字符串函数
~~~~~~~~~~~
编译器将对 ``memcpy()``/``memmove()``/``memset()`` 的调用替换为以下函数。这些函
数在数据结构初始化或复制时也会被调用,确保阴影和原始值与数据一起复制::
void *__msan_memcpy(void *dst, void *src, uintptr_t n)
void *__msan_memmove(void *dst, void *src, uintptr_t n)
void *__msan_memset(void *dst, int c, uintptr_t n)
错误报告
~~~~~~~~
对于每个值的使用,编译器发出一个阴影检查,在值中毒的情况下调用
``__msan_warning()``::
void __msan_warning(u32 origin)
``__msan_warning()`` 使 KMSAN 运行时打印错误报告。
内联汇编插桩
~~~~~~~~~~~~
KMSAN 对每个内联汇编输出进行插桩,调用::
void __msan_instrument_asm_store(void *addr, uintptr_t size)
,该函数解除内存区域的污染。
这种方法可能会掩盖某些错误,但也有助于避免许多位操作、原子操作等中的假阳性。
有时传递给内联汇编的指针不指向有效内存。在这种情况下,它们在运行时被忽略。
运行时库
--------
代码位于 ``mm/kmsan/``。
每个任务 KMSAN 状态
~~~~~~~~~~~~~~~~~~~
每个 task_struct 都有一个关联的 KMSAN 任务状态,它保存 KMSAN
上下文(见上文)和一个每个任务计数器以禁止 KMSAN 报告::
struct kmsan_context {
...
unsigned int depth;
struct kmsan_context_state cstate;
...
}
struct task_struct {
...
struct kmsan_context kmsan;
...
}
KMSAN 上下文
~~~~~~~~~~~~
在内核任务上下文中运行时,KMSAN 使用 ``current->kmsan.cstate`` 来
保存函数参数和返回值的元数据。
但在内核运行于中断、softirq 或 NMI 上下文中, ``current`` 不可用时,
KMSAN 切换到每 CPU 中断状态::
DEFINE_PER_CPU(struct kmsan_ctx, kmsan_percpu_ctx);
元数据分配
~~~~~~~~~~
内核中有多个地方存储元数据。
1. 每个 ``struct page`` 实例包含两个指向其影子和内存页面的指针
::
struct page {
...
struct page *shadow, *origin;
...
};
在启动时,内核为每个可用的内核页面分配影子和源页面。这是在内核地址空间已经碎片
化时后完成的,完成的相当晚,因此普通数据页面可能与元数据页面任意交错。
这意味着通常两个相邻的内存页面,它们的影子/源页面可能不是连续的。因此,如果内存
访问跨越内存块的边界,访问影子/源内存可能会破坏其他页面或从中读取错误的值。
实际上,由相同 ``alloc_pages()`` 调用返回的连续内存页面将具有连续的元数据,而
如果这些页面属于两个不同的分配,它们的元数据页面可能会被碎片化。
对于内核数据( ``.data``、 ``.bss`` 等)和每 CPU 内存区域,也没有对元数据连续
性的保证。
在 ``__msan_metadata_ptr_for_XXX_YYY()`` 遇到两个页面之间的
非连续元数据边界时,它返回指向假影子/源区域的指针::
char dummy_load_page[PAGE_SIZE] __attribute__((aligned(PAGE_SIZE)));
char dummy_store_page[PAGE_SIZE] __attribute__((aligned(PAGE_SIZE)));
``dummy_load_page`` 被初始化为零,因此读取它始终返回零。对 ``dummy_store_page`` 的
所有写入都被忽略。
2. 对于 vmalloc 内存和模块,内存范围、影子和源之间有一个直接映射。KMSAN 将
vmalloc 区域缩小了 3/4,仅使前四分之一可用于 ``vmalloc()``。vmalloc
区域的第二个四分之一包含第一个四分之一的影子内存,第三个四分之一保存源。第四个
四分之一的小部分包含内核模块的影子和源。有关更多详细信息,请参阅
``arch/x86/include/asm/pgtable_64_types.h``。
当一系列页面映射到一个连续的虚拟内存空间时,它们的影子和源页面也以连续区域的方
式映射。
参考文献
========
E. Stepanov, K. Serebryany. `MemorySanitizer: fast detector of uninitialized
memory use in C++
<https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/43308.pdf>`_.
In Proceedings of CGO 2015.
.. _MemorySanitizer tool: https://clang.llvm.org/docs/MemorySanitizer.html
.. _LLVM documentation: https://llvm.org/docs/GettingStarted.html
.. _LKML discussion: https://lore.kernel.org/all/20220614144853.3693273-1-glider@google.com/
@@ -0,0 +1,110 @@
Copyright 2004 Linus Torvalds
Copyright 2004 Pavel Machek <pavel@ucw.cz>
Copyright 2006 Bob Copeland <me@bobcopeland.com>
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/dev-tools/sparse.rst
:翻译:
Li Yang <leoyang.li@nxp.com>
:校译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
.. _cn_sparse:
Sparse
======
Sparse是一个C程序的语义检查器;它可以用来发现内核代码的一些潜在问题。 关
于sparse的概述,请参见https://lwn.net/Articles/689907/;本文档包含
一些针对内核的sparse信息。
关于sparse的更多信息,主要是关于它的内部结构,可以在它的官方网页上找到:
https://sparse.docs.kernel.org。
使用 sparse 工具做类型检查
~~~~~~~~~~~~~~~~~~~~~~~~~~
"__bitwise" 是一种类型属性,所以你应该这样使用它::
typedef int __bitwise pm_request_t;
enum pm_request {
PM_SUSPEND = (__force pm_request_t) 1,
PM_RESUME = (__force pm_request_t) 2
};
这样会使 PM_SUSPEND 和 PM_RESUME 成为位方式(bitwise)整数(使用"__force"
是因为 sparse 会抱怨改变位方式的类型转换,但是这里我们确实需要强制进行转
换)。而且因为所有枚举值都使用了相同的类型,这里的"enum pm_request"也将
会使用那个类型做为底层实现。
而且使用 gcc 编译的时候,所有的 __bitwise/__force 都会消失,最后在 gcc
看来它们只不过是普通的整数。
坦白来说,你并不需要使用枚举类型。上面那些实际都可以浓缩成一个特殊的"int
__bitwise"类型。
所以更简单的办法只要这样做::
typedef int __bitwise pm_request_t;
#define PM_SUSPEND ((__force pm_request_t) 1)
#define PM_RESUME ((__force pm_request_t) 2)
现在你就有了严格的类型检查所需要的所有基础架构。
一个小提醒:常数整数"0"是特殊的。你可以直接把常数零当作位方式整数使用而
不用担心 sparse 会抱怨。这是因为"bitwise"(恰如其名)是用来确保不同位方
式类型不会被弄混(小尾模式,大尾模式,cpu尾模式,或者其他),对他们来说
常数"0"确实 **是** 特殊的。
使用sparse进行锁检查
--------------------
下面的宏对于 gcc 来说是未定义的,在 sparse 运行时定义,以使用sparse的“上下文”
跟踪功能,应用于锁定。 这些注释告诉 sparse 什么时候有锁,以及注释的函数的进入和
退出。
__must_hold - 指定的锁在函数进入和退出时被持有。
__acquires - 指定的锁在函数退出时被持有,但在进入时不被持有。
__releases - 指定的锁在函数进入时被持有,但在退出时不被持有。
如果函数在不持有锁的情况下进入和退出,在函数内部以平衡的方式获取和释放锁,则不
需要注释。
上面的三个注释是针对sparse否则会报告上下文不平衡的情况。
获取 sparse 工具
~~~~~~~~~~~~~~~~
你可以从 Sparse 的主页获取最新的发布版本:
https://www.kernel.org/pub/software/devel/sparse/dist/
或者,你也可以使用 git 克隆最新的 sparse 开发版本:
git://git.kernel.org/pub/scm/devel/sparse/sparse.git
一旦你下载了源码,只要以普通用户身份运行:
make
make install
如果是标准的用户,它将会被自动安装到你的~/bin目录下。
使用 sparse 工具
~~~~~~~~~~~~~~~~
用"make C=1"命令来编译内核,会对所有重新编译的 C 文件使用 sparse 工具。
或者使用"make C=2"命令,无论文件是否被重新编译都会对其使用 sparse 工具。
如果你已经编译了内核,用后一种方式可以很快地检查整个源码树。
make 的可选变量 CHECKFLAGS 可以用来向 sparse 工具传递参数。编译系统会自
动向 sparse 工具传递 -Wbitwise 参数。
注意sparse定义了__CHECKER__预处理器符号。
@@ -0,0 +1,163 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/dev-tools/testing-overview.rst
:Translator: 胡皓文 Hu Haowen <2023002089@link.tyut.edu.cn>
============
内核测试指南
============
有许多不同的工具可以用于测试Linux内核,因此了解什么时候使用它们可能
很困难。本文档粗略概述了它们之间的区别,并阐释了它们是怎样糅合在一起
的。
编写和运行测试
==============
大多数内核测试都是用kselftest或KUnit框架之一编写的。它们都让运行测试
更加简化,并为编写新测试提供帮助。
如果你想验证内核的行为——尤其是内核的特定部分——那你就要使用kUnit或
kselftest。
KUnit和kselftest的区别
----------------------
.. note::
由于本文段中部分术语尚无较好的对应中文释义,可能导致与原文含义
存在些许差异,因此建议读者结合原文
(Documentation/dev-tools/testing-overview.rst)辅助阅读。
如对部分翻译有异议或有更好的翻译意见,欢迎联系译者进行修订。
KUnit(Documentation/dev-tools/kunit/index.rst)是用于“白箱”测
试的一个完整的内核内部系统:因为测试代码是内核的一部分,所以它能够访
问用户空间不能访问到的内部结构和功能。
因此,KUnit测试最好针对内核中较小的、自包含的部分,以便能够独立地测
试。“单元”测试的概念亦是如此。
比如,一个KUnit测试可能测试一个单独的内核功能(甚至通过一个函数测试
一个单一的代码路径,例如一个错误处理案例),而不是整个地测试一个特性。
这也使得KUnit测试构建和运行非常地快,从而能够作为开发流程的一部分被
频繁地运行。
有关更详细的介绍,请参阅KUnit测试代码风格指南
Documentation/dev-tools/kunit/style.rst
kselftest(Documentation/dev-tools/kselftest.rst),相对来说,大量用
于用户空间,并且通常测试用户空间的脚本或程序。
这使得编写复杂的测试,或者需要操作更多全局系统状态的测试更加容易(诸
如生成进程之类)。然而,从kselftest直接调用内核函数是不行的。这也就
意味着只有通过某种方式(如系统调用、驱动设备、文件系统等)导出到了用
户空间的内核功能才能使用kselftest来测试。为此,有些测试包含了一个伴
生的内核模块用于导出更多的信息和功能。不过,对于基本上或者完全在内核
中运行的测试,KUnit可能是更佳工具。
kselftest也因此非常适合于全部功能的测试,因为这些功能会将接口暴露到
用户空间,从而能够被测试,而不是展现实现细节。“system”测试和
“end-to-end”测试亦是如此。
比如,一个新的系统调用应该伴随有新的kselftest测试。
代码覆盖率工具
==============
支持两种不同代码之间的覆盖率测量工具。它们可以用来验证一项测试执行的
确切函数或代码行。这有助于决定内核被测试了多少,或用来查找合适的测试
中没有覆盖到的极端情况。
Documentation/translations/zh_CN/dev-tools/gcov.rst 是GCC的覆盖率测试
工具,能用于获取内核的全局或每个模块的覆盖率。与KCOV不同的是,这个工具
不记录每个任务的覆盖率。覆盖率数据可以通过debugfs读取,并通过常规的
gcov工具进行解释。
Documentation/dev-tools/kcov.rst 是能够构建在内核之中,用于在每个任务
的层面捕捉覆盖率的一个功能。因此,它对于模糊测试和关于代码执行期间信
息的其它情况非常有用,比如在一个单一系统调用里使用它就很有用。
动态分析工具
============
内核也支持许多动态分析工具,用以检测正在运行的内核中出现的多种类型的
问题。这些工具通常每个去寻找一类不同的缺陷,比如非法内存访问,数据竞
争等并发问题,或整型溢出等其他未定义行为。
如下所示:
* kmemleak检测可能的内存泄漏。参阅
Documentation/dev-tools/kmemleak.rst
* KASAN检测非法内存访问,如数组越界和释放后重用(UAF)。参阅
Documentation/dev-tools/kasan.rst
* UBSAN检测C标准中未定义的行为,如整型溢出。参阅
Documentation/dev-tools/ubsan.rst
* KCSAN检测数据竞争。参阅 Documentation/dev-tools/kcsan.rst
* KFENCE是一个低开销的内存问题检测器,比KASAN更快且能被用于批量构建。
参阅 Documentation/dev-tools/kfence.rst
* lockdep是一个锁定正确性检测器。参阅
Documentation/locking/lockdep-design.rst
* 运行时确认(Runtime Verification)支持检查给定子系统的特定行为。参阅
Documentation/trace/rv/runtime-verification.rst。
* 除此以外,在内核中还有一些其它的调试工具,大多数能在
lib/Kconfig.debug 中找到。
这些工具倾向于对内核进行整体测试,并且不像kselftest和KUnit一样“传递”。
它们可以通过在启用这些工具时运行内核测试以与kselftest或KUnit结合起来:
之后你就能确保这些错误在测试过程中都不会发生了。
一些工具与KUnit和kselftest集成,并且在检测到问题时会自动打断测试。
静态分析工具
============
除了测试运行中的内核,我们还可以使用**静态分析**工具直接分析内核的源代
码(**在编译时**)。内核中常用的工具允许人们检查整个源代码树或其中的特
定文件。它们使得在开发过程中更容易发现和修复问题。
Sparse可以通过执行类型检查、锁检查、值范围检查来帮助测试内核,此外还
可以在检查代码时报告各种错误和警告。关于如何使用它的细节,请参阅
Documentation/translations/zh_CN/dev-tools/sparse.rst。
Smatch扩展了Sparse,并提供了对编程逻辑错误的额外检查,如开关语句中
缺少断点,错误检查中未使用的返回值,忘记在错误路径的返回中设置错误代
码等。Smatch也有针对更严重问题的测试,如整数溢出、空指针解除引用和内
存泄漏。见项目页面http://smatch.sourceforge.net/。
Coccinelle是我们可以使用的另一个静态分析器。Coccinelle经常被用来
帮助源代码的重构和并行演化,但它也可以帮助避免常见代码模式中出现的某
些错误。可用的测试类型包括API测试、内核迭代器的正确使用测试、自由操
作的合理性检查、锁定行为的分析,以及已知的有助于保持内核使用一致性的
进一步测试。详情请见Documentation/dev-tools/coccinelle.rst。
不过要注意的是,静态分析工具存在**假阳性**的问题。在试图修复错误和警
告之前,需要仔细评估它们。
何时使用Sparse和Smatch
----------------------
Sparse做类型检查,例如验证注释的变量不会导致无符号的错误,检测
``__user`` 指针使用不当的地方,以及分析符号初始化器的兼容性。
Smatch进行流程分析,如果允许建立函数数据库,它还会进行跨函数分析。
Smatch试图回答一些问题,比如这个缓冲区是在哪里分配的?它有多大?这
个索引可以由用户控制吗?这个变量比那个变量大吗?
一般来说,在Smatch中写检查比在Sparse中写检查要容易。尽管如此,
Sparse和Smatch的检查还是有一些重叠的地方。
Smatch和Coccinelle的强项
------------------------
Coccinelle可能是最容易写检查的。它在预处理器之前工作,所以用Coccinelle
检查宏中的错误更容易。Coccinelle还能为你创建补丁,这是其他工具无法做到的。
例如,用Coccinelle你可以从 ``kmalloc(x * size, GFP_KERNEL)``
到 ``kmalloc_array(x, size, GFP_KERNEL)`` 进行大规模转换,这真的很
有用。如果你只是创建一个Smatch警告,并试图把转换的工作推给维护者,他们会很
恼火。你将不得不为每个警告争论是否真的可以溢出。
Coccinelle不对变量值进行分析,而这正是Smatch的强项。另一方面,Coccinelle
允许你用简单的方法做简单的事情。
@@ -0,0 +1,88 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/dev-tools/ubsan.rst
:翻译:
慕冬亮 Dongliang Mu <dzm91@hust.edu.cn>
:校译:
王昱力 WangYuli <wangyuli@uniontech.com>
未定义行为消毒剂 - UBSAN
====================================
UBSAN是一种动态未定义行为检查工具。
UBSAN使用编译时插桩捕捉未定义行为。编译器在可能导致未定义行为的操作前插入特定
检测代码。如果检查失败,即检测到未定义行为,__ubsan_handle_* 函数将被调用打印
错误信息。
GCC自4.9.x [1_] (详见 ``-fsanitize=undefined`` 选项及其子选项)版本后引入这
一特性。GCC 5.x 版本实现了更多检查器 [2_]。
报告样例
--------------
::
================================================================================
UBSAN: Undefined behaviour in ../include/linux/bitops.h:110:33
shift exponent 32 is to large for 32-bit type 'unsigned int'
CPU: 0 PID: 0 Comm: swapper Not tainted 4.4.0-rc1+ #26
0000000000000000 ffffffff82403cc8 ffffffff815e6cd6 0000000000000001
ffffffff82403cf8 ffffffff82403ce0 ffffffff8163a5ed 0000000000000020
ffffffff82403d78 ffffffff8163ac2b ffffffff815f0001 0000000000000002
Call Trace:
[<ffffffff815e6cd6>] dump_stack+0x45/0x5f
[<ffffffff8163a5ed>] ubsan_epilogue+0xd/0x40
[<ffffffff8163ac2b>] __ubsan_handle_shift_out_of_bounds+0xeb/0x130
[<ffffffff815f0001>] ? radix_tree_gang_lookup_slot+0x51/0x150
[<ffffffff8173c586>] _mix_pool_bytes+0x1e6/0x480
[<ffffffff83105653>] ? dmi_walk_early+0x48/0x5c
[<ffffffff8173c881>] add_device_randomness+0x61/0x130
[<ffffffff83105b35>] ? dmi_save_one_device+0xaa/0xaa
[<ffffffff83105653>] dmi_walk_early+0x48/0x5c
[<ffffffff831066ae>] dmi_scan_machine+0x278/0x4b4
[<ffffffff8111d58a>] ? vprintk_default+0x1a/0x20
[<ffffffff830ad120>] ? early_idt_handler_array+0x120/0x120
[<ffffffff830b2240>] setup_arch+0x405/0xc2c
[<ffffffff830ad120>] ? early_idt_handler_array+0x120/0x120
[<ffffffff830ae053>] start_kernel+0x83/0x49a
[<ffffffff830ad120>] ? early_idt_handler_array+0x120/0x120
[<ffffffff830ad386>] x86_64_start_reservations+0x2a/0x2c
[<ffffffff830ad4f3>] x86_64_start_kernel+0x16b/0x17a
================================================================================
用法
-----
使用如下内核配置启用UBSAN::
CONFIG_UBSAN=y
排除要被检测的文件::
UBSAN_SANITIZE_main.o := n
排除一个目录中的所有文件::
UBSAN_SANITIZE := n
当全部文件都被禁用,可通过如下方式为特定文件启用::
UBSAN_SANITIZE_main.o := y
未对齐的内存访问检测可通过开启独立选项 - CONFIG_UBSAN_ALIGNMENT 检测。
该选项在支持未对齐访问的架构上(CONFIG_HAVE_EFFICIENT_UNALIGNED_ACCESS=y)
默认为关闭。该选项仍可通过内核配置启用,但它将产生大量的UBSAN报告。
参考文献
----------
.. _1: https://gcc.gnu.org/onlinedocs/gcc-4.9.0/gcc/Debugging-Options.html
.. _2: https://gcc.gnu.org/onlinedocs/gcc/Debugging-Options.html
.. _3: https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html