Linux内核调试的方式以及工具集锦
| CSDN | GitHub |
|---|---|
| Linux内核调试的方式以及工具集锦 | AderXCoding/system/tools/you-get |
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可, 转载请注明出处, 谢谢合作
#1 printk 介绍
##1.1 printk 的由来
在内核调试技术之中, 最简单的就是 printk 的使用了, 它的用法和C语言应用程序中的 printf 使用类似, 在应用程序中依靠的是 stdio.h 中的库, 而在 linux 内核中没有这个库, 所以在 linux 内核中, 实现了自己的一套库函数, printk 就是标准的输出函数
##1.2 printk 与 printf 的差异
是什么导致一个运行在内核态而另一个运行用户态? 其实这两个函数几乎是相同的, 出现这种差异是因为 tty_write 函数需要使用 fs 指向的被显示的字符串, 而 fs 是专门用于存放用户态段选择符的, 因此, 在内核态时, 为了配合 tty_write函数, printk 会把 fs 修改为内核态数据段选择符 ds 中的值, 这样才能正确指向内核的数据缓冲区, 当然这个操作会先对 fs 进行压栈保存, 调用 tty_write 完毕后再出栈恢复. 总结说来, printk 与 printf 的差异是由 fs 造成的, 所以差异也是围绕对 fs 的处理。
##1.3 printk 的原理
printk 是在内核中运行的向控制台输出显示的函数, Linux 内核首先在内核空间分配一个静态缓冲区, 作为显示用的空间, 然后调用sprintf, 格式化显示字符串, 最后调用 tty_write 向终端进行信息的显示. 但是根据不同的操作系统也会有不一样的效果, 例如编写一个 hello world 内核模块, 使用这个函数不一定会将内容显示到终端上, 但是一定在内核缓冲区里, 可以使用 dmesg 查看效果.
#2 printk 的内核实现
##2.1 printk 实现
有关 printk 的函数实现都在kernel/printk/printk.c?v=4.10, line 1863
/**
* printk - print a kernel message
* @fmt: format string
*
* This is printk(). It can be called from any context. We want it to work.
*
* We try to grab the console_lock. If we succeed, it's easy - we log the
* output and call the console drivers. If we fail to get the semaphore, we
* place the output into the log buffer and return. The current holder of
* the console_sem will notice the new output in console_unlock(); and will
* send it to the consoles before releasing the lock.
*
* One effect of this deferred printing is that code which calls printk() and
* then changes console_loglevel may break. This is because console_loglevel
* is inspected when the actual printing occurs.
*
* See also:
* printf(3)
*
* See the vsnprintf() documentation for format string extensions over C99.
*/
asmlinkage __visible int printk(const char *fmt, ...)
{
va_list args;
int r;
va_start(args, fmt);
r = vprintk_func(fmt, args);
va_end(args);
return r;
}
EXPORT_SYMBOL(printk);
关于vprintk_func 的定义可以参见 kernel/printk/internal.h, line 34, 如果定义 CONFIG_PRINTK_NMI, 那么它会被定义成PER CPU的, 否则总是被定义成vprintk_default
##2.2 日志级别
printk(日志级别 "消息文本");
printk 会在开头处加上 "<N>" 样式的字符, N 的范围是 0~7, 表示这个信息的级别.
这里的日志级别通俗的说指的是对文本信息的一种输出范围上的指定.
日志级别一共有8个级别
| 日志级别 | 编号 | 描述 |
|---|---|---|
| KERN_EMERG | 0 | 紧急事件消息, 系统崩溃之前提示, 表示系统不可用 |
| KERN_ALERT | 1 | 报告消息, 表示必须立即采取措施 |
| KERN_CRIT | 2 | 临界条件, 通常涉及严重的硬件或软件操作失败 |
| KERN_ERR | 3 | 错误条件, 驱动程序常用KERN_ERR来报告硬件的错误 |
| KERN_WARNING | 4 | 警告条件, 对可能出现问题的情况进行警告 |
| KERN_NOTICE | 5 | 正常但又重要的条件, 用于提醒. 常用于与安全相关的消息 |
| KERN_INFO | 6 | 提示信息, 如驱动程序启动时, 打印硬件信息 |
| KERN_DEBUG | 7 | 调试级别的消息 |
printk 的日志级别定义如下(在include/linux/kern_levels.h中)
// http://lxr.free-electrons.com/source/include/linux/kern_levels.h?v=4.10#L13
#define KERN_SOH "\001" /* ASCII Start Of Header */
#define KERN_SOH_ASCII '\001'
#define KERN_EMERG KERN_SOH "" /* 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 */
#define KERN_DEFAULT KERN_SOH "d" /* the default kernel loglevel */
当然这些日志级别是用来在内核和驱动中使用的, 内核用了一些LOGLEVEL_ 的变量来实现 printk 控制, 这些变量的值与 KERN_ 一一对应, 同时多定义了 LOGLEVEL_SCHED, 参见include/linux/kern_levels.h?v=4.10, line 25
##2.3 日志级别的重要性控制
定义了这么多日志级别, 这些日志源源不断的向内核缓冲区输出信息, 但是不能所有的日志都被显示在终端中, 因此内核设计了一套控制机制来识别日志的重要性, 重要的日志将被立即显示在终端中, 通知用户, 那么看起来不那么重要的日志被输出到后台日志系统, 只要通过dmesg 来查看.
内核通过了四个字值, 它们根据日志记录消息的重要性, 定义将其发送到何处. 关于不同日志级别的更多信息, 请查阅 syslog(2) 联机帮助.
| 重要性级别 | 字段 | 标识 | 描述 |
|---|---|---|---|
| 控制台日志级别 | console_loglevel | DEFAULT_CONSOLE_LOGLEVEL | 优先级高于该值的消息将被打印至控制台 |
| 默认的消息日志级别 | default_message_loglevel | DEFAULT_MESSAGE_LOGLEVEL | 将用该优先级来打印没有优先级的消息 |
| 最低的控制台日志级别 | minimum_console_loglevel | MINIMUM_CONSOLE_LOGLEVEL | 控制台日志级别可被设置的最小值(最高优先级) |
| 默认的控制台日志级别 | default_console_loglevel | DEFAULT_CONSOLE_LOGLEVEL | 控制台日志级别的缺省值 |
内核可把消息打印到当前控制台上, 可以指定控制台为字符模式的终端或打印机等. 默认情况下, "控制台" 就是当前的虚拟终端.
为了更好地控制不同级别的信息显示在控制台上, 内核设置了控制台的日志级别 console_loglevel. printk 日志级别的作用是打印一定级别的消息, 与之类似, 控制台只显示一定级别的消息。
当日志级别小于console_loglevel时,消息才能显示出来. 控制台相应的日志级别定义如下, 参见kernel/printk/printk.c?v=4.10, line 59
// http://lxr.free-electrons.com/source/kernel/printk/printk.c?v=4.10#L59
int console_printk[4] = {
CONSOLE_LOGLEVEL_DEFAULT, /* console_loglevel, 控制台日志级别 */
MESSAGE_LOGLEVEL_DEFAULT, /* default_message_loglevel, 默认日志级别 */
CONSOLE_LOGLEVEL_MIN, /* minimum_console_loglevel, 最低的控制台日志级别 */
CONSOLE_LOGLEVEL_DEFAULT, /* default_console_loglevel, 默认控制台日志级别 */
};
这些日志级别的值并定义在include/linux/printk.h?v=4.10, line 44
//http://lxr.free-electrons.com/source/include/linux/printk.h?v=4.10#L44
#define CONSOLE_EXT_LOG_MAX 8192
/* printk's without a loglevel use this.. */
// 默认日志级别, 可由CONFIG_配置
#define MESSAGE_LOGLEVEL_DEFAULT CONFIG_MESSAGE_LOGLEVEL_DEFAULT
/* We show everything that is MORE important than this.. */
#define CONSOLE_LOGLEVEL_SILENT 0 /* Mum's the word */
// 最低的控制台日志级别, 默认为1
#define CONSOLE_LOGLEVEL_MIN 1 /* Minimum loglevel we let people use */
#define CONSOLE_LOGLEVEL_QUIET 4 /* Shhh ..., when booted with "quiet" */
#define CONSOLE_LOGLEVEL_DEBUG 10 /* issue debug messages */
#define CONSOLE_LOGLEVEL_MOTORMOUTH 15 /* You can't shut this one up */
/*
* Default used to be hard-coded at 7, we're now allowing it to be set from
* kernel config.
*/
// 默认控制台日志级别, 可由CONFIG_配置
#define CONSOLE_LOGLEVEL_DEFAULT CONFIG_CONSOLE_LOGLEVEL_DEFAULT
extern int console_printk[];
// [0] = 控制台日志级别
#define console_loglevel (console_printk[0])
// [1] = 默认消息级别, 未指明的日志均为此级别
#define default_message_loglevel (console_printk[1])
// [2] = 控制台最小的日志级别
#define minimum_console_loglevel (console_printk[2])
// [3] = 默认的控制台日志级别
#define default_console_loglevel (console_printk[3])
没有指定日志级别的 printk 语句默认采用的级别是 DEFAULT_ MESSAGE_LOGLEVEL(这个默认级别一般为 4, 即与KERN_WARNING 在一个级别上), 其定义在linux26/kernel/printk.c中可以找到, 可以通过编译时配置 CONFIG_MESSAGE_LOGLEVEL_DEFAULT 变量来修改
#3 用户空间
##3.1 用户空间的日志信息
如果系统运行了 klogd 或者 syslogd, 则无论console_loglevel为何值, 内核消息都将追加到系统的日志文件中.
使用 ps -aux | grep -E "syslogd|klogd", 查看系统日志服务是否运行
可以查看系统的日志文件, Ubuntu 系统中 /var/log/kern, 或者直接使用 dmesg 查看
如果 klogd 没有运行, 消息不会传递到用户空间, 只能查看 /proc/kmsg.
变量 console_loglevel 的初始值是 DEFAULT_CONSOLE_LOGLEVEL, 可以通过 sys_syslog 系统调用进行修改. 调用 klogd 时可以指定 -c 开关选项来修改这个变量. 如果要修改它的当前值, 必须先杀掉 klogd, 再加 -c 选项重新启动它.
在控制台(这里指的是虚拟终端 Ctrl+Alt+(F1~F6) )加载模块以后, 我们在伪终端(如果对伪终端不是很清楚可以看相关的内容)上运行命令 dmesg 查看日志文件刚才得到的运行记录
可以发现 messages 中的值为 KERN_WARNING 级别之后所要求输出到信息值. 而如果我们在文件 syslog 和 kern-log 中查看系统日志文件,一般情况下可以得到所有的输出信息, 即一般情况下, syslog 和 kern.log 两个文件中记录的内容从编程这个角度来看是基本一致的.
在目录 /var/log/ 下有一下四个文件可以查看日志
syslog, kern.log, messages, debug.
| 日志文件 | 描述 |
|---|---|
| syslog, kern.log | 一般情况下可以得到所有的系统输出值 |
| messages | 得到的是比控制台日志级别低的输出值 |
| debug | 得到的仅仅是 debug 级别的输出值. |
syslog 和 kern.log 一般情况下可以得到所有的系统输出值, 而 messages 得到的是比控制台日志级别低的输出值, debug 得到的仅仅是 debug 级别的输出值.
一般情况下, 优先级高于控制台日志级别的消息将被打印到控制台. 优先级低于控制台日志级别的消息将被打印到messages 日志文件中, 而在伪终端下不打印任何的信息.
关于系统中日志文件的用途, 请参见ubuntu /var/log/下各个日志文件描述及修复无message文件和debug文件, 如果系统中没有message, debug 日志文件也请参见ubuntu /var/log/下各个日志文件描述及修复无message文件和debug文件
我们在进行有关编程的时候, 若使用到 printk 这个函数, 一般查看信息是在 messages 和虚拟终端下进行查看, 而对于 syslog 和 kern.log 下是用来检验所有信息的输出情况.
##3.2 修改日志级别
通过读写 /proc/sys/kernel/printk文件可读取和修改控制台的日志级别.
内容依次分别为, 控制台日志级别 console_loglevel, 默认消息级别 default_message_loglevel, 控制台最小的日志级别 minimum_console_loglevel, 默认的控制台日志级别 default_console_loglevel
可以看到控制台日志级别为4, 对应的是 KERN_WARNING, 也就是说优先级别比KERN_WARNING的内容都会显示messaging, 而KERN_DEBUG的内容会显示在debug中
我们通过一个例子来展示
#include <linux/init.h>
#include <linux/module.h>
#include <linux/printk.h>
MODULE_LICENSE("Dual BSD/GPL");
static void print_loginfo(void)
{
printk(KERN_EMERG "EMERG = %s\n", KERN_EMERG);
printk(KERN_ALERT "ALERT = %s\n", KERN_ALERT);
printk(KERN_CRIT "CRIT = %s\n", KERN_CRIT);
printk(KERN_ERR "ERR = %s\n", KERN_ERR);
printk(KERN_WARNING "WARNING= %s\n", KERN_WARNING);
printk(KERN_NOTICE "NOTICE = %s\n", KERN_NOTICE);
printk(KERN_INFO "INFO = %s\n", KERN_INFO);
printk(KERN_DEBUG "DEBUG = %s\n", KERN_DEBUG);
}
static int book_init(void)
{
printk("Book module init\n");
print_loginfo();
return 0;
}
static void book_exit(void)
{
printk(KERN_ALERT "Book module exit\n");
}
module_init(book_init);
module_exit(book_exit);
查看日志的信息 dmesg 或者 cat /var/log/syslog
查看 message 信息, 或者 cat /var/log/syslog
查看 debug 信息
#4 参考
驱动程序调试方法之printk——printk的原理与直接使用
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可, 转载请注明出处, 谢谢合作.



