Linux 调试之动态打印

时间:2024-12-18 13:07:59

目录

  • 一、概述
  • 二、printk
    • 1、printk 消息级别
    • 2、调整内核 printk 打印级别
  • 三、dynamic debug 的使用
    • 1、dev_xxx 函数
    • 2、动态输出支持的特性
    • 3、命令行格式
    • 4、动态打印


一、概述

在 kernel 驱动代码中,使用动态输出是系统内核调试的重要手段之一,printk 打印是全局的,只能设置输出等级,而且使用 printk 每次都要重新编译内核,很不方便。。而动态输出可以在不需要重新编译内核的情况下,方便的打印出内核的 debug 信息。动态输出可以动态选择打开某个内核子系统的输出,可以有选择性地打开某些模块的输出,printkdev_infodev_dbgdev_err 之类的函数代替,dev_xxx 函数的本质还是使用 printk 打印的,只是对 printk 进行了一层包装。

在系统运行时候,动态打印可以由系统维护者动态打开内核子系统的打印,可以有选择性地打开某些模块的打印。要使用动态打印,必须在内核配置时打开 CONFIG_DYNAMIC_DEBUG 宏。

CONFIG_DEBUG_FS=y
CONFIG_DYNAMIC_DEBUG=y

CONFIG_DYNAMIC_DEBUG 是配置动态输出,它依赖于 CONFIG_DEBUG_FS,而 CONFIG_DEBUG_FSdebugfs 文件系统。debugfs默认会挂载到 /sys/kernel/debug,如果没有挂载,可以执行以下命令挂载:

$ mount -t debugfs none /sys/kernel/debug

二、printk

1、printk 消息级别

Linux 内核共提供了八种不同的消息级别,分为级别 0~7。数值越大,表示级别越低,对应的消息越不重要。相应的宏定义在 include/linux/kern_levels.h 文件中。

#define KERN_SOH    "\001"      /* ASCII Start Of Header */
#define KERN_SOH_ASCII  '\001'

#define KERN_EMERG  KERN_SOH "0"    /* system is unusable */
#define KERN_ALERT  KERN_SOH "1"    /* action must be taken immediately */
#define KERN_CRIT   KERN_SOH "2"    /* critical conditions */
#define KERN_ERR    KERN_SOH "3"    /* error conditions */
#define KERN_WARNING    KERN_SOH "4"    /* warning conditions */
#define KERN_NOTICE KERN_SOH "5"    /* normal but significant condition */
#define KERN_INFO   KERN_SOH "6"    /* informational */
#define KERN_DEBUG  KERN_SOH "7"    /* debug-level messages */
  • KERN_EMERG 表示紧急事件,一般是系统崩溃之前提示的消息;
  • KERN_ALERT 表示必须立即采取行动的消息;
  • KERN_CRIT 表示临界状态,通常涉及严重的硬件或软件操作失败;
  • KERN_ERR 用于报告错误状态,设备驱动程序会经常使用该级别来报告来自硬件的问题;
  • KERN_WARNING 对可能出现问题的情况进行警告,这类情况通常不会对系统造成严重的问题;
  • KERN_NOTICE 表示有必要进行提示的正常情形,许多与安全相关的状况用这个级别进行汇报;
  • KERN_INFO 表示内核提示信息,很多驱动程序在启动的时候,用这个级别打印出它们找到的硬件信息;
  • KERN_DEBUG 用于调试信息。

2、调整内核 printk 打印级别

通过 /proc/sys/kernel/printk 文件可以调节 printk 的输出等级,该文件有 4 个数字值:

$ cat /proc/sys/kernel/printk
4	4	1	7

四个数值含义分别如下:

  • 控制台日志级别:优先级高于该值的消息将被打印至控制台;
  • 默认的消息日志级别:将用该优先级来打印没有优先级的消息(即 printk 没有指定消息级别);
  • 最低的控制台日志级别:控制台日志级别可被设置的最小值(最高优先级);
  • 默认的控制台日志级别:控制台日志级别的缺省值。

通过修改 /proc/sys/kernel/printk 中的值来改变内核打印效果。例如,屏蔽掉所有的内核 printk 打印,只需要把第一个数值调到最小值 1 或者 0,指令如下:

$ echo 1 4 1 7 > /proc/sys/kernel/printk

三、dynamic debug 的使用

1、dev_xxx 函数

下面简述下几个 dev_xxx 函数的基本使用规则,以及动态调试使用方式。

  • dev_info(): 启动过程、或者模块加载过程等 “通知类的” 信息等,一般只会通知一次,例如 probe 函数;
  • dev_dbg(): 一般使用在普通错误,如 -EINVAL、-ENOMEM 等 errno 发生处,用于调试;
  • dev_err(): 一般使用在严重错误,尤其是用户无法得到 errno 的地方,或者程序员不容易猜测系统哪里出了问题的地方。

dev_debug 的定义在文件 include/linux/

#if defined(CONFIG_DYNAMIC_DEBUG)
#define dev_dbg(dev, fmt, ...)	\
 dynamic_dev_dbg(dev, dev_fmt(fmt), ##__VA_ARGS__)
#elif defined(DEBUG)
#define dev_dbg(dev, fmt, ...)	\
 dev_printk(KERN_DEBUG, dev, dev_fmt(fmt), ##__VA_ARGS__)
#else
#define dev_dbg(dev, fmt, ...)	\
({	\
 if (0)	\
  dev_printk(KERN_DEBUG, dev, dev_fmt(fmt), ##__VA_ARGS__); \
})
#endif

pr_debug 的定义在文件 include/linux/,从 pr_debug 的源码注释建议:如果写驱动,请用 dev_dbg

/* If you are writing a driver, please use dev_dbg instead */
#if defined(CONFIG_DYNAMIC_DEBUG)
#include <linux/dynamic_debug.h>

/* dynamic_pr_debug() uses pr_fmt() internally so we don't need it here */
#define pr_debug(fmt, ...) \
 dynamic_pr_debug(fmt, ##__VA_ARGS__)
#elif defined(DEBUG)
#define pr_debug(fmt, ...) \
 printk(KERN_DEBUG pr_fmt(fmt), ##__VA_ARGS__)
#else
#define pr_debug(fmt, ...) \
 no_printk(KERN_DEBUG pr_fmt(fmt), ##__VA_ARGS__)
#endif
配置 pr_debug/dev_dbg输出情况
CONFIG_DYNAMIC_DEBUG=y
DEBUG=n
调用 dynamic_pr_debug/dynamic_dev_dbgecho -n “file +p” > /sys/kernel/debug/dynamic_debug/control
CONFIG_DYNAMIC_DEBUG=y
DEBUG=y
调用 dynamic_pr_debug/dynamic_dev_dbg,增加启动参数 loglevel=8 之后,kernel 启动阶段就能看到 log
CONFIG_DYNAMIC_DEBUG=n
DEBUG=y
调用 printk,打印等级是 KERN_DEBUG=7,所以要将打印等级设置为 8(echo 8 > /proc/sys/kernel/printk)才能看到输出
CONFIG_DYNAMIC_DEBUG=n
DEBUG=n
不打印

2、动态输出支持的特性

动态输出在 debugfs 文件系统中对应的是 control 文件节点。control 文件节点记录了系统中所有使用动态输出技术的文件名路径,输出语句所在的行号、模块名和将要输出的语句等。

你可以通过以下命令查看目前所有调试状态的行为配置:

$ cat /sys/kernel/debug/dynamic_debug/control

你也可以应用标准的 Unix 文本过滤命令来过滤这些数据, 例如:

$ grep -i rdma /sys/kernel/debug/dynamic_debug/control  | wc -l

3、命令行格式

在语法层面上,一个命令由一系列的规格匹配组成,最后由一个标记来改变这规格。

command ::= match-spec* flags-spec

match-spec 常用来选择一个已知的 dprintk() 调用点的子集来套用 flags-spec。把他们当做彼此之间的每对做隐式查询。注意,一个空的 match_specs 列表是有可能的,但不是非常有用,因为它不会匹配任何调用点的调试子句。

一个匹配规范由一个关键字组成,关键字控制被比较的调用点的属性和要比较的值。可能关键字是:

match-spec ::= 'func' string    |
	           'file' string    |
	           'module' string  |
	           'format' string  |
	           'line' line-range
	           

line-range ::= lineno |
       		'-'lineno |
       	    lineno'-' |
  	        lineno'-'lineno

注意:line-range 不能包含空格,例如,“1-30”是有效的范围,但“1 - 30”就是无效的

每个关键字的含义如下:

  • func:给定的字符串会和每个调用点的函数名比较。例如: func svc_tcp_accept
  • file:给定的字符串会和每个调用点的源文件的全路径名或者相对名比较。例如: file file /usr/src/packages/BUILD/sgi-enhancednfs-1.4/default/net/sunrpc/
  • module:给定的字符串会和每个调用点的模块名进行比较。模块名是和在 ls mod 里看到的字符串一样。例如,module sunrpc
  • format:给定的字符串会在动态调试格式字符串里查找。注意这字符串不需要匹配这个格式。空格和其他特殊字符能够用八进制字符语法来转义,例如空字符是 \040。作为选择,这个字符串可以附上双引号 " 或者是单引号 。例如:
format svcrdma:         // NFS/RDMA 服务器的dprintks
format readahead        // 一些在预加载缓存里的dprintks
format nfsd:\040SETATTR // 一个使用空格来匹配格式的方式
format "nfsd: SETATTR"  // 一个整齐的方法来用空格匹配格式
format 'nfsd: SETATTR'  // 同样是一个用空格来匹配格式的方法和
  • line:给定的行号或者是行号范围会和每个 dprintk() 调用点的行号进行比较。例如:
line 1603      // 准确定位到1603行 
line 1600-1605 // 1600行到1605行之间的6行
line -1605     // 从第一行到1605行之间的1605行
line 1600-     // 从1600行到结尾的全部行

标记规范包含了一个由一个或多个标记字符跟随的变化操作。这变化操作如下所示:

- // 移除给定的标记
+ // 加入给定的标记
= // 设置标记到给定的标记上 
f // 包含已打印消息的函数名
l // 包含已在打印消息的行号
m // 包含已打印消息的模块名
p // 产生一个 printk() 消息到显示系统启动日志
t // 包含了不在中断上下文中产生的消息里的线程ID

4、动态打印

例:

# 打开一个文件中所有动态打印语句
$ echo -n "file  +p" > /sys/kernel/debug/dynamic_debug/control

# 打开一个模块所有动态打印语句
$ echo "moudle dwc3 +p" > /sys/kernel/debug/dynamic_debug/control

# 打开一个函数中所有的动态打印语句
$ echo "func svc_process +p" > /sys/kernel/debug/dynamic_debug/control

# 打开文件路径中包含 usb 的文件里所有的动态打印语句
echo -n "*usb* +p" > /sys/kernel/debug/dynamic_debug/control

上面是打开动态打印语句的例子,除了能打印 pr_debug() / dev_dbg() 函数中定义的输出外,还能打印一些额外信息,例如函数名、行号、模块名字和线程 ID 等。

参数:

  • p:打开动态打印语句。
  • f:打印函数名
  • l:打印行号
  • m:打印模块名字
  • t:打印线程 ID

另外,还可以在各个子系统的 Makefile 中添加 ccflags 来打开动态输出语句:

Makefile:
ccflags-y += -DDEBUG
ccflags-y += -DVERBOSE_DEBUG