Linux Device Driver之调试
linux提供了大量的调试方法和接口,熟悉使用它们,对于内核及驱动开发都会有很大的帮助。
与调试有关的内核配置
CONFIG_DEBUG_KERNEL - 内核调试总开关,使能其他调试配置。
CONFIG_DEBUG_SLAB - 使能内核内存分配的检查,能探测到内存覆盖和遗漏初始化等错误。分配的每一个字节的值都是0xa5,在释放后都是0x6b。
CONFIG_DEBUG_PAGEALLOC - 满的页在被释放时,从内核地址空间去除,能探测到某些内存损坏错误。
CONFIG_DEBUG_SPINLOCK - 内核能捕捉到对未初始化的自旋锁的操作,以及多次解锁同一个锁。
CONFIG_DEBUG_SPINLOCK_SLEEP - 内核检查持有自旋锁时进入睡眠,事实上,一旦调用可能睡眠的函数,内核就会提示。
CONFIG_INIT_DEBUG - 对于初始化完成后,访问__init和__initdata标志的项,内核就会提示。
CONFIG_DEBUG_INFO - 在编译内核时,包含完整的调试信息,对于gdb有用,要是有gdb,还要使能CONFIG_FRAME_POINTER。
CONFIG_MAGIC_SYSRQ - 使能sysrq键。
CONFIG_DEBUG_STACKOVERFLOW - 内核堆栈溢出检查。
CONFIG_DEBUG_STACK_USAGE - 统计内核堆栈的使用情况。
CONFIG_KALLSYMS - 内核符号信息。
CONFIG_IKCONFIG - 完整的内核配置状态。
CONFIG_IKCONFIG_PROC - 完整的内核配置状态。
CONFIG_ACPI_DEBUG - ACPI(Advanced Configuration and Power Interface)相关的调试信息。
CONFIG_DEBUG_DRIVER - 驱动核心的调试信息。
CONFIG_SCSI_CONSTANTS - SCSI错误信息。
CONFIG_INPUT_EVBUG - 输入事件的详细日志。记录所有键盘输入,包括密码。
CONFIG_PROFILING - 内核性能调试,分析内核挂起。
内核打印调试
printk
它类似于应用程序中的printf,但是是分级打印的。一共8个级别。
KERN_EMERG - 紧急消息。
KERN_ALERT - 需要立即关注的消息。
KERN_CRIT - 严重的软硬件失效。
KERN_ERR - 出错信息,报告硬件故障。
KERN_WARNING - 警告信息。
KERN_NOTICE - 正常情况,但是值得注意,与安全相关。
KERN_INFO - 信息,打印硬件信息。
KERN_DEBUG - 调试消息。
KERN_EMERG对应于0,KERN_ALERT对应于1,...,KERN_DEBUG对应于7。
基于记录的级别,内核可能打印消息到当前控制台,可能是文本模式终端,串口,并口打印机。如果优先级小于console_loglevel,消息输出到控制台,如果klogd和syslogd都在运行,消息追加到/var/log/messages,如果klogd没有运行,可以使用dmsg将/proc/kmsg读取到用户空间。klogd对重复消息只打印一遍,之后显示重复的行数。
使用klogd -c可以修改console_loglevel的值,先kill掉老的klogd。
也可以通过修改/proc/sys/kernel/printk来修改控制台记录级别,当我们cat /proc/sys/kernel/printk时,会显示4个数字,依次表示的是:
当前记录级别 没有明确记录级别的消息的缺省级别 最小记录级别 启动时缺省记录级别
echo 8 > /proc/sys/kernel/printk
将输出所有消息。
重定向控制台消息
如下的程序可以改变接受消息的控制台。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/ioctl.h>
int main(int argc, char **argv)
{
char bytes[2] = {11,0}; /* 11 is the TIOCLINUX cmd number */
if (argc==2) bytes[1] = atoi(argv[1]); /* the chosen console */
else {
fprintf(stderr, "%s: need a single arg\n",argv[0]); exit(1);
}
if (ioctl(STDIN_FILENO, TIOCLINUX, bytes)<0) { /* use stdin */
fprintf(stderr,"%s: ioctl(stdin, TIOCLINUX): %s\n",
argv[0], strerror(errno));
exit(1);
}
exit(0);
}
这个程序使用特殊的ioctl命令TIOCLINUX,参数是一个数组,第一个字节是子命令,第二个参数是要操作的控制台。
printk将消息写入__LOG_BUF_LEN(4KB到1MB)字节长的环形缓存,从/proc/kmsg读取是从日志缓存中消费数据,而syslog系统调用能够选择地返回日志数据同时保留它给其他进程。klogd是直接读取/proc/kmsg,而dmsg可用来查看缓存的内容,并不会冲掉它。
如果环形缓存填满,printk绕回并从缓存的开头增加新数据,覆盖最老的数据。printk可以在任何地方调用,不限制打印多少数据,是经典的调试方法。
如果klogd在运行,它获取内核消息并分发给syslogd,syslogd检查/etc/syslog.conf来如何处理它们。如果klogd没有运行,数据保留在环形缓存直到有人读取或者被覆盖。
关闭和打开调试
如下代码展示如何定义调试宏
# ifdef __KERNEL__
/* 内核空间 */
# define PDEBUG(fmt, args...) printk( KERN_DEBUG "xxxxx: " fmt, ## args)
# else
/* 用户空间 */
# define PDEBUG(fmt, args...) fprintf(stderr, fmt, ## args)
# endif
限制打印的速率
if (printk_ratelimit())
printk(KERN_NOTICE "The printer is still on fire\n");
通过修改如下2个文件,可以改变速率限制
重新使能消息前等待的秒数
/proc/sys/kernel/printk_ratelimit
限速前可接收的消息数
/proc/sys/kernel/printk_ratelimit_burst
重新使能消息前等待的秒数
/proc/sys/kernel/printk_ratelimit
限速前可接收的消息数
/proc/sys/kernel/printk_ratelimit_burst
打印设备号
int print_dev_t(char *buffer, dev_t dev);
char *format_dev_t(char *buffer, dev_t dev);
用查询来调试
proc vs sysfs
proc vs seq_file
当一个进程读取驱动的proc文件时,内核分配一个PAGE_SIZE字节的内存页,用于驱动写入返回给用户空间的数据。
int (*read_proc)(char *page, char **start, off_t offset, int count, int *eof, void *data);
创建proc文件
包含头文件
#include <linux/proc_fs.h>;
使用如下函数创建
struct proc_dir_entry *create_proc_read_entry(const char *name,mode_t mode, struct proc_dir_entry *base, read_proc_t *read_proc, void *data);
使用如下函数去除
void remove_proc_entry(const char *name, struct proc_dir_entry *parent);
创建seq_file
包含头文件
#include <linux/seq_file.h>;
需要实现如下几个方法:
void *start(struct seq_file *sfile, loff_t *pos);
void *next(struct seq_file *sfile, void *v, loff_t *pos);
void stop(struct seq_file *sfile, void *v);
int show(struct seq_file *sfile, void *v);
int seq_printf(struct seq_file *sfile, const char *fmt, ...);
int seq_putc(struct seq_file *sfile, char c);
int seq_puts(struct seq_file *sfile, const char *s);
int seq_escape(struct seq_file *m, const char *s, const char *esc);
seq_printf对应于C库中的printf,seq_putc对应于putc,seq_puts对应于puts;seq_ecape类似于seq_puts,只是以八进制输出。
做一个结构体
static struct seq_operations xx_seq_ops = {
.start = xx_seq_start,
.next = xx_seq_next,
.stop = xx_seq_stop,
.show = xx_seq_show
};
这里需要实现xx_seq_start,xx_seq_next,xx_seq_stop,xx_seq_show。
然后实现如下函数来把file联系到seq_file操作。
static int xx_proc_open(struct inode *inode, struct file *file)
{
return seq_open(file, &xx_seq_ops);
}
然后建立file_operations结构体:
static struct file_operations xx_proc_ops = {
.owner = THIS_MODULE,
.open = xx_proc_open,
.read = seq_read,
.llseek = seq_lseek,
.release = seq_release
};
最后调用如下函数:
struct proc_dir_entry *create_proc_entry(const char *name,mode_t mode,struct proc_dir_entry *parent);
例如:
entry = create_proc_entry("xxseq", 0, NULL);
if (entry)
entry->proc_fops = &xx_proc_ops;
ioctl方法
需要在驱动中实现一些调试和查看信息的命令,以及一些结构体用于返回数据。需要写一个用户空间的应用程序,通过调用ioctl方法来获取、显示驱动信息。
ioctl实现很简单,不需要实现文件操作的那些方法;对数据大小也没有要求,不需要划分为一页;而且看不到api和驱动代码的人不会知道ioctl命令的存在。缺点是驱动功能代码里有了调试用的代码,驱动的体积变大。
使用strace
strace显示用户空间程序发出的系统调用以及参数和返回值。
例如:
strace ls /dev > /dev/null
最有用的命令行参数如下:
-t 来显示每个调用执行的时间
-T 来显示调用中花费的时间
-e 来限制被跟踪调用的类型
-o 来重定向输出到一个文件
分析oops
当使用NULL或者其他不正确指针时,会抛出oops消息。
当解引用一个无效的指针时,分页机制无法映射指针到一个物理地址,处理器发出一个页错误给操作系统。 如果地址无效,,内核无法“页入”缺失的地址。
oops 显示了出错时的处理器状态,,包括CPU 寄存器内容。
有2个很有用的信息:
EIP和Stack/Call Trace
在x86体系下,0xC0000000以下是用户空间,以上是内核空间。
系统挂起
当单处理器禁止了抢占时,如果代码进入了一个无限循环,那么内核停止调度,系统就挂起了。
在多处理器和单处理器使能了抢占时,内核还是有可能在其他处理器上调度或者抢占当前的进程。
插入schedule可以阻止无限循环。
当驱动持有自旋锁时,不能调用schedule。
sysrq
alt + sysrq +
r - 关闭键盘原始模式;
k - SAK,干掉当前控制台所有运行的进程;
s - 对所有磁盘紧急同步;
u - 重新以只读模式加载所有磁盘;
b - 重启;
p - 打印处理器消息;
t - 打印当前任务列表;
m - 打印内存信息。
运行时关闭sysrq功能:
echo 0 > /proc/sys/kernel/sysrq
文件/proc/sysrq-trigger是一个可写的可触发sysrq请求的好地方,例如:
echo 'p' > /proc/sysrq-trigger
通过
cat /var/log/message
可以查看如上命令的输出结果。
documentation/basic_profiling.txt讲述了如何使用readprofile和oprofile。
调试器
在内核上使用一个交互式的调试器是很困难的,并且像断点、单步都很难使用。因此万不得已才考虑使用。并且编译内核时需要使能CONFIG_DEBUG_INFO,这样的话,镜像就会很大。
使用gdb
gdb /usr/src/linux/vmlinux /proc/kcore
使用gdb不断打印某个变化的数据项的值时,由于gdb是以多个几KB块的方式读取核心的,并且只会缓存已读取的块,所以需要使用命令gdb core-file /proc/kcore来刷新缓存。
模块是独立于vmlinux的可执行镜像,那么要调试模块也比较困难,不过有3个节跟调试相关:
.txt - 模块的可执行代码
.bss - 编译时不初始化的变量
.data - 编译时需要初始化的变量
当每个模块被加载后,在/sys/module/xxx/sections/.text给出了节的基地址,我们需要使用的命令是add-symbol-file,这个命令需要模块名以及节的基地址作为输入,也可以添加另外一些感兴趣的输入:
例如:
gdb add-symbol-file xxx.ko 0xdddd1000 -s .bss 0xdddd3000 -s .data 0xdddd2000
gdb print *(addr),指定addr为一函数指针地址,就会打印出对应函数文件及行号
使用kdb
要使用kdb,需要到
http://oss.sgi.com/projects/kdb/下载相对应的patch:(ftp地址
ftp://oss.sgi.com/www/projects/kdb/download/)
patch -p1 < kdb-xxx
make *config
使能CONFIG_KDB,重现编译内核。
在控制台按下键盘的Pause或者Break键启动kdb,kdb在发生oops或者命中断点时也会启动。
kdb命令:
bp xxx - 设置断点
go - 开始执行
bt - 显示调用栈
mds - 显示数据
mm - 修改数据
使用kgdb
patch -p1 < kdb-xxx
make *config
使能CONFIG_KDB,重现编译内核。
在控制台按下键盘的Pause或者Break键启动kdb,kdb在发生oops或者命中断点时也会启动。
kdb命令:
bp xxx - 设置断点
go - 开始执行
bt - 显示调用栈
mds - 显示数据
mm - 修改数据
使用kgdb
kgdb使用串口或者网络,连接发调试命令的机器和运行调试内核的机器。
使用UML - 用户模式的Linux
将内核运行在由系统调用实现的虚拟机上,可以调试和硬件无关的代码。可以运行各种调试器,加快内核开发。可以参考
http://user-mode-linux.sourceforge.net/。
使用Linux Trace Toolkit
可以用来追踪内核事件,调试性能问题。可以参考
http://www.opersys.com/LTT/。
使用Dynamic Probes
IBM为IA-32开发的Linux调试工具。可以在内核空间、用户空间插入探针,用来返回信息给用户空间或者改变寄存器的值。