驱动程序开发调试建议

本文档用作调试设备驱动程序的通用起点和查找点。虽然本指南侧重于需要重新编译模块/内核的调试,但 用户空间调试指南 将指导您使用动态调试、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 中数据的长度

返回

复制的字节数

void dev_coredump_put(struct device *dev)

移除设备核心转储

参数

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