• 第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
    ", 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键启动调试器。

    无欲速,无见小利。欲速,则不达;见小利,则大事不成。
  • 相关阅读:
    01-Django 简介
    函数及函数的嵌套等
    循环及循环嵌套
    运算符
    if, elif, else及if嵌套
    变量及变量计算和引用
    Python的3种执行方式
    Jmeter设置默认中文启动
    Vysor
    python基础学习(二)
  • 原文地址:https://www.cnblogs.com/ch122633/p/9183606.html
Copyright © 2020-2023  润新知