第4章 调试技术

一、内核调试支持

我们列出用来开发的内核应当激活的配置选项。

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\n", argv[0]);
        exit(1);
    }
    if(ioctl(STDIN_FILENO, TIOCLINUX, bytes) < 0) { /*use stdin */
        fprintf(stderr, "%d: ioctl(stdin, TIOCLINUX): %s\n", 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\n");

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,"\nDevice %i: qset %i, q %i, sz %li\n", 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\n", 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\n", 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, "\nDevice %i: qset %i, sz %li\n",
        (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\n", 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\n", 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键启动调试器。

posted @ 2018-06-14 16:20  习惯就好233  阅读(438)  评论(0编辑  收藏  举报