动态调试¶
简介¶
动态调试允许你动态地启用/禁用内核调试打印(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 文件中给出的参数一起传递给内核,顺序如下
通过
/etc/modprobe.d/*.conf给出的参数options foo dyndbg=+pt options foo dyndbg # defaults to +p
引导参数中给出的
foo.dyndbg,会剥离foo.后传递foo.dyndbg=" func bar +p; func buz +mp"
传递给 modprobe 的参数
modprobe foo dyndbg==pmf # override previous settings
这些 dyndbg 查询按顺序应用,最后一个具有最终决定权。这允许引导参数覆盖或修改来自 /etc/modprobe.d 的参数(这很合理,因为前者是系统范围的,后者是内核或引导专用的),而 modprobe 参数可以覆盖两者。
在 foo.dyndbg="QUERY" 形式中,查询必须排除 module foo。foo 会从参数名称中提取出来,并应用到 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。