驱动程序开发调试建议¶
本文档用作调试设备驱动程序的通用起点和查找点。虽然本指南侧重于需要重新编译模块/内核的调试,但 用户空间调试指南 将指导您使用动态调试、ftrace 和其他工具,这些工具可用于调试问题和行为。 有关一般调试建议,请参阅 一般建议文档。
以下部分向您展示可用的工具。
printk() 和朋友¶
这些是 printf() 的派生形式,具有不同的目标,并且支持或不支持动态开启或关闭。
简单 printk()¶
经典方式,可以非常有效地用于新模块的快速而粗糙的开发,或者提取任意必要的数据以进行故障排除。
先决条件:CONFIG_PRINTK
(通常默认启用)
优点:
无需学习任何东西,简单易用
易于精确地修改以满足您的需求(数据格式 (参见:如何正确使用 printk 格式说明符),日志中的可见性)
可能会导致代码执行延迟(有助于确认时序是否是一个因素)
缺点:
需要重建内核/模块
可能会导致代码执行延迟(这可能导致问题无法重现)
有关完整文档,请参阅 使用 printk 记录消息
Trace_printk¶
先决条件:CONFIG_DYNAMIC_FTRACE
& #include <linux/ftrace.h>
使用起来比 printk()
稍微不方便一点,因为您必须从跟踪文件中读取消息(参见:读取 ftrace 日志 而不是从内核日志中读取,但当 printk()
向代码执行中添加不必要的延迟时非常有用,这会导致问题不稳定或隐藏。)
如果此过程仍然导致时序问题,那么您可以尝试 trace_puts()
。
有关完整文档,请参见 trace_printk()
dev_dbg¶
打印语句,可以通过 动态调试 定位,其中包含有关上下文中使用的设备的附加信息。
什么时候适合在代码中保留调试打印?
永久调试语句对于开发人员解决驱动程序行为不端的问题必须有用。 判断这一点有点像艺术而不是科学,但是 编码风格指南 中有一些指导原则。 在几乎所有情况下,调试语句都不应该被提交到上游,因为一个工作的驱动程序应该是静默的。
自定义 printk¶
示例
#define core_dbg(fmt, arg...) do { \
if (core_debug) \
printk(KERN_DEBUG pr_fmt("core: " fmt), ## arg); \
} while (0)
您应该在什么时候这样做?
最好只使用 pr_debug()
,以后可以使用动态调试来打开/关闭它。 此外,许多驱动程序通过模块参数设置的变量(如 core_debug
)来激活这些打印。 但是,模块参数 不再推荐。
Ftrace¶
创建自定义 Ftrace 追踪点¶
追踪点会在您的代码中添加一个钩子,当追踪点被启用时,将会调用并记录该钩子。 例如,这可以用于追踪命中条件分支,或者在调试会话期间转储代码流程中特定点的内部状态。
这是 如何实现新追踪点 的基本描述。
有关完整的事件追踪文档,请参见 事件追踪
有关完整的 Ftrace 文档,请参见 ftrace - 函数追踪器
DebugFS¶
先决条件:CONFIG_DEBUG_FS` & `#include <linux/debugfs.h>
DebugFS 与其他调试方法不同,因为它不会将消息写入内核日志,也不会向代码添加追踪。 相反,它允许开发人员处理一组文件。 使用这些文件,您可以存储变量的值或进行寄存器/内存转储,也可以使这些文件可写并修改驱动程序中的值/设置。
可能的用例包括
存储寄存器值
跟踪变量
存储错误
存储设置
切换设置,如调试开启/关闭
错误注入
当数据转储的大小难以作为通用内核日志的一部分消化(例如,当转储原始比特流数据时),或者当您不一直对所有值感兴趣,而是可以检查它们时,这尤其有用。
总体思路是
在 probe 期间创建目录 (
struct dentry *parent = debugfs_create_dir("my_driver", NULL);
)创建文件 (
debugfs_create_u32("my_value", 444, parent, &my_variable);
)在此示例中,该文件位于
/sys/kernel/debug/my_driver/my_value
中(具有用户/组/所有人的读取权限)对文件的任何读取都将返回变量
my_variable
的当前内容
移除设备时清理目录 (
debugfs_remove(parent);
)
有关完整文档,请参见 DebugFS。
KASAN、UBSAN、lockdep 和其他错误检查器¶
KASAN (内核地址清理器)¶
先决条件:CONFIG_KASAN
KASAN 是一种动态内存错误检测器,可帮助查找 use-after-free 和 out-of-bounds 错误。 它使用编译时检测来检查每次内存访问。
有关完整文档,请参见 内核地址清理器 (KASAN)。
UBSAN (未定义行为清理器)¶
先决条件:CONFIG_UBSAN
UBSAN 依赖于编译器检测和运行时检查来检测未定义的行为。 它旨在查找各种问题,包括有符号整数溢出、数组索引越界等等。
有关完整文档,请参见 未定义行为清理器 - UBSAN
lockdep (锁依赖验证器)¶
先决条件:CONFIG_DEBUG_LOCKDEP
lockdep 是一种运行时锁依赖验证器,可检测内核中潜在的死锁和其他与锁定相关的问题。 它会跟踪锁的获取和释放,构建依赖关系图,并分析该图以查找潜在的死锁。 lockdep 对于验证内核中锁顺序的正确性尤其有用。
PSI (压力暂缓信息跟踪)¶
先决条件:CONFIG_PSI
PSI 是一种测量工具,用于识别硬件资源上的过度过提交,这可能会导致性能中断甚至 OOM 终止。
设备核心转储¶
先决条件:CONFIG_DEV_COREDUMP
& #include <linux/devcoredump.h>
为驱动程序提供基础设施,以便向用户空间提供任意数据。 它最常与 udev 或类似的用户空间应用程序结合使用,以监听内核 uevent,这些 uevent 指示转储已准备就绪。 Udev 具有一些规则,可以将该文件复制到某个位置以进行长期存储和分析,因为默认情况下,转储的数据会在默认的 5 分钟后自动清理。 该数据使用特定于驱动程序的工具或 GDB 进行分析。
可以使用 vmalloc 区域创建设备核心转储,带有读取/释放方法,或者作为分散/聚集列表。
您可以在以下位置找到示例实现:drivers/media/platform/qcom/venus/core.c,在 Bluetooth HCI 层中,在多个无线驱动程序中,以及在多个 DRM 驱动程序中。
devcoredump 接口¶
-
void dev_coredumpm(struct device *dev, struct module *owner, void *data, size_t datalen, gfp_t gfp, ssize_t (*read)(char *buffer, loff_t offset, size_t count, void *data, size_t datalen), void (*free)(void *data))¶
使用读取/释放方法创建设备核心转储
参数
struct device *dev
崩溃设备的
struct device
struct module *owner
包含读取/释放函数的模块,使用
THIS_MODULE
void *data
read/free 函数的数据 cookie
size_t datalen
数据长度
gfp_t gfp
分配标志
ssize_t (*read)(char *buffer, loff_t offset, size_t count, void *data, size_t datalen)
从给定缓冲区读取的函数
void (*free)(void *data)
释放给定缓冲区的函数
说明
为给定设备创建新的设备核心转储。 如果之前的核心转储尚未被读取,则新的核心转储将被丢弃。 数据生命周期由设备核心转储框架确定,当不再需要数据时,将调用 free 函数来释放数据。
-
void dev_coredumpv(struct device *dev, void *data, size_t datalen, gfp_t gfp)¶
使用 vmalloc 数据创建设备核心转储
参数
struct device *dev
崩溃设备的
struct device
void *data
包含设备核心转储的 vmalloc 数据
size_t datalen
数据长度
gfp_t gfp
分配标志
说明
此函数获取 vmalloc’ed 数据的所有权,并在不再使用时释放它。 有关更多信息,请参见 dev_coredumpm()
。
-
void devcd_free_sgtable(void *data)¶
释放给定 scatterlist 表的所有内存(即页面和 scatterlist 实例)
参数
void *data
指向要释放的 sg_table 的指针
注意
如果使用 devcd_alloc_sgtable 分配了两个表,然后使用 sg_chain 函数将它们链接起来,则应该只在链接表上调用该函数一次
-
ssize_t devcd_read_from_sgtable(char *buffer, loff_t offset, size_t buf_len, void *data, size_t data_len)¶
将数据从 sg_table 复制到给定缓冲区并返回读取的字节数
参数
char *buffer
要将数据复制到的缓冲区
loff_t offset
从给定 scatterlist 中数据的头部开始的第 offset**** 字节开始复制
size_t buf_len
缓冲区长度
void *data
要从中复制的 scatterlist 表
size_t data_len
sg_table 中数据的长度
返回
复制的字节数
参数
struct device *dev
崩溃设备的
struct device
说明
dev_coredump_put()
从文件系统中移除给定设备的核心转储(如果存在),并释放其关联数据;否则,不执行任何操作。
它对于不想在卸载后保持核心转储可用的模块很有用。
-
void dev_coredumpm_timeout(struct device *dev, struct module *owner, void *data, size_t datalen, gfp_t gfp, ssize_t (*read)(char *buffer, loff_t offset, size_t count, void *data, size_t datalen), void (*free)(void *data), unsigned long timeout)¶
使用具有自定义超时的读取/释放方法创建设备核心转储。
参数
struct device *dev
崩溃设备的
struct device
struct module *owner
包含读取/释放函数的模块,使用
THIS_MODULE
void *data
read/free 函数的数据 cookie
size_t datalen
数据长度
gfp_t gfp
分配标志
ssize_t (*read)(char *buffer, loff_t offset, size_t count, void *data, size_t datalen)
从给定缓冲区读取的函数
void (*free)(void *data)
释放给定缓冲区的函数
unsigned long timeout
移除核心转储的时间(以 jiffies 为单位)
说明
为给定设备创建新的设备核心转储。 如果之前的核心转储尚未被读取,则新的核心转储将被丢弃。 数据生命周期由设备核心转储框架确定,当不再需要数据时,将调用 free 函数来释放数据。
-
void dev_coredumpsg(struct device *dev, struct scatterlist *table, size_t datalen, gfp_t gfp)¶
创建将 scatterlist 用作数据参数的设备核心转储
参数
struct device *dev
崩溃设备的
struct device
struct scatterlist *table
转储数据
size_t datalen
数据长度
gfp_t gfp
分配标志
说明
为给定设备创建新的设备核心转储。 如果之前的核心转储尚未被读取,则新的核心转储将被丢弃。 数据生命周期由设备核心转储框架确定,当不再需要数据时,它将释放数据。
版权所有 ©2024:Collabora