一、内核调试支持
我们列出用来开发的内核应当激活的配置选项。
CONFIG_DEBUG_KERNEL
这个选项只是使其他调试选项可用; 它应当打开, 但是它自己不激活任何的特性.
CONFIG_DEBUG_SLAB
CONFIG_DEBUG_PAGEALLOC
满的页在释放时被从内核地址空间去除. 这个选项会显著拖慢系统, 但是它也能快速指出某些类型的内存损坏错误.
CONFIG_DEBUG_SPINLOCK
激活这个选项, 内核捕捉对未初始化的自旋锁的操作, 以及各种其他的错误( 例如2 次解锁同一个锁 ).
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
这些选项能帮助跟踪内核堆栈溢出. 堆栈溢出的确证是一个 oops 输出, 但是没有任何形式的合理的回溯. 第一个选项给内核增加了明确的溢出检查; 第 2 个使得内核监测堆栈使用并作一些统计, 这些统计可以用魔术 SysRq 键得到.
CONFIG_KALLSYMS
这个选项(在"Generl setup/Standard features"下)使得内核符号信息建在内核中;缺省是激活的. 符号选项用在调试上下文中; 没有它, 一个 oops 列表只能以 16进制格式给你一个内核回溯, 这不是很有用.
CONFIG_IKCONFIG
CONFIG_IKCONFIG_PROC
CONFIG_ACPI_DEBUG
CONFIG_DEBUG_DRIVER
CONFIG_SCSI_CONSTANTS
CONFIG_INPUT_EVBUG
CONFIG_PROFILING
二、用打印调试
2.1 printk
printk允许你根据消息的严重程度会其分类,通过附加不同的记录级别或者优先级的消息上。
头文件<linux/kernel.h>
KERN_EMERG
用于紧急消息, 常常是那些崩溃前的消息.
KERN_ALERT
需要立刻动作的情形.
KERN_CRIT
严重情况, 常常与严重的硬件或者软件失效有关
KERN_ERR
用来报告错误情况; 设备驱动常常使用 KERN_ERR 来报告硬件故障
KERN_WARNING
有问题的情况的警告, 这些情况自己不会引起系统的严重问题
KERN_NOTICE
正常情况,但是仍然值得注意。在这个级别一些安全相关的情况会报告.
KERN_INFO
信息型消息,在这个级别,很多驱动在启动时打印它们发现的硬件信息。
KERN_DEBUG
用作调用消息
内核中的消息优先级在printk语句缺省是DEFAULT_MESSAGE_LOGLEVEL,在kernel/printk.c里指定作为一个整数。
如果klogd和syslogd都在系统中运行,内核消息被追加到/var/log/messages,如果klogd没有运行,只能读/proc/kmsg(用dmsg)
klogd不会保留同样的行,它只保留第一个这样的行。后面是重复的行数
通过使用sys_syslog系统调用可以修改DEFAULT_CONSOLE_LOGLEVEL,来修改console_loglevel的初始化值。通过klogd -c也可以修改
特定值。不过修改前必须先杀掉klogd,然后用-c重启它。
也可以简单的用命令行改
echo 8 > /proc/sys/kernel/printk
2.2 重定向控制台消息
#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]); else { fprintf(stderr, "%s: need a signle arg ", argv[0]); exit(1); } if(ioctl(STDIN_FILENO, TIOCLINUX, bytes) < 0) { /*use stdin */ fprintf(stderr, "%d: ioctl(stdin, TIOCLINUX): %s ", argv[0], strerror(errno)); exit(1); } exit(0); }
2.3 消息如何记录
printk将消息写入__LOG_BUF_LEN字节长的环形缓存,它是从4KB到1MB的值,当config内核时可以选择。
后面的完全看不懂,大概就是syslogd和klogd的区别
2.4 打开或关闭消息
一种编码printk调用的方法,可以单独或全局的打开时或关闭他们;这个技术依靠定义一个宏,在你想使用它时就转变成printk调用。
每个printk语句可以打开或关闭,通过取出或添加单个字符到宏定义的名子。
所有消息可以马上关闭,通过在编译前改变CFLAGS变量的值。
同一个print语句可以在内核代码和用户级代码中使用,因此对于格外的消息,驱动和测试程序能以同样的方式被管理。
来自头文件scull.h:
/* * Macros to help debugging */ #undef PDEBUG /* undef it, just in case */ #ifdef SCULL_DEBUG # ifdef __KERNEL__ /* This one if debugging is on, and kernel space */ # define PDEBUG(fmt, args...) printk( KERN_DEBUG "scull: " fmt, ## args) # else /* This one for user space */ # define PDEBUG(fmt, args...) fprintf(stderr, fmt, ## args) # endif #else # define PDEBUG(fmt, args...) /* not debugging: nothing */ #endif
可以添加下面行到makefile里,以进一步简化过程。
# Comment/uncomment the following line to disable/enable debugging DEBUG = y # Add your debugging flag (or not) to CFLAGS ifeq ($(DEBUG), y) DEBFLAGS = -0 -g -DSCULL_DEBUG # "-0" is needed to expand inlines else DEBFLAGS = -02 endif CFLAGS+= $(DEBFLAGS)
2.5 速率限制
如果不小心用printk产生了上千条消息。过慢的控制带可能使得没有中断来控制,最好的做法是设置一个标志说“我已经抱怨过这个了”
int printk_ratelimit(void);
如果这个函数狯非零值,继续打印你的消息。否则跳过它。
if(printk_ratelimit()) printk(KERN_NOTICE "The printer is still on file ");
printk_ratelimit的行为可以通过修改/proc/sys/kern/printk_ratelimit和/proc/sys/kernel/printk_ratelimit_burst来定制
2.6 打印设备编号
打印主次编号不是特别难,但是为了一致性,内核提供了一些使用的宏定义在<linux/kdev_t.h>中
int print_dev_t(char *buffer, dev_t dev); char *format_dev_t(char *buffer, dev_t dev);
三、用查询来调试
/proc文件系统是一个特殊的软件创建的文件系统,内核用来输出消息到外界。
/proc下的每个文件都绑到一个内核函数上,当文件被读的时候即时产生内容。例如,/proc/modudles,常常返回当前已加载的模块列表。
3.1 使用/proc文件系统
所有使用/proc的模块应当包含<linux/proc_fs.h>来定义正确的函数。
要创建一个只读的/proc文件,驱动必须实现一个在文件读时产生数据的函数。当某个进程读文件时(read系统调用),这个请求通过函数到达你的模块。
进程读/proc文件时,内核会分配一页内存(PAGE_SIZE字节),驱动可以写入数据返回给用户空间。缓存区传递给你的函数叫read_proc:
int (*read_proc)(char *page, char **start, off_t offset, int count, int *eof, void *data); page:指针是你写你数据的缓存区 start:有关的数据写在哪里 offset:和read类似 count:和read类似 eof:指向一个整数,必须由驱动设置来指示它不再有数据返回 data:驱动特定的数据指针 返回实际摆放于page缓存区的数据字节数,类似read eof返回简单的标志,start略复杂,它实现大/proc文件(超过一页)
不知道这个start什么用法,
使用例子:
int scull_read_procmem(char *buf, char **start, off_t offset, int count, int *eof, void *data) { int i, j, len = 0; int limit = count - 80; /* Don't print more than this */ for (i = 0; i < scull_nr_devs && len <= limit; i++) { struct scull_dev *d = &scull_devices[i]; struct scull_qset *qs = d->data; if (down_interruptible(&d->sem)) return -ERESTARTSYS; len += sprintf(buf+len," Device %i: qset %i, q %i, sz %li ", i , d->qset, d->quantum, d->size); for (; qs && len <= limit; qs = qs->next) { /* scan the list */ len += sprintf(buf + len, " item at %p, qset at %p ", qs, qs->data); if (qs->data && !qs->next) /* dump only the last item */ for (j = 0; j < d->qset; j++) { if (qs->data[j]) len += sprintf(buf + len, " % 4i: %8p ", j, qs->data[j]); } } up(&scull_devices[i].sem); } *eof = 1; return len; }
3.1 创建你的proc文件
一旦你有一个定义好的read_proc函数,它应当连接到/proc层次中的一个入口项。使用一个create_proc_read_entry调用:
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); name:要创建的文件名子 mod:是文件保护掩码 base:要创建文件的目录(如果是NULL,就在/proc下创建) data:被内核忽略,但传递给read_proc
在scull中这样调用它:
create_proc_read_entry("scullmem", 0 /* default mode */ NULL /* parent dir */, scull_read_procmem, NULL /* client data */); 我们创建了一个名为scullmem的文件,直接在/proc下,带有缺省的,全局可读的保护。
相对于create_proce_read_entry应当有卸载函数:
void remove_proc_entry( const char *name, struct proc_dir_entry *parent ); name:要卸载的文件名子 parent:parent目录位置(NULL,就在/proc下创建)
scull中的调用:
remove_proc_entry("scullmem", NULL /* parent dir */);
3.4 seq_file接口
/proc方法因为当输出数量变大时的错误实现变的声名狼藉。
作为一种清理/proc代码以及使用内核开发者获得轻松些的方法,添加了seq_file接口。
第一步,包含<linux/seq_file.h>,接着必须创建4个iterator方法。称为start, next, stop 和 show
start方法一直是首先调用:
void *start(struct seq_file *sfile, loff_t *pos); sfile:参数可以几乎是一直被忽略 pos:整型位置值,从哪里读
scull使用start方法:
static void *scull_seq_start(struct seq_file *s, loff_t *pos) { if(*pos >= scull_nr_devs) return NULL; /* No more to read */ return scull_devices + *pos; }
返回值如果是非NULL,则是一个可以被iterator实现使用的私有值
next方法应当移动iterator到下一个位置:
void *next(struct seq_file *sfile, void *v, loff_t *pos); v:对start或者next调用返回的iterator pos:文件的当前位置
scull所做的:
static void *scull_seq_next(struct seq_file *s, void *v, loff_t *pos) { (*pos)++; if(*pos >= scull_nr_devs) return NULL; return scull_devices + *pos; }
当内核处理完iterator,调用stop清理:
void stop(struct seq_file *sfile, void *v);
在这些调用中,内核调用show方法来真正输出有用的东西给用户空间,原型是:
int show(struct seq_file *sfile, void *v); v:指示的项的输出,但是有一套特殊的作用seq_file输出的函数: int seq_printf(struct seq_file *sfile, const char *fmt, ...); 类似于printf int seq_putc(struct seq_file *sfile, char c); int seq_puts(struct seq_file *sfile, const char *s); 等价于用户空间的putc和puts int seq_escape(struct seq_file *m, const char *s, const char *esc); 这个函数是seq_puts的对等体 除了s中的任何也在esc中出现的字符以八进制格式打印。 int seq_path(struct seq_file *sfile, struct vfsmount *m, struct dentry *dentry, char *esc);
在scull使用的show方法是:
static int scull_seq_show(strcut seq_file *s, void *v) { struct scull_dev *dev = (struct scull_dev *)v; struct sculL_qset *d; int i; if(down_interruptible(&dev->sem)) return -ERESTARTSYS; seq_printf(s, " Device %i: qset %i, sz %li ", (int)(dev-scull_devices), dev->qset, dev->quantum, dev->size); for(d = dev->data; d; d = d->next) { /* scanf the list */ seq_printf(s, " item at %p, qset at %p ", d, d->data); if(d->data && !d->next) /* dump only the last item */ for(i=0;i<dev->qset;i++) { if(d->data[i]) seq_printf(s, " %4i: %8p ", i, d->data[i]); } } up(&dev->sem); return 0; }
scull必须包装这些操作集合,填充到seq_operations结构:
static struct seq_operations scull_seq_ops = { .start = scull_seq_start, .next = scull_seq_next, .stop = scull_seq_stop, .show = scull_seq_show };
在使用seq_file时,最好在稍微低级别上连接到/proc,意味着创建一个file_operations结构
static int scull_proc_open(struct inode *inode, struct file *file) { return seq_open(file, &scull_seq_ops); } static struct file_operations scull_proc_ops = { .owner = THIS_MODULE, .open = scull_proc_open, .read = seq_read, .llseek = seq_lseek, .release = seq_release };
这里使用我们自己的open方法,但是使用与装好的方法seq_read,seq_lseek,seq_release。
最后步骤是创建/proc中的实际文件:
entry = create_proc_entry("scullseq", 0, NULL); if(entry) entry->proc_fops = &scull_proc_ops;
不使用create_proc_read_entry,而调用低层的create_proc_entry,我们有这个原型:
struct proc_dir_entry *create_proc_entry(const char *name, mode_t mode, struct proc_dir_entry *parent); name:文件名子 mode:位置 parent:父目录
3.5 ioctl方法
ioctl相比于上面的,速度快,封装好,不要求一页。另外接口什么的不为人所知,都包括在内核中了,哪怕出了问题。
四、使用观察来调试
有几个方法来监视用户空间程序运行:
- 运行一个调试器来单步过它的函数
- 增加打印语句
- 在strace下运行程序。
strace命令是一个有力工具,显示所有用户空间程序发出的系统调用。它不仅显示调用,还以符号形式显示调用的参数和返回值。当一个系统调用失败,错误的符号值(ENOMEM)和对应的字串(Out of memory)都显示。
strace命令行选项,其中最有用的是:
-t 来显示每个调用执行的时间
-T 来显示调用中花费的时间
-e 来限制被跟踪调用的类型
-o 重定向输出到一个文件
strace从内核自身获取信息,这意味着可以跟踪一个程序,不管他是否带有调试支持编译(gcc -g)
4.1 调试系统故障
即便你已使用了所有的监视和调试技术,有时故障还留在驱动里,当驱动执行时系统出错,当发生这个时,能够收集尽可能多的信息来解决问题是重要的。
oops消息
大部分bug以解引用NULL指针或者使用其他不正确指针来表现自己,此类bug通常的输出一个oops消息。
4.2 系统挂起
什么魔术组合键,好像没什么用。
4.3 调试器和相关工具
使用gdb对于看系统内部非常有用,这个级别精通调试器的的使用要求对gdb命令有信心。
需要理解目标平台的汇编代码,以及对应与那吗和优化的汇编码的能力。核心文件时内核核心映象,/proc/kcore
gdb /usr/src/linux/vmlinux /proc/kcore
如果要能用gdb调试内核,必须设置CONFIG_DEBUG_INFO来编译内核,结果会产生一个很大的内核镜像文件。
linux中ELF文件格式被分成几个节,一个典型的模块可能包含一打或更多节,但是有3个典型的与一次调试会话相关:
.text 包含可执行代码
.bss
.data 这两个节持有模块的变量,在编译时不初始化的任何变量在.bss中,而那些要初始化的在.data里。
gdb中可以用add-symble-flile
kdb内核调试器
kdb是一个非官方补丁,一旦运行一个使能的kdb内核,有几个方法进入调试器,在控制台上按下Pause或Break键启动调试器。