文章目录
- 1. 前言
- 2. 什么是 Dynamic debug (dyndbg) ?
- 3. Dynamic debug (dyndbg) 的使用
- 3.1 开启 Dynamic debug (dyndbg) 功能
- 3.2 使用 Dynamic debug (dyndbg) 功能
- 4. Dynamic debug (dyndbg) 的实现
- 4.1 内核接口 dynamic_pr_debug() 的实现
- 4.2 debugfs 导出控制节点 control 实现
- 4.2.1 Dynamic debug 初始化
- 4.2.2 Dynamic debug 的用户空间接口
- 4.2.2.1 control 文件的建立
- 4.2.2.2 control 文件的查询(读)
- 4.2.2.3 control 文件的修改(写)
- 4.3 外置模块的 dyndbg 参数
- 4.3.1 外置模块 dyndbg 参数的解析
- 4.3.2 外置模块 dyndbg 参数的提供方式
- 4.3.2.1 通过模块配置文件
- 4.3.2.2 通过内核命令行参数
- 4.3.2.3 通过模块参数
- 5. Dynamic debug (dyndbg) 的优缺点
- 6. 参考资料
1. 前言
限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。
2. 什么是 Dynamic debug (dyndbg) ?
Dynamic debug(dyndbg)
功能,简单来讲,就是允许用户空间通过 debugfs
导出的文件节点 /sys/kernel/debug/dynamic_debug/control
,动态的
、在运行时
控制 Linux
内核 KERN_DEBUG
类型日志的开启和关闭。在没开启 Dynamic debug
时,直到下次重新编译内核做出改变之前,内核 KERN_DEBUG
类型日志要么一直关闭,要么一直开启,无法在运行时做出调整。
3. Dynamic debug (dyndbg) 的使用
3.1 开启 Dynamic debug (dyndbg) 功能
来看 Linux
内核配置文件 lib/Kconfig.debug
中,配置项 CONFIG_DYNAMIC_DEBUG
的定义:
config DYNAMIC_DEBUGbool "Enable dynamic printk() support"default ndepends on PRINTKdepends on DEBUG_FShelpCompiles debug level messages into the kernel, which would nototherwise be available at runtime. These messages can then beenabled/disabled based on various levels of scope - per source file,function, module, format string, and line number. This mechanismimplicitly compiles in all pr_debug() and dev_dbg() calls, whichenlarges the kernel text size by about 2%.If a source file is compiled with DEBUG flag set, anypr_debug() calls in it are enabled by default, but can bedisabled at runtime as below. Note that DEBUG flag isturned on by many CONFIG_*DEBUG* options.Usage:......
要开启 Dynamic debug
功能,要开启配置项 CONFIG_DYNAMIC_DEBUG
,其依赖于配置项 CONFIG_PRINTK
和 CONFIG_DEBUG_FS
。由于最终调用的打印接口都是 printk()
,所以要开启 CONFIG_PRINTK
;另外 Dynamic debug
通过 debugfs
导出的文件节点 /sys/kernel/debug/dynamic_debug/control
控制每个 KERN_DEBUG
调试语句,所以需要启用 CONFIG_DEBUG_FS
。
学习一样技能,最好先学会如何使用它,这往往是一个不错的起点。本文采用 QEMU + vexpress-a9 + Linux 4.14.132 + ubuntu-base-16.04-core-armhf.tar.gz
组合来进行 Dynamic debug
功能测试。如何使用 QEMU
搭建需要的测试环境,不在本文的讨论范围之内,读者可以自行查询相关资料或去笔者的 Gitee 仓库 去拿一个构建脚本。
开启 CONFIG_DYNAMIC_DEBUG
和其依赖配置项 CONFIG_PRINTK
和 CONFIG_DEBUG_FS
。这里只展示 CONFIG_DYNAMIC_DEBUG
的开启,CONFIG_PRINTK
和 CONFIG_DEBUG_FS
默认处于开启状态。
$ cd linux-4.14.132
$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- O=output menuconfig
然后编译运行内核:
$ cd linux-4.14.132
$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- O=output -j8
$ cd ..
$ sudo qemu-system-arm \-M vexpress-a9 -smp 4 -m 512M \-kernel linux-4.14.132/output/arch/arm/boot/zImage \-dtb linux-4.14.132/output/arch/arm/boot/dts/vexpress-v2p-ca9.dtb \-nographic \-append "root=/dev/mmcblk0 rw rootfstype=ext4 console=ttyAMA0" \-sd rootfs/arm-ubuntu-16.04.img
其中,rootfs/arm-ubuntu-16.04.img
是根文件系统,可通过前面笔者提供的脚本构建,或者读者自行查阅相关资料。运行并登录后,查看 /sys/kernel/debug
目录,会发现一个名为 dynamic_debug
的目录,目录下有一个 control
文件:
root@qemu-ubuntu:~# ls /sys/kernel/debug/ -l
......
drwxr-xr-x 2 root root 0 Jan 1 1970 dynamic_debug
......
root@qemu-ubuntu:~# ls /sys/kernel/debug/dynamic_debug/ -l
total 0
-rw-r--r-- 1 root root 0 Jan 1 1970 control
可以看到,文件 /sys/kernel/debug/dynamic_debug/control
具有可读写权限,读下它看它包含些什么内容:
root@qemu-ubuntu:~# cat /sys/kernel/debug/dynamic_debug/control
# filename:lineno [module]function flags format
init/main.c:743 [main]initcall_blacklist =p "blacklisting initcall %s\012"
init/main.c:774 [main]initcall_blacklisted =p "initcall %s blacklisted\012"
init/initramfs.c:485 [initramfs]unpack_to_rootfs =_ "Detected %s compressed data\012"
arch/arm/vfp/vfpmodule.c:300 [vfp]vfp_emulate_instruction =_ "VFP: emulate: INST=0x%08x SCR=0x%08x\012"
[......]
lib/kobject_uevent.c:392 [kobject_uevent]kobject_uevent_env =_ "kobject: '%s' (%p): %s: unset subsystem caused the event to drop!\012"
lib/kobject_uevent.c:434 [kobject_uevent]kobject_uevent_env =_ "kobject: '%s' (%p): %s: uevent() returned %d\012"
输出内容实在有点太多了,这里只截取一部分。对于每行输出内容的格式,第一行
给出了注释:
# filename:lineno [module]function flags format
用这个格式解释下第二行
内容:
init/main.c:743 [main]initcall_blacklist =p "blacklisting initcall %s\012"
filename => init/main.c
lineno => 743
[module]function => [main]initcall_blacklist
flags => =p
format => "blacklisting initcall %s\012"
让人很好奇不是,文件 init/main.c
的 743
行,函数 initcall_blacklist()
到底包含是什么内容?来看一下:
都猜到了吧?正是一个 pr_debug()
调用。
3.2 使用 Dynamic debug (dyndbg) 功能
读
/sys/kernel/debug/dynamic_debug/control
文件,将列举内核中当前可通过写
control
文件使能、关闭的调试语句信息。对 control
文件的读
、写
分别代表不同的含义。对 control
文件写入操作的语法如下:
command ::= match-spec* flags-specmatch-spec ::= 'func' string |'file' string |'module' string |'format' string |'line' line-rangeline-range ::= lineno |'-'lineno |lineno'-' |lineno'-'linenolineno ::= unsigned-int// 对于 flags-spec,内核文档不知道是不是漏掉了,
// 这里是笔者给出的定义,不太准确,读者能理解意思即可。
flags-spec ::= flag-ops flags
flags-ops ::= - | + | =
flags ::= p | f | l | m | t | _
对 flags
支持如下 flag-ops
操作,包括 - (移除)
、+ (添加)
、= (赋值)
:
- remove the given flags
+ add the given flags
= set the flags to the given flags
有下列可选 flags
:
p enables the pr_debug() callsite.
f Include the function name in the printed message
l Include line number in the printed message
m Include module name in the printed message
t Include thread ID in messages not generated from interrupt context
_ No flags are set. (Or'd with others on input)
由于内核原有原有代码的 pr_debug()
, dev_debug()
等调试语句,要依赖一定条件才能触发,对于演示 Dynamic debug
功能的使用不是很方便,也不够直观,所以本文通过编写一个测试模块来演示 Dynamic debug
功能的使用。Dynamic debug
功能测试模块的代码 dynamic_debug_example.c
如下:
#include <linux/module.h>
#include <linux/sched.h>
#include <linux/kthread.h>
#include <linux/delay.h>static struct task_struct *dyndbg_tsk;static int dyndbg_task_fn(void *ignored)
{int count = 0;int ret = 0;while (!kthread_should_stop()) {pr_debug("dyndbg test message %d\n", count++);msleep(2000);}return ret;
}static int __init dyndbg_test_init(void)
{int ret = 0;printk(KERN_INFO "dyndbg test module init.\n");dyndbg_tsk = kthread_run(dyndbg_task_fn, NULL, "dyndbg_test");if (IS_ERR(dyndbg_tsk)) {ret = PTR_ERR(dyndbg_tsk);printk(KERN_ERR "%s: Failed to create kernel thread, ret = [%d]\n", __func__, ret);}return ret;
}module_init(dyndbg_test_init);
module_exit(dyndbg_test_exit);MODULE_LICENSE("GPL");
编译模块并添加到根文件系统镜像,然后启动 QEMU 运行:
root@qemu-ubuntu:~# lsmod
Module Size Used by
root@qemu-ubuntu:~# cat /sys/kernel/debug/dynamic_debug/control | grep "dynamic_debug_example"
通过查看 control
文件,没有看到关于测试模块 dynamic_debug_example.c
的任何信息,这是因为测试模块还没有加载,加载模块后再来看一下:
root@qemu-ubuntu:~# modprobe dynamic_debug_example
root@qemu-ubuntu:~# lsmod
Module Size Used by
dynamic_debug_example 16384 0
root@qemu-ubuntu:~# cat /sys/kernel/debug/dynamic_debug/control | grep "dynamic_debug_example"
samples/dynamic_debug/dynamic_debug_example.c:16 [dynamic_debug_example]dyndbg_task_fn =_ "dyndbg test message %d\012"
看看,samples/dynamic_debug/dynamic_debug_example.c
已经出现在 control
的读
输出中了。但是查看当前的内核日志,还是没发现测试模块的日志:
root@qemu-ubuntu:~# dmesg | grep "dyndbg test message"
我们注意到,control
的读
输出中关于 dynamic_debug_example.c
的信息中,带有 =_
标记,这表示 dynamic_debug_example.c
文件中第 16
行的 pr_debug()
没有开启。是时候施展魔法标记 +p
来开启这个调试语句,来看看效果:
root@qemu-ubuntu:~# echo -n "file samples/dynamic_debug/dynamic_debug_example.c +p" > /sys/kernel/debug/dynamic_debug/controlroot@qemu-ubuntu:~# dmesg | tail -n 8
[ 1677.219423] dyndbg test message 2
[ 1679.410816] dyndbg test message 3
[ 1681.580533] dyndbg test message 4
[ 1683.792157] dyndbg test message 5
[ 1686.052255] dyndbg test message 6
[ 1688.202825] dyndbg test message 7
[ 1690.373784] dyndbg test message 8
[ 1692.562494] dyndbg test message 9root@qemu-ubuntu:~# cat /sys/kernel/debug/dynamic_debug/control | grep "dynamic_debug_example"
samples/dynamic_debug/dynamic_debug_example.c:16 [dynamic_debug_example]dyndbg_task_fn =p "dyndbg test message %d\012"
可以看到,现在有了测试模块的日志;另外,读 control
文件的输出, dynamic_debug_example.c
第 16
行调试语句的标志已经变为了 =p
了,证明已经开启了。
嗯,越打越多,有点烦人,通过 -p
标记关闭这个调试语句:
root@qemu-ubuntu:~# echo -n "file samples/dynamic_debug/dynamic_debug_example.c -p" > /sys/kernel/debug/dynamic_debug/control
root@qemu-ubuntu:~# cat /sys/kernel/debug/dynamic_debug/control | grep "dynamic_debug_example"
samples/dynamic_debug/dynamic_debug_example.c:16 [dynamic_debug_example]dyndbg_task_fn =_ "dyndbg test message %d\012"
查看调试语句的标记已经变为 =_
了,同时也确实没有日志输出了。通过 +pflm
标记玩一个花一点的输出:
root@qemu-ubuntu:~# echo -n "file samples/dynamic_debug/dynamic_debug_example.c +pflm" > /sys/kernel/debug/dynamic_debug/control
[ 2287.948918] dynamic_debug_example:dyndbg_task_fn:16: dyndbg test message 158
[ 2290.159226] dynamic_debug_example:dyndbg_task_fn:16: dyndbg test message 159
[ 2292.440786] dynamic_debug_example:dyndbg_task_fn:16: dyndbg test message 160
[ 2294.640683] dynamic_debug_example:dyndbg_task_fn:16: dyndbg test message 161
[ 2296.801579] dynamic_debug_example:dyndbg_task_fn:16: dyndbg test message 162
root@qemu-ubuntu:~# cat /sys/kernel/debug/dynamic_debug/control | grep "dynamic_debug_example"
samples/dynamic_debug/dynamic_debug_example.c:16 [dynamic_debug_example]dyndbg_task_fn =pmfl "dyndbg test message %d\012"
root@qemu-ubuntu:~# echo -n "file samples/dynamic_debug/dynamic_debug_example.c -pflm" > /sys/kernel/debug/dynamic_debug/control
在 +pflm
标记的加持下,调试日志相对之前的输出,增加了 模块名
(+m
)、函数名
(+f
)、行号(+l
) 的输出。
本文对 Dynamic debug
输出的示范,就演示到此。关注 Dynamic debug
输出更多用法的读者,可参考 Linux 内核文档 Dynamic debug 。
4. Dynamic debug (dyndbg) 的实现
4.1 内核接口 dynamic_pr_debug() 的实现
对于 Linux
内核的 Dynamic debug
功能,我们可能既熟悉又陌生。来看下 include/linux/printk.h
和 include/linux/device.h
中的相关定义:
/* include/linux/printk.h *//* 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
/* include/linux/device.h */#if defined(CONFIG_DYNAMIC_DEBUG)
#define dev_dbg(dev, format, ...) \
do { \dynamic_dev_dbg(dev, format, ##__VA_ARGS__); \
} while (0)
#elif defined(DEBUG)
#define dev_dbg(dev, format, arg...) \dev_printk(KERN_DEBUG, dev, format, ##arg)
#else
#define dev_dbg(dev, format, arg...) \
({ \if (0) \dev_printk(KERN_DEBUG, dev, format, ##arg); \
})
#endif
从上面 pr_debug()
和 dev_dbg()
定义看到,开启 Dynamic debug
的情形下(即配置 CONFIG_DYNAMIC_DEBUG=y
),pr_debug()
实现为 dynamic_pr_debug()
,dev_dbg()
实现为 dynamic_dev_dbg()
,这意味着:
1. Dynamic debug 只影响 KERN_DEBUG 类型的内核日志。
2. Dynamic debug 影响所有基于 pr_debug(), dev_dbg() 的 API 接口。
继续看 dynamic_pr_debug()
的实现细节:
/* include/linux/dynamic_debug.h */#ifdef HAVE_JUMP_LABEL#define dd_key_init(key, init) key = (init)#ifdef DEBUG
#define DEFINE_DYNAMIC_DEBUG_METADATA(name, fmt) \DEFINE_DYNAMIC_DEBUG_METADATA_KEY(name, fmt, .key.dd_key_true, \(STATIC_KEY_TRUE_INIT))#define DYNAMIC_DEBUG_BRANCH(descriptor) \static_branch_likely(&descriptor.key.dd_key_true)
#else /* !DEBUG */
#define DEFINE_DYNAMIC_DEBUG_METADATA(name, fmt) \DEFINE_DYNAMIC_DEBUG_METADATA_KEY(name, fmt, .key.dd_key_false, \(STATIC_KEY_FALSE_INIT))#define DYNAMIC_DEBUG_BRANCH(descriptor) \static_branch_unlikely(&descriptor.key.dd_key_false)
#endif /* DEBUG */#else /* !HAVE_JUMP_LABEL */#define dd_key_init(key, init)#define DEFINE_DYNAMIC_DEBUG_METADATA(name, fmt) \DEFINE_DYNAMIC_DEBUG_METADATA_KEY(name, fmt, 0, 0)#ifdef DEBUG
#define DYNAMIC_DEBUG_BRANCH(descriptor) \likely(descriptor.flags & _DPRINTK_FLAGS_PRINT)
#else /* !DEBUG */
#define DYNAMIC_DEBUG_BRANCH(descriptor) \unlikely(descriptor.flags & _DPRINTK_FLAGS_PRINT)
#endif /* DEBUG */#endif /* HAVE_JUMP_LABEL */#define dynamic_pr_debug(fmt, ...) \
do { \DEFINE_DYNAMIC_DEBUG_METADATA(descriptor, fmt); \if (DYNAMIC_DEBUG_BRANCH(descriptor)) \__dynamic_pr_debug(&descriptor, pr_fmt(fmt), \##__VA_ARGS__); \
} while (0)
把 dynamic_pr_debug()
的定义按 HAVE_JUMP_LABEL
和 DEBUG
的不同组合展开,得到如下 4
种可能的代码片段:
/* (1) HAVE_JUMP_LABEL && DEBUG */static struct _ddebug __aligned(8)
__attribute__((section("__verbose"))) descriptor = {.modname = KBUILD_MODNAME,.function = __func__,.filename = __FILE__,.format = (fmt),.lineno = __LINE__, .flags = _DPRINTK_FLAGS_PRINT,.key.dd_key_true = STATIC_KEY_TRUE_INIT, /* 调试语句 默认为 启用 状态 */
};
if (static_branch_likely(&descriptor.key.dd_key_true))__dynamic_pr_debug(&descriptor, pr_fmt(fmt), ##__VA_ARGS__);
/* (2) HAVE_JUMP_LABEL && !DEBUG */static struct _ddebug __aligned(8)
__attribute__((section("__verbose"))) descriptor = {.modname = KBUILD_MODNAME,.function = __func__,.filename = __FILE__,.format = (fmt),.lineno = __LINE__, .flags = 0,.key.dd_key_true = STATIC_KEY_FALSE_INIT, /* 调试语句 默认为 关闭 状态 */
};
if (static_branch_unlikely(&descriptor.key.dd_key_false))__dynamic_pr_debug(&descriptor, pr_fmt(fmt), ##__VA_ARGS__);
/* (3) !HAVE_JUMP_LABEL && DEBUG */static struct _ddebug __aligned(8)
__attribute__((section("__verbose"))) descriptor = {.modname = KBUILD_MODNAME,.function = __func__,.filename = __FILE__,.format = (fmt),.lineno = __LINE__, .flags = _DPRINTK_FLAGS_PRINT, /* 调试语句 默认为 启用 状态 */
};
if (likely(descriptor.flags & _DPRINTK_FLAGS_PRINT))__dynamic_pr_debug(&descriptor, pr_fmt(fmt), ##__VA_ARGS__);
/* (4) !HAVE_JUMP_LABEL && !DEBUG */static struct _ddebug __aligned(8)
__attribute__((section("__verbose"))) descriptor = {.modname = KBUILD_MODNAME,.function = __func__,.filename = __FILE__,.format = (fmt),.lineno = __LINE__, .flags = 0, /* 调试语句 默认为 关闭 状态 */
};
if (unlikely(descriptor.flags & _DPRINTK_FLAGS_PRINT))__dynamic_pr_debug(&descriptor, pr_fmt(fmt), ##__VA_ARGS__);
从上面的代码片段看到,内核为每个 pr_debug()(, dev_dbg())
语句建立了一个 struct _ddebug
对象来管理它们,在编译时
将 struct _ddebug
放置在名为 "__verbose"
的 section
内,在链接时
通过 include/asm-generic/vmlinux.lds.h
如下链接脚本片段
/** .data section*/
#define DATA_DATA \...... \/* implement dynamic printk debug */ \. = ALIGN(8); \VMLINUX_SYMBOL(__start___jump_table) = .; \KEEP(*(__jump_table)) \VMLINUX_SYMBOL(__stop___jump_table) = .; \. = ALIGN(8); \VMLINUX_SYMBOL(__start___verbose) = .; \KEEP(*(__verbose)) \VMLINUX_SYMBOL(__stop___verbose) = .; \......
将所有编译输出的、包含 struct _ddebug
的 "__verbose"
section
全部放入区间 [__start___verbose, __stop___verbose)
内。这些信息的用途将在后面分析章节进一步讨论,这里暂不做展开。
从上面的 4
种不同配置组合的代码片段,从上面的分析,dynamic_pr_debug()
的逻辑就很清楚了:通过判定一个标记,来决定是否调用 __dynamic_pr_debug()
打印调试输出。当然,对于上面 4
种可能出现的组合,分别在 HAVE_JUMP_LABEL
和 !HAVE_JUMP_LABEL
时,判定的标记有所不同。对于 HAVE_JUMP_LABEL
的场景,稍显复杂,更多的细节读者可自行查阅 Jump label
相关资料,或参考博文 Linux: Jump label实现简析 ;而对于 !HAVE_JUMP_LABEL
场景,则是一目了然,不过是对 descriptor.flags
的判定。
最后看一下 __dynamic_pr_debug()
实现:
/* lib/dynamic_debug.c */static char *dynamic_emit_prefix(const struct _ddebug *desc, char *buf)
{int pos_after_tid;int pos = 0;*buf = '\0';if (desc->flags & _DPRINTK_FLAGS_INCL_TID) { /* 't' 标记 */if (in_interrupt())pos += snprintf(buf + pos, remaining(pos), "<intr> "); /* 在中断上下文,用 "<intr>" 代替 线程 ID */elsepos += snprintf(buf + pos, remaining(pos), "[%d] ", /* 线程 ID */task_pid_vnr(current));}pos_after_tid = pos;if (desc->flags & _DPRINTK_FLAGS_INCL_MODNAME) /* 'm' 标记 */pos += snprintf(buf + pos, remaining(pos), "%s:",desc->modname); /* 模块名 */if (desc->flags & _DPRINTK_FLAGS_INCL_FUNCNAME) /* 'f' 标记 */pos += snprintf(buf + pos, remaining(pos), "%s:",desc->function); /* 函数名 */if (desc->flags & _DPRINTK_FLAGS_INCL_LINENO) /* 'l' 标记 */pos += snprintf(buf + pos, remaining(pos), "%d:",desc->lineno); /* 行号 */if (pos - pos_after_tid)pos += snprintf(buf + pos, remaining(pos), " ");if (pos >= PREFIX_SIZE)buf[PREFIX_SIZE - 1] = '\0';return buf;
}void __dynamic_pr_debug(struct _ddebug *descriptor, const char *fmt, ...)
{va_list args;struct va_format vaf;char buf[PREFIX_SIZE];BUG_ON(!descriptor);BUG_ON(!fmt);va_start(args, fmt);vaf.fmt = fmt;vaf.va = &args;/* 最终调用 printk() 输出 */printk(KERN_DEBUG "%s%pV", dynamic_emit_prefix(descriptor, buf), &vaf);va_end(args);
}
4.2 debugfs 导出控制节点 control 实现
4.2.1 Dynamic debug 初始化
/* lib/dynamic_debug.c */static int __init dynamic_debug_init(void)
{struct _ddebug *iter, *iter_start;const char *modname = NULL;char *cmdline;int ret = 0;int n = 0, entries = 0, modct = 0;int verbose_bytes = 0;/** [__start___verbose, __stop___verbose):* 为系列接口* pr_debug() -> dynamic_pr_debug() * dev_dbg() -> dynamic_dev_dbg()* netdev_dbg() -> dynamic_netdev_dbg()* print_hex_dump_bytes(), print_hex_dump_debug() -> dynamic_hex_dump()* 每个调用定义的 struct _ddebug 对象列表。*/if (__start___verbose == __stop___verbose) {pr_warn("_ddebug table is empty in a CONFIG_DYNAMIC_DEBUG build\n");return 1;}iter = __start___verbose;modname = iter->modname;iter_start = iter;for (; iter < __stop___verbose; iter++) {entries++; /* dyndbg 调试语句总数 +1 *//* 非调试信息部分总长度: 模块名 + 函数名 + 文件名 + 格式字串 */verbose_bytes += strlen(iter->modname) + strlen(iter->function)+ strlen(iter->filename) + strlen(iter->format);if (strcmp(modname, iter->modname)) { /* 不同于当前模块 @modname 的 新模块 @iter->modname */modct++; /* 模块总数 +1 *//* 添加当前模块 @modname 到 模块表 @ddebug_tables */ret = ddebug_add_module(iter_start, n, modname);if (ret)goto out_err;/* * 更新当前 dyndb 模块:*/n = 0; /* 复位当前模块包含的 dyndb 调试语句总数 */modname = iter->modname; /* 更新当前 dyndb 模块名 */iter_start = iter; /* 当前模块的首个 dyndb 调试语句信息对象 */}n++; /* 当前模块包含的 dyndb 调试语句总数 */}ret = ddebug_add_module(iter_start, n, modname); /* 添加最后一个 dyndb 模块 */if (ret)goto out_err;ddebug_init_success = 1; /* 标记 dyndb 初始化完成 */vpr_info("%d modules, %d entries and %d bytes in ddebug tables, %d bytes in (readonly) verbose section\n",modct, entries, (int)(modct * sizeof(struct ddebug_table)),verbose_bytes + (int)(__stop___verbose - __start___verbose));/* apply ddebug_query boot param, dont unload tables on err *//* * 可通过内核命令行参数 ddebug_query="QUERY" 指定 BOOT 期间启用的调试语句。* 如 ddebug_query="file xxx.c +p" 开启 xxx.c 中的所有 dyndbg 调试语句。* 已废弃,内核不建议使用 ddebug_query="QUERY" 。* ddebug_setup_string[] 设置见后面的分析。*/if (ddebug_setup_string[0] != '\0') {pr_warn("ddebug_query param name is deprecated, change it to dyndbg\n");ret = ddebug_exec_queries(ddebug_setup_string, NULL);if (ret < 0)pr_warn("Invalid ddebug boot param %s\n",ddebug_setup_string);elsepr_info("%d changes by ddebug_query\n", ret);}/* now that ddebug tables are loaded, process all boot args* again to find and activate queries given in dyndbg params.* While this has already been done for known boot params, it* ignored the unknown ones (dyndbg in particular). Reusing* parse_args avoids ad-hoc parsing. This will also attempt* to activate queries for not-yet-loaded modules, which is* slightly noisy if verbose, but harmless.*//** 处理内核命令行参数: dyndbg="QUERY", module.dyndbg="QUERY"* module.dyndbg="QUERY" 用于内核内置模块。*/cmdline = kstrdup(saved_command_line, GFP_KERNEL);parse_args("dyndbg params", cmdline, NULL,0, 0, 0, NULL, &ddebug_dyndbg_boot_param_cb);kfree(cmdline);return 0;out_err:ddebug_remove_all_tables();return 0;
}
/* Allow early initialization for boot messages via boot param */
early_initcall(dynamic_debug_init);
前面演示的都是在内核启动完成后对 dyndbg
调试语句的控制,那如果在内核 BOOT
启动阶段,需要控制 dyndbg
语句的启停呢?方式一
通过内核命令行参数 ddebug_query="QUERY"
,dyndbg="QUERY"
,module.dyndbg="QUERY"
来完成;方式二
是在包含的调试接口头文件前定义 DEBUG
宏,这样 dyndbg
调试语句默认就是开启的
(详见 4.1
中 dynamic_pr_debug()
展开的 4
种形式)。其中 ddebug_query="QUERY"
已废弃,不建议使用;module.dyndbg="QUERY"
针对编译到内核镜像的内置模块
,如 dynamic_debug_example.dyndbg
指定模块是外置模块 dynamic_debug_example.ko
,虽然内核启动阶段仍然会对这种情形处理,但不会有任何效果,只有在 modprobe/insmod
时才会真正处理外置模块
的 dyndbg
参数,这会后面的章节 4.3 外置模块的 dyndbg 参数
进行分析。
先看下 ddebug_setup_string[]
的设置过程(即解析内核命令行参数 ddebug_query="QUERY"
):
/* lib/dynamic_debug.c */#define DDEBUG_STRING_SIZE 1024
static __initdata char ddebug_setup_string[DDEBUG_STRING_SIZE];static __init int ddebug_setup_query(char *str)
{if (strlen(str) >= DDEBUG_STRING_SIZE) {pr_warn("ddebug boot param string too large\n");return 0;}strlcpy(ddebug_setup_string, str, DDEBUG_STRING_SIZE);return 1;
}__setup("ddebug_query=", ddebug_setup_query);
对 ddebug_setup_string[]
的处理(即对内核命令行参数 ddebug_query="QUERY"
处理):
dynamic_debug_init()/* 处理 ddebug_query="QUERY" */ret = ddebug_exec_queries(ddebug_setup_string, NULL);
对内核命令行参数 dyndbg="QUERY"
,module.dyndbg="QUERY"
的处理:
dynamic_debug_init()cmdline = kstrdup(saved_command_line, GFP_KERNEL);parse_args("dyndbg params", cmdline, NULL,0, 0, 0, NULL, &ddebug_dyndbg_boot_param_cb);...ddebug_dyndbg_boot_param_cb()return ddebug_dyndbg_param_cb(param, val, NULL, 0);/* helper for ddebug_dyndbg_(boot|module)_param_cb */
static int ddebug_dyndbg_param_cb(char *param, char *val,const char *modname, int on_err)
{char *sep;sep = strchr(param, '.'); /* module.dyndbg="QUERY" */if (sep) {/* needed only for ddebug_dyndbg_boot_param_cb */*sep = '\0';modname = param;param = sep + 1;} /*else {*//* dyndbg="QUERY" *//*}*/if (strcmp(param, "dyndbg"))return on_err; /* determined by caller */ddebug_exec_queries((val ? val : "+p"), modname);return 0; /* query failure shouldnt stop module load */
}
可见,不管是处理 ddebug_query="QUERY"
,还是处理 module.dyndbg="QUERY"
、dyndbg="QUERY"
,最终都调用 ddebug_exec_queries()
来处理。下面分析 ddebug_exec_queries()
的处理过程:
/* handle multiple queries in query string, continue on error, returnlast error or number of matching callsites. Module name is eitherin param (for boot arg) or perhaps in query string.
*/
static int ddebug_exec_queries(char *query, const char *modname)
{char *split;int i, errs = 0, exitcode = 0, rc, nfound = 0;for (i = 0; query; query = split) {split = strpbrk(query, ";\n");if (split)*split++ = '\0';query = skip_spaces(query);if (!query || !*query || *query == '#')continue;vpr_info("query %d: \"%s\"\n", i, query);/* 查询、修改(使能 或 关闭) 对应的 dyndbg 调试语句 */rc = ddebug_exec_query(query, modname);if (rc < 0) {errs++;exitcode = rc;} else {nfound += rc;}i++;}...if (exitcode)return exitcode;return nfound;
}/* 查询、修改(使能 或 关闭) 对应的 dyndbg 调试语句 */
static int ddebug_exec_query(char *query_string, const char *modname)
{unsigned int flags = 0, mask = 0;struct ddebug_query query;
#define MAXWORDS 9int nwords, nfound;char *words[MAXWORDS];nwords = ddebug_tokenize(query_string, words, MAXWORDS);if (nwords <= 0) {pr_err("tokenize failed\n");return -EINVAL;}/* check flags 1st (last arg) so query is pairs of spec,val *//* * . 解析 flags 操作符: + (增加), - (移除), = (赋值) * . 解析 flags: p, m, f, l, t, _ */if (ddebug_parse_flags(words[nwords-1], &flags, &mask)) {pr_err("flags parse failed\n");return -EINVAL;}/* 查询 dyndbg 调试语句表项 */if (ddebug_parse_query(words, nwords-1, &query, modname)) {pr_err("query parse failed\n");return -EINVAL;}/* actually go and implement the change */nfound = ddebug_change(&query, flags, mask); /* 按请求修改对应的 dyndbg 调试语句表项 */vpr_info_dq(&query, nfound ? "applied" : "no-match");return nfound;
}static int ddebug_change(const struct ddebug_query *query,unsigned int flags, unsigned int mask)
{int i;struct ddebug_table *dt;unsigned int newflags;unsigned int nfound = 0;char flagbuf[10];/* search for matching ddebugs */mutex_lock(&ddebug_lock);list_for_each_entry(dt, &ddebug_tables, link) {...for (i = 0; i < dt->num_ddebugs; i++) {struct _ddebug *dp = &dt->ddebugs[i];...nfound++;newflags = (dp->flags & mask) | flags;if (newflags == dp->flags)continue;/** dyndbg 系列接口* pr_debug() -> dynamic_pr_debug() * dev_dbg() -> dynamic_dev_dbg()* netdev_dbg() -> dynamic_netdev_dbg()* print_hex_dump_bytes(), print_hex_dump_debug() -> dynamic_hex_dump()* 在* (1) HAVE_JUMP_LABEL 情形,使能 或 关闭 static key ,* 通过 static key 状态决定是否输出调试信息;* (2) !HAVE_JUMP_LABEL 情形,仅修改 flags ,通过* 通过 flags 状态决定是否输出调试信息。*/
#ifdef HAVE_JUMP_LABELif (dp->flags & _DPRINTK_FLAGS_PRINT) {if (!(flags & _DPRINTK_FLAGS_PRINT))static_branch_disable(&dp->key.dd_key_true);} else if (flags & _DPRINTK_FLAGS_PRINT)static_branch_enable(&dp->key.dd_key_true);
#endif/* !HAVE_JUMP_LABEL 情形,仅修改 flags,是通过 flags */dp->flags = newflags;...}}mutex_unlock(&ddebug_lock);...return nfound;
}
4.2.2 Dynamic debug 的用户空间接口
4.2.2.1 control 文件的建立
static const struct file_operations ddebug_proc_fops = {.owner = THIS_MODULE,.open = ddebug_proc_open,.read = seq_read,.llseek = seq_lseek,.release = seq_release_private,.write = ddebug_proc_write
};static int __init dynamic_debug_init_debugfs(void)
{struct dentry *dir, *file;if (!ddebug_init_success)return -ENODEV;/* 创建 /sys/kernel/debug/dynamic_debug 目录 */dir = debugfs_create_dir("dynamic_debug", NULL);if (!dir)return -ENOMEM;/* 创建 /sys/kernel/debug/dynamic_debug/control 文件 */file = debugfs_create_file("control", 0644, dir, NULL,&ddebug_proc_fops);if (!file) {debugfs_remove(dir);return -ENOMEM;}return 0;
}/* Debugfs setup must be done later */
fs_initcall(dynamic_debug_init_debugfs);
4.2.2.2 control 文件的查询(读)
用户空间通过查询(读) control 文件
,查看系统中所有 dyndbg
调试语句的状态信息:
# cat /sys/kernel/debug/dynamic_debug/control
[......]
这在前面已经演示过,这里主要看它的内核实现。cat
首先触发 ddebug_proc_open()
调用:
static const struct seq_operations ddebug_proc_seqops = {.start = ddebug_proc_start,.next = ddebug_proc_next,.show = ddebug_proc_show,.stop = ddebug_proc_stop
};static int ddebug_proc_open(struct inode *inode, struct file *file)
{vpr_info("called\n");return seq_open_private(file, &ddebug_proc_seqops,sizeof(struct ddebug_iter));
}
接着调用 ddebug_proc_start
() :
ddebug_proc_start()dp = ddebug_iter_first(iter); /* 取 @ddebug_tables 表中第1条 dyndbg 调试语句信息 */
然后通过 ddebug_proc_show()
打印第 1 条信息:
static int ddebug_proc_show(struct seq_file *m, void *p)
{struct ddebug_iter *iter = m->private;struct _ddebug *dp = p;char flagsbuf[10];vpr_info("called m=%p p=%p\n", m, p);if (p == SEQ_START_TOKEN) { /* 第 1 条信息前,加个 标题栏 打印 */seq_puts(m,"# filename:lineno [module]function flags format\n");return 0;}/* 打印一条 dyndbg 调试语句信息 struct _ddebug */seq_printf(m, "%s:%u [%s]%s =%s \"",trim_prefix(dp->filename), dp->lineno,iter->table->mod_name, dp->function,ddebug_describe_flags(dp, flagsbuf, sizeof(flagsbuf)));seq_escape(m, dp->format, "\t\r\n\"");seq_puts(m, "\"\n");return 0;
}
然后通过 ddebug_proc_next()
取下一条数据,再通过 ddebug_proc_show()
打印:
4.2.2.3 control 文件的修改(写)
用户空间通过修改(写) control 文件
,控制 dyndbg
调试的启停:
# echo -n "command ::= match-spec* flags-spec" > /sys/kernel/debug/dynamic_debug/control
同 cat
一样,echo
首先触发 ddebug_proc_open()
调用,这个前面已经进行分析;接下来 echo
会触发 ddebug_proc_write()
调用:
#define USER_BUF_PAGE 4096
static ssize_t ddebug_proc_write(struct file *file, const char __user *ubuf,size_t len, loff_t *offp)
{char *tmpbuf;int ret;if (len == 0)return 0;if (len > USER_BUF_PAGE - 1) {pr_warn("expected <%d bytes into control\n", USER_BUF_PAGE);return -E2BIG;}tmpbuf = memdup_user_nul(ubuf, len);if (IS_ERR(tmpbuf))return PTR_ERR(tmpbuf);vpr_info("read %d bytes from userspace\n", (int)len);ret = ddebug_exec_queries(tmpbuf, NULL);kfree(tmpbuf);if (ret < 0)return ret;*offp += len;return len;
}
可以看到,ddebug_proc_write()
调用 了 ddebug_exec_queries()
,这和前面解析内核命令行参数 ddebug_query="QUERY"
、dyndbg="QUERY"
、module.dyndbg="QUERY"
的逻辑一样,都是查询、修改(启停)匹配的 dyndbg 调试的状态
,在次不再赘述。
4.3 外置模块的 dyndbg 参数
4.3.1 外置模块 dyndbg 参数的解析
到此,基本分析了所有 Dynamic debug
功能的相关实现分析,但还缺少对外置模块
的 dyndbg
参数的分析。用户空间通过内核接口 sys_finit_module()
加载模块,在加载模块时,解析 dyndbg
参数以控制 dyndbg 调试语句
的启停:
/* kernel/module.c */SYSCALL_DEFINE3(finit_module, int, fd, const char __user *, uargs, int, flags)
{...return load_module(&info, uargs, flags);
}static int load_module(struct load_info *info, const char __user *uargs,int flags)
{.../* Module is ready to execute: parsing args may do that. *//* 解析内核模块参数(包括 dyndbg 参数) */after_dashes = parse_args(mod->name, mod->args, mod->kp, mod->num_kp,-32768, 32767, mod,unknown_module_param_cb);...
}static int unknown_module_param_cb(char *param, char *val, const char *modname,void *arg)
{.../* Check for magic 'dyndbg' arg */ret = ddebug_dyndbg_module_param_cb(param, val, modname);if (ret != 0)pr_warn("%s: unknown parameter '%s' ignored\n", modname, param);return 0;
}/* lib/dynamic_debug.c */
int ddebug_dyndbg_module_param_cb(char *param, char *val, const char *module)
{vpr_info("module: %s %s=\"%s\"\n", module, param, val);return ddebug_dyndbg_param_cb(param, val, module, -ENOENT);
}
看到了吧,这里和内核启动阶段解析 dyndbg
和 module.dyndbg
参数一样,都调用了 ddebug_dyndbg_param_cb()
,流程与前面一样,这里就不再赘述。
4.3.2 外置模块 dyndbg 参数的提供方式
外置模块 dyndbg
参数的提供方式包含如下 3 种形式,这 3 种形式如果都配置了,后面的配置会覆盖前面的,也即 4.3.2.1 通过模块配置文件
的优先级最低
,4.3.2.3 通过模块参数
的优先级最高
。
4.3.2.1 通过模块配置文件
通过模块配置文件 /etc/modprobe.d/*.conf
进行配置,modprobe
会解析它:
options foo dyndbg=+pt
options foo dyndbg # defaults to +p
4.3.2.2 通过内核命令行参数
foo.dyndbg=" func bar +p; func buz +mp"
前面提到,外置模块 dyndbg
参数虽然在内核启动阶段不生效,但在 modprobe
加载模块时,modprobe
会从内核命令行参数提取,并传给内核接口解析并生效。
4.3.2.3 通过模块参数
modprobe foo dyndbg==pmf # override previous settings
对这最后一种形式,以前面的 dynamic_debug_example.ko
外置模块为例,来示范一下:
root@qemu-ubuntu:~# modprobe dynamic_debug_example dyndbg==pmflt
root@qemu-ubuntu:~# lsmod
Module Size Used by
dynamic_debug_example 16384 0
root@qemu-ubuntu:~# dmesg | tail -n 8
[ 126.608822] dyndbg test module init.
[ 126.626985] [1005] dynamic_debug_example:dyndbg_task_fn:16: dyndbg test message 0
[ 128.688377] [1005] dynamic_debug_example:dyndbg_task_fn:16: dyndbg test message 1
[ 130.761998] [1005] dynamic_debug_example:dyndbg_task_fn:16: dyndbg test message 2
[ 132.861402] [1005] dynamic_debug_example:dyndbg_task_fn:16: dyndbg test message 3
[ 134.937928] [1005] dynamic_debug_example:dyndbg_task_fn:16: dyndbg test message 4
[ 137.016631] [1005] dynamic_debug_example:dyndbg_task_fn:16: dyndbg test message 5
root@qemu-ubuntu:~# modprobe -r dynamic_debug_example
root@qemu-ubuntu:~# lsmod
Module Size Used by
root@qemu-ubuntu:~# dmesg | tail -n 8
[ 189.167825] [1005] dynamic_debug_example:dyndbg_task_fn:16: dyndbg test message 30
[ 191.245793] [1005] dynamic_debug_example:dyndbg_task_fn:16: dyndbg test message 31
[ 193.402753] [1005] dynamic_debug_example:dyndbg_task_fn:16: dyndbg test message 32
[ 195.477486] [1005] dynamic_debug_example:dyndbg_task_fn:16: dyndbg test message 33
[ 197.554432] [1005] dynamic_debug_example:dyndbg_task_fn:16: dyndbg test message 34
[ 199.631214] [1005] dynamic_debug_example:dyndbg_task_fn:16: dyndbg test message 35
[ 201.708996] [1005] dynamic_debug_example:dyndbg_task_fn:16: dyndbg test message 36
[ 203.775266] dyndbg test module exit.
5. Dynamic debug (dyndbg) 的优缺点
Dynamic debug
的优点显而易见,它可以在不重新编译内核的前提下,动态的在运行时启用、关闭调试语句输出,这带来了灵活性;Dynamic debug
的缺点也很明显,由于它使用额外的数据来记录每个 KERN_DEBUG
类型调试语句的状态信息,所以它增大了内核的体积。从前面的 Kconfig 配置,Dynamic debug
功能的作者注释大约增长 2%
,经笔者实测,不是很准确,有时候不到 2%
。同时插入判定语句多少也会带来一些开销。Linux
内核在默认情形下不会开启 Dynamic debug
。
6. 参考资料
[1] Dynamic debug
[2] 浅析 linux kernel dynamic debug
[3] 动态调试
[4] linux dynamic debug