动态调试

简介

动态调试允许你动态地启用/禁用内核调试打印(debug-print)代码,以获取额外的内核信息。

如果 /proc/dynamic_debug/control 存在,说明你的内核支持动态调试。你需要 root 权限(sudo su)来使用它。

动态调试提供

  • 内核中所有 prdbg 的目录。可以通过运行 cat /proc/dynamic_debug/control 来查看它们。

  • 一种简单的查询/命令语言,通过选择以下 0 或 1 种条件的任意组合来修改 prdbg

    • 源文件名

    • 函数名

    • 行号(包括行号范围)

    • 模块名

    • 格式化字符串

    • 类名(由各个模块已知/声明)

注意:要真正在控制台上获取调试打印输出,你可能需要调整内核的 loglevel=,或者使用 ignore_loglevel。请在 内核的命令行参数 中了解这些内核参数。

查看动态调试行为

你可以在 prdbg 目录中查看当前配置的行为

:#> head -n7 /proc/dynamic_debug/control
# filename:lineno [module]function flags format
init/main.c:1179 [main]initcall_blacklist =_ "blacklisting initcall %s\n"
init/main.c:1218 [main]initcall_blacklisted =_ "initcall %s blacklisted\n"
init/main.c:1424 [main]run_init_process =_ "  with arguments:\n"
init/main.c:1426 [main]run_init_process =_ "    %s\n"
init/main.c:1427 [main]run_init_process =_ "  with environment:\n"
init/main.c:1429 [main]run_init_process =_ "    %s\n"

以空格分隔的第 3 列显示当前的标志,前面带有 =,以便与 grep/cut 配合使用。=p 表示已启用的调用点(callsite)。

控制动态调试行为

prdbg 点的行为通过向控制文件写入查询/命令来控制。例如

# grease the interface
:#> alias ddcmd='echo $* > /proc/dynamic_debug/control'

:#> ddcmd '-p; module main func run* +p'
:#> grep =p /proc/dynamic_debug/control
init/main.c:1424 [main]run_init_process =p "  with arguments:\n"
init/main.c:1426 [main]run_init_process =p "    %s\n"
init/main.c:1427 [main]run_init_process =p "  with environment:\n"
init/main.c:1429 [main]run_init_process =p "    %s\n"

错误消息将发送到控制台/syslog

:#> ddcmd mode foo +p
dyndbg: unknown keyword "mode"
dyndbg: query parse failed
bash: echo: write error: Invalid argument

如果同时启用了并挂载了 debugfs,dynamic_debug/control 也会在挂载目录下,通常是 /sys/kernel/debug/

命令语言参考

在基础词法层面,命令是由空格或制表符分隔的单词序列。因此,以下命令是完全等效的

:#> ddcmd file svcsock.c line 1603 +p
:#> ddcmd "file svcsock.c line 1603 +p"
:#> ddcmd '  file   svcsock.c     line  1603 +p  '

命令提交由 write() 系统调用界定。多个命令可以写在一起,用 ;\n 分隔

:#> ddcmd "func pnpacpi_get_resources +p; func pnp_assign_mem +p"
:#> ddcmd <<"EOC"
func pnpacpi_get_resources +p
func pnp_assign_mem +p
EOC
:#> cat query-batch-file > /proc/dynamic_debug/control

你还可以在每个查询项中使用通配符。匹配规则支持 *(匹配零个或多个字符)和 ?(恰好匹配一个字符)。例如,你可以匹配所有的 usb 驱动程序

:#> ddcmd file "drivers/usb/*" +p     # "" to suppress shell expansion

从语法上讲,一个命令是“关键字-值”对,后面跟着标志更改或设置

command ::= match-spec* flags-spec

匹配规范(match-spec)从目录中选择要应用标志规范(flags-spec)的 prdbg,所有约束条件都按逻辑与(AND)组合。缺省的关键字等同于关键字 “*”。

请注意,由于匹配规范可以为空,系统会先检查标志,然后检查关键字和值的组合。标志错误会掩盖关键字错误

bash-5.2# ddcmd mod bar +foo
dyndbg: read 13 bytes from userspace
dyndbg: query 0: "mod bar +foo" mod:*
dyndbg: unknown flag 'o'
dyndbg: flags parse failed
dyndbg: processed 1 queries, with 0 matches, 1 errs

因此,匹配规范是一个关键字(选择要比较的调用点属性)和一个要进行比较的值。可能的关键字有

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

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

lineno ::= unsigned-int

注意

line-range 不能包含空格,例如 “1-30” 是有效范围,而 “1 - 30” 不是。

各个关键字的含义如下

func

将给定字符串与每个调用点的函数名进行比较。例如

func svc_tcp_accept
func *recv*             # in rfcomm, bluetooth, ping, tcp
file

将给定字符串与每个调用点的“相对于源码根目录的路径名”或“源文件的基本文件名(basename)”进行比较。例如

file svcsock.c
file kernel/freezer.c   # ie column 1 of control file
file drivers/usb/*      # all callsites under it
file inode.c:start_*    # parse :tail as a func (above)
file inode.c:1-100      # parse :tail as a line-range (above)
module

将给定字符串与每个调用点的模块名进行比较。模块名是在 lsmod 中看到的字符串,即不包含目录或 .ko 后缀,且将 - 转换为 _。例如

module sunrpc
module nfsd
module drm*     # both drm, drm_kms_helper
format

在动态调试格式字符串中搜索给定字符串。注意,该字符串不需要匹配整个格式,只需匹配一部分即可。可以使用 C 语言八进制字符转义 \ooo 符号来转义空白字符和其他特殊字符,例如空格字符是 \040。或者,该字符串可以用双引号(")或单引号(')括起来。例如

format svcrdma:         // many of the NFS/RDMA server pr_debugs
format readahead        // some pr_debugs in the readahead cache
format nfsd:\040SETATTR // one way to match a format with whitespace
format "nfsd: SETATTR"  // a neater way to match a format with whitespace
format 'nfsd: SETATTR'  // yet another way to match a format with whitespace
class

针对每个模块验证给定的 class_name,这些模块可能已经声明了已知 class_name 的列表。如果在某个模块中找到了该 class_name,则继续进行调用点与类的匹配及调整。例如

class DRM_UT_KMS        # a DRM.debug category
class JUNK              # silent non-match
// class TLD_*          # NOTICE: no wildcard in class names
line

将给定的行号或行号范围与每个 pr_debug() 调用点的行号进行比较。单个行号与调用点行号精确匹配。行号范围匹配包含首尾行号在内的任意调用点之间的调用点。空的第一个数字表示文件中的第一行,空的最后一个数字表示文件中的最后一行。例如

line 1603           // exactly line 1603
line 1600-1605      // the six lines from line 1600 to line 1605
line -1605          // the 1605 lines from line 1 to line 1605
line 1600-          // all lines from line 1600 to the end of the file

标志规范由一个更改操作后跟一个或多个标志字符组成。更改操作是以下字符之一

-    remove the given flags
+    add the given flags
=    set the flags to the given flags

标志如下:

p    enables the pr_debug() callsite.
_    enables no flags.

Decorator flags add to the message-prefix, in order:
t    Include thread ID, or <intr>
m    Include module name
f    Include the function name
s    Include the source file name
l    Include line number
d    Include call trace

对于 print_hex_dump_debug()print_hex_dump_bytes(),只有 p 标志具有意义,其他标志将被忽略。

请注意,正则表达式 ^[-+=][fslmptd_]+$ 匹配一个标志规范。要一次性清除所有标志,请使用 =_-fslmptd

引导过程中的调试消息

要在引导过程中激活核心代码和内联模块的调试消息(甚至在用户空间和 debugfs 存在之前),请使用 dyndbg="QUERY"module.dyndbg="QUERY"。QUERY 遵循上述语法,但长度不得超过 1023 个字符。你的引导加载程序(bootloader)可能会施加更严格的限制。

这些 dyndbg 参数在 ddebug 表处理完毕后立即处理,作为 early_initcall 的一部分。因此,你可以通过此引导参数在在此 early_initcall 之后运行的所有代码中启用调试消息。

例如,在 x86 系统上,ACPI 的启用是一个 subsys_initcall,并且

dyndbg="file ec.c +p"

如果你的机器(通常是笔记本电脑)带有嵌入式控制器(Embedded Controller),它将在 ACPI 设置期间显示早期的嵌入式控制器事务。PCI(或其他设备)初始化也是使用此引导参数进行调试的热门候选场景。

如果 foo 模块不是内建的,foo.dyndbg 仍会在引导时被处理(尽管没有效果),但当稍后加载该模块时会重新处理。裸的 dyndbg= 仅在引导时处理。

模块初始化时的调试消息

调用 modprobe foo 时,modprobe 会扫描 /proc/cmdline 以查找 foo.params,剥离 foo. 前缀,并将其与 modprobe 参数或 /etc/modprobe.d/*.conf 文件中给出的参数一起传递给内核,顺序如下

  1. 通过 /etc/modprobe.d/*.conf 给出的参数

    options foo dyndbg=+pt
    options foo dyndbg # defaults to +p
    
  2. 引导参数中给出的 foo.dyndbg,会剥离 foo. 后传递

    foo.dyndbg=" func bar +p; func buz +mp"
    
  3. 传递给 modprobe 的参数

    modprobe foo dyndbg==pmf # override previous settings
    

这些 dyndbg 查询按顺序应用,最后一个具有最终决定权。这允许引导参数覆盖或修改来自 /etc/modprobe.d 的参数(这很合理,因为前者是系统范围的,后者是内核或引导专用的),而 modprobe 参数可以覆盖两者。

foo.dyndbg="QUERY" 形式中,查询必须排除 module foofoo 会从参数名称中提取出来,并应用到 QUERY 中的每个查询中,并且每种类型只允许有 1 个匹配规范(match-spec)。

dyndbg 选项是一个“伪”模块参数,这意味着

  • 模块不需要显式定义它

  • 每个模块都会隐式获得它,无论它们是否使用 pr_debug

  • 它不会出现在 /sys/module/$module/parameters/ 中。要查看它,请 grep 控制文件,或者检查 /proc/cmdline

对于启用了 CONFIG_DYNAMIC_DEBUG 的内核,如果不再需要调试消息,可以通过 debugfs 接口在稍后禁用在引导时给出的任何设置(或在编译期间通过 -DDEBUG 标志启用的设置)

echo "module module_name -p" > /proc/dynamic_debug/control

示例

// enable the message at line 1603 of file svcsock.c
:#> ddcmd 'file svcsock.c line 1603 +p'

// enable all the messages in file svcsock.c
:#> ddcmd 'file svcsock.c +p'

// enable all the messages in the NFS server module
:#> ddcmd 'module nfsd +p'

// enable all 12 messages in the function svc_process()
:#> ddcmd 'func svc_process +p'

// disable all 12 messages in the function svc_process()
:#> ddcmd 'func svc_process -p'

// enable messages for NFS calls READ, READLINK, READDIR and READDIR+.
:#> ddcmd 'format "nfsd: READ" +p'

// enable messages in files of which the paths include string "usb"
:#> ddcmd 'file *usb* +p'

// enable all messages
:#> ddcmd '+p'

// add module, function to all enabled messages
:#> ddcmd '+mf'

// boot-args example, with newlines and comments for readability
Kernel command line: ...
  // see what's going on in dyndbg=value processing
  dynamic_debug.verbose=3
  // enable pr_debugs in the btrfs module (can be builtin or loadable)
  btrfs.dyndbg="+p"
  // enable pr_debugs in all files under init/
  // and the function parse_one, #cmt is stripped
  dyndbg="file init/* +p #cmt ; func parse_one +p"
  // enable pr_debugs in 2 functions in a module loaded later
  pc87360.dyndbg="func pc87360_init_device +p; func pc87360_find +p"

内核配置

动态调试通过内核配置项启用

CONFIG_DYNAMIC_DEBUG=y        # build catalog, enables CORE
CONFIG_DYNAMIC_DEBUG_CORE=y   # enable mechanics only, skip catalog

如果你不想全局启用动态调试(例如在某些嵌入式系统中),你可以设置 CONFIG_DYNAMIC_DEBUG_CORE 作为动态调试的基本支持,并在你稍后想要动态调试的任何模块的 Makefile 中添加 ccflags := -DDYNAMIC_DEBUG_MODULE

内核 prdbg API

启用动态调试后,以下函数将被编入目录并可控

pr_debug()
dev_dbg()
print_hex_dump_debug()
print_hex_dump_bytes()

否则,它们默认是关闭的;在源文件中使用 ccflags += -DDEBUG#define DEBUG 将适当地启用它们。

如果未设置 CONFIG_DYNAMIC_DEBUG,则 print_hex_dump_debug() 只是 print_hex_dump(KERN_DEBUG) 的快捷方式。

对于 print_hex_dump_debug()/print_hex_dump_bytes(),如果格式字符串是常量字符串,则它是其 prefix_str 参数;如果 prefix_str 是动态构建的,则格式字符串为 hexdump