linux驱动的调试技术读书笔记(上)

内核中的调试支持

2.内核中的几个选项

CONFIG_DEBUG_KERNEL

这个选项仅仅是其他调试选项可用。我们应该打开这个选项,但是它本身不会打开所有的调试功能。

CONFIG_DEBUG_SLAB

这是一个十分重要的选项,打开了内核内存分配函数中的多个类型检查;打开检查后,就可检查许多内存溢出
以及忘记初始化的错误。在将已经分配内存返回给调用者之前,内核将他的每个字节设置为0xa5,而在释放之后
将他设置为0x6b。如果读者在自己的驱动程序中,或者在oops信息中查看到毒剂字符串,则可以轻松判断位置所在。
在打开这个调试选项之后,内核会为对象分配的前面和后面加入防护值,这样防护值发生变化的时候,内核就会知道
哪些代码超出了内存的正常访问范围

CONFIG_DEBUG_PAGEALLOC

在释放的时候,全部内存页将从内核地址空间移出。这个选项会大大降低运行效率,但可以定位内存损坏的位置。

CONFIG_DEBUG_SPINLOCK

打开这个选项,内核将会捕获对未初始化自旋锁,也会捕获两次解开同一个锁的错误。

CONFIG_DEBUG_SPINLOCK_SLEEP

这个选项将拥有自旋锁时的休眠企图。

CONFIG_INIT_DEBUG
标记为__init的符号将会在系统初始化或者模块卸载之后被丢弃。这个选项可以用来检测初始化完成之后
对用于初始化内存空间的访问企图

CONFIG_DEBUG_INFO

这个选项让内核的构造包含完整的调试信息。如果利用gdb调试,应该打开CONFIG_FRAME_POINTER选项。

CONFIG_INIT_SYSRQ

打开sysRq魔法按键。

CONFIG_DEBUG_STACKOVERFLOW
CONFIG_DEBUG_STACK_USAGE

这个选项用来帮助定位内核栈溢出问题。

CONFIG_KALLSYMS

这个选项出现在一般设置、标准功能菜单中;这个选项默认是打开的。这个符号信息用来调试上下文;没有
这个符号,oops清单只给出十六进制的内核反向跟踪信息,这通常没有多少用处。

CONFIG_IKCONFIG
CONFIG_IKCONFIG_PROC

这些选项出现在一般设置的菜单中,会让内核配置状态包含到内核中,并且通过/proc访问。大多数内核开发
者清楚的知道自己的使用的配置,因此不是很需要这两个选项,如果内核是被其他人构建的那么我想你是需要的。

CONFIG_ACPI_DEBUG

这个选项出现在电源管理、ACPI中。如果怀疑自己遇到的问题和ACPI有关,打开这个选项。

CONFIG_DEBUG_DRIVER
在设备驱动菜单中,他会打开驱动程序核心调试信息。

CONFIG_SCSI_CONSTANTS

打开详细的SCSI错误信息,如果写SCSI驱动程序,可以打开这个选项。

CONFIG_INPUT_EVBUG

打开输入设备的详细输入事件记录,如果读者要针对输入设备编写驱动程序,可以使用这个选项。

CONFIG_PROFILING

剖析支持的菜单中找到,通常用来调试性能。

2.通过打印调试

linux内核可以通过printk来打印。

printk,可以打印不同级别的日志

printk(KERN_DEBUG "Here I am:%s:%i\n", __FILE__, __LINE__);
printk(KERN_CRIT "Here I am:%s:%i\n", __FILE__, __LINE__);

linux 内核提供了8中级别的错误提醒:

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
中被指定为一个整数。在2.6.10内核中,DEFAULT_MESSAGE_LOGLEVEL就是KERN_WARING,但是以前的版本
也取过不同的值。

当优先级小于console_loglevel这个整数变量的值,消息才能显示出来,而且每次只会输出一行。
如果系统运行了kogd和syslogd,则无论console_loglevel为什么值,内核都会把消息追加到/var/log/message
中。如果klogd没有运转,这些消息都会被追加到/var/log/message中。如果klogd没有运转,这些消息
不会传递到用户空间,这种情况下,只能查看/proc/kmsg文件。如果使用使用klogd,则应该了解
他不会保存连续的相同信息,只会保存连续相同的第一行,并在最后标记相同的重复数。

变量console_loglevel 的初始值是DEFAULT_CONSOLE_LOGLEVEL,而且还可以通过sys_syslog系统调用进行修改。调用klogd
的时候可以指定-c的开关来修改这个变量。修改当前值之后必须要杀死klogd,如果设置为了8,那么包括调试
信息的所有消息都会被显示出来。

我们也可以通过对文件/proc/sys/kernel/printk的访问来读取和修改控制台的日志级别。这个文件包含了
4个整数值,分别是当前的日志级别、未明确日志级别时候的默认消息级别、最小允许的日志级别以及引导时候的默认日志级别。

测试结果:

zhanglei@zhanglei-Latitude-5400:~/ourc/test$ cat /proc/sys/kernel/printk
4	4	1	7
zhanglei@zhanglei-Latitude-5400:~/ourc/test$ 

3.重定向控制台消息

对于控制台日志策略,linux允许有某些灵活性:内核可以将消息发送到一个指定的虚拟控制台。默认情况下控制台就是当前的
虚拟终端。可以在任何一个控制台上调用ioctl来指定接收内核消息的其他控制台。这个程序必须由超级用户运行,在mics-proc
可以找到他。

下面是这个程序的完整清单。调用这个程序的时候,请附加一个控制台编号。

setconsole使用了特殊ioctl命令:TIOCLINUX,这个命令可以完成一些特殊的linux功能。
使用TIOCLINUX的时候,需要传给它一个指向字节数组的指针参数。数组的第一个字节指定所有请求子命令的编号,随后字节
所具有的功能由这个子命令来决定。在setconsole的子命令是11,后面的那个字节用来标识虚拟控制台。关于TIOCLINUX
的完整描述在内核源码中drivers/char/tty_io.c文件中得到标识。

#include <iostream>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <cstring>
#include <sys/ioctl.h>

int main (int argc, char** argv)
{
    char bytes[2] = {11, 0};
    if (argc == 2) {
        bytes[1] = atoi(argv[1]);
    } else {
        fprintf(stderr, "%s: need a single arg\n", argv[0]);
        exit(1);
    }

    if (ioctl(STDIN_FILENO, TIOCLINUX, bytes) < 0) {
        fprintf(stderr, "%s: ioctl(stdin, TIOCLINUX):%s\n", argv[0], strerror(errno));
        exit(1);
    }
    return 0;
}

没搞懂这个函数是做什么用的

4.消息是如何被记录的

printk 函数将消息写到一个长度为__LOG_BUF_LEN字节的循环缓冲区中。然后,这个函数就会唤醒任何正在等待
消息的进程,也就是正在等待syslogd消息的进程,或者正在读取/proc/kmsg的进程。/proc/kmsg是一个环形的缓冲区,
读取的内容不会继续保存。一般而言,/proc要容易一些,这也是klogd的默认方法。dmesg可在不刷新缓冲区的情况下获取
缓冲区的内容

读者会发现/proc/kmsg文件很像一个FIFO,如果已经有进程在读取,就不能用这种方法进行读取,会产生竞态关系。

klogd指定-f选项,将klogd保存到一个文件中,但是需要kill这个klogd。

5.开启或者关闭消息

可以通过在宏的名字中删减或者增加一个字母来启用或者禁用每一条打印语句。

#undef PDEBUG
#ifdef SCULL_DEBUG
# ifdef __KERNEL__
#  define PDEBUG(fmt, args...) printk(KERN_DEBUG "scull:" fmt, ##args)
# else
#  define PDEBUG(fmt, args...) fprintf(stderr, fmt, ## args)
# endif
#else
#  define PDEBUG(fmt, args...)
#endif

#undef PDEBUGG
#define PDEBUGG(fmt, args...)

6.限制速度

printk 可能产生上千条消息打印到控制台,会让系统文件溢出。过高的消息打印会让系统变慢,甚至没有办法响应。
当我们系统运行结束的时候,不明智的进程会让系统设备不停重试。

所以我们可以使用

int printk_ratelimit(void);

具体事例:

if (printk_ratelimit())
    printk(KERN_NOTICE "The printer is still on file");

printk_ratelimit通过跟踪发送到控制台的消息数量工作。如果输出的速度超过一个阀值,printk_ratelimit将返回
为0,避免重复发送消息。

我们可以通过修改/proc/sys/kernel/printk_ratelimit以及/proc/sys/kernel/printk_ratelimit_burst
来定制printk_ratelimit的行为。

7.打印设备编号

当从一个驱动程序打印消息的时候,我们希望会打印和硬件相关联的设备号。内核提供了一些辅助函数,函数放到

<linux/kdev_t.h>

int print_dev_t(char* buffer, dev_t dev);
char *format_dev_t(char *buffer, dev_t dev);

print_dev_t 是返回打印的字符串数,而format_dev_t返回的是缓冲区,这样可以直接调用printk来使用。
因为在未来内核版本中,使用64位的设备号十分明显,因此这个缓冲区的大小是20字节长。

代码demo

8.通过查询调试

驱动人员可以使用如下方法进行查询:在文件系统/proc中创建文件、使用驱动程序ioctl控制,以及通过sysfs导出属性。

/proc文件系统

/proc是一种特殊的,由软连接创建的文件系统,内核使用他导出信息,/proc每个文件都绑定了一个内核函数,用户读取其中
文件的时候,这个函数就动态的生成文件内容。例如/proc/modules列出的就是当前模块。

我们不建议在/proc下面添加过多文件,但是我们可以使用sysfs来导出信息。

在/proc中实现的文件,可以通过<linux/proc_fs.h>,并且通过这个函数来定义正确的函数。

某个进程在我们读取/proc文件的时候,内核会分配一个内存页,驱动程序可以将数据通过这个内存页返回到用户空间。
这个函数名字叫read_proc方法。

#include <linux/proc_fs.h>
int (*read_proc)(char* page, char **start, off_t offset, int count, int *eof, void *data);

创建自己的/proc

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是要创建的文件名;mode是这个文件的保护掩码;base指定这个文件所在的目录;read_proc是实现函数,内核会忽略data参数,但是
会将参数传递到read_proc。下面是scull调用函数创建/proc文件的代码。

create_proc_read_entry("scullmem", 0, NULL, scull_read_procmem, NULL);

这个函数已经被废弃了,新的proc_create_data可以继续使用,而且还有惊喜,哈哈哈哈,有了操作的结构体


struct proc_fs_info {
    
    
        struct pid_namespace *pid_ns;
        struct dentry *proc_self;        /* For /proc/self */
        struct dentry *proc_thread_self; /* For /proc/thread-self */
        kgid_t pid_gid;
        enum proc_hidepid hide_pid;
        enum proc_pidonly pidonly;
};
#define proc_create_data(name, mode, parent, proc_ops, data) ({NULL;})

在卸载模块的时候,/proc中的入口项也应该被删除

remove_proc_entry("scullmem", NULL);

最重要的问题和/proc项的删除有关。删除调用可能在文件正在被使用的时候发生,因为/proc入口项不存在关联的所有者,
因为/proc入口项不存在关联的所有者,因此对这些文件的使用并不会作用到模块的引用计数上,执行
sleep 100 < /proc/myfile

今天时间有点晚了,先到这里,下周会跟进/proc

猜你喜欢

转载自blog.csdn.net/qq_32783703/article/details/112759771
今日推荐