一、内核中的调试支持
1、内核开发的配置选项。(P76-P78)
1)这些选项都在内核配置工具的“kernel hacking”菜单中。
2)、并非所有的体系架构都支持其中的某些选项。
二、通过打印测试
1、printk
1)、printk与pritf的不同
I、日志级别或消息优先级的不同——pritk可以根据消息优先级所表示的严重程度对消息进行分类。
A、通常采用宏来指示日志级别。(P79)
每个字符串表示一个尖括号中的整数。整数值范围为0-7,数值越小,优先级越高。
未指定优先级的printk语句的默认级别为DEFAULT_MESSAGE_LOGLEVEL。
2)、根据日志级别,内核可能会把消息打印到当前控制台上,这个控制台可以使一个字符模式的终端、一个串口打印机或是一个并口打印机。当优先级小于console_loglevel
这个整数变量的值,消息才能显示出来,而且每次输出一行。
I、如果系统同时运行了klogd和syslogd,则无论console_loglevel为何值,内核消息都将追加到/var/log/messages中(否则按照syslogd的配置处理)。
II、如果klogd没有运行,这些消息就不会传递到用户空间,这种情况下,只能查看/proc/kmsg文件(或通过dmesg命令)。
II、如果使用了klogd,则应该了解它不会保存连续相同的信息行;它只会八村连续相同的第一行,并在最后打印这一行的重复次数。
3)、变量console_loglevel的初始值是DEFAULT_MESSAGE_LOGLEVEL,而且还可以通过sys_syslog系统调用修改。
I、调用klogd时可以指定-c开关项来修改这个变量。
II、注意,修改其当前值,必须先杀死klogd,然后再用新的-c选项重新启动它。
III、还可以通过编写程序来改变控制台的日志级别。
4)、通过文本文件/proc/sys/kernel/printk的访问来读取和修改控制台的日志级别。此文件包含了4个整数值:
I、当前的日志级别。
II、未明确指定日志级别时的默认消息级别。
III、最小允许的日志级别。
IV、引导时的默认日志级别。
2、重定向控制台信息
1)、对于控制台日志策略。Linux允许有某些灵活性:内核可以将消息发送到一个指定的虚拟控制台。默认情况下,“控制台”就是当前的虚拟控制端。
I、可以在任何一个控制台设备上调用ioctl(TIOCLINUX)来指定接收消息的其他虚拟终端。
II、setconsole程序。
3、消息如何被记录
1)、访问日志引擎接口
I、syslog系统调用。
II、/proc/kmsg。
III、对/proc/kmsg进行读写操作时,日志缓冲区中被读取的数据就不再保留,而syslog系统调用却能通过选项返回日志数据并保留这些数据,以便其他进程也能使用。
2)、Linux消息处理方法的另一个特点是,可以在任何地方调用printk,甚至在中断处理函数里也可以调用,而且对数据量的大小没有限制。但缺点是可能丢失某些数据。
3)、避免扰乱系统日志
I、为klogd指定-f(file)选项,指示klogd将消息保存到某个特定文件,或者修改/etc/syslog.conf来满足自己的需求。
II、杀掉klogd,而将消息详细地打印到空间的虚拟终端上,或者在一个为使永的xterm上执行命令cat /proc/kmsg来显示信息。
4、开启和关闭信息
1)、调用printk的编码方法可以个别或全局地开关printk语句:此技巧是定义一个宏,在需要时,这个宏开为一个printk调用。
2)、实现代码。
3)、每个驱动程序都会有自身的功能和监视需求。良好的编程技术在于选择灵活性和效率的最佳折衷点。
5、速度限制
1)、通常,正式的代码不应该在正常的操作下打印任何信息,而打印出的信息应作为对需要引起注意的异常情形显示。
2)、在许多情况下,最好的办法是设置一个标志,并在该标志被设置时不再打印任何信息。但在某些情况下,仍然有理由偶尔发出一条“该设备停止工作”这样的信息。内核提
供了一个有用的函数:
int printk_ratelimit(void);
6、打印编号
1)、打印设备的主设备号和次设备号,内核提供了一对辅助宏:
int print_dev_t(char *buffer, dev_t dev);
char *format_dev_t(char *buffer, dev_t dev);
三、通过查询调试
1、printk打印调试的缺点
2、对系统进行查询的方法
1)、在/proc文件系统中创建文件。
2)、使用驱动程序的ioctl方法。
3)、通过sysfs导出属性。
3、使用/proc文件系统
1)、/proc文件系统文件系统是一种特殊的、由软件创建的文件系统,内核使用它向外界导出信息。/proc下面的每一个文件都绑定于一个内核函数,用户读取其中的文件时,
该函数动态地生成文件的“内容”。
2)、/proc对Linux系统有着重要的作用。
3)、具有完整特征的/proc入口项相当复杂:在特征之中,/proc不仅可以用于读数据,也可以用于写数据。
4)、不建议在/proc下添加文件,建议新的代码通过sysfs来向外界导出信息。
4、在/proc中实现文件
1)、所有使用/proc的模块必须包含<linux/proc_fs.h>,并通过这个头文件来定义正确的函数。
2)、在某个进程读取我们的/proc文件时,内核会分配一个内存页,驱动程序可以将数据通过这个内存页返回到用户空间。该缓冲区会传入我们定义的函数,该函数称为
read_proc方法:
int (read_proc)(char *page, char **start, off_t offset, int count, int *eof, void *data);
I、page指针指向用来写入数据的缓冲区。
II、函数应使用start返回实际的数据写到内存页的哪个位置。
III、offset与count这两个参数与read方法相同。
IV、eof参数指向一个整数值,当没有数据可返回时,驱动程序必须设置此参数。
V、data参数是提供给驱动程序专用的数据指针,可用于内部记录。
3)、scull设备read_proc函数的简单实现
5、老的/proc接口
6、创建自己的/proc文件
1)、把read_proc函数与一个/proc入口项链接起来:
struct proc_dir_entry *create_proc_entry(const char *name, mode_t mode,struct proc_dir_entry *base,
read_proc_t *read_proc, void *data);
I、name是创建的文件名称。
II、mode是该文件的保护掩码(可传入0表示系统默认值)。
III、base指定该文件所在的目录。
IV、read_proc是实现该文件的read_proc函数。
V、内核会忽略打他参数,但是会将该参数传递给read_proc。
VI、目录项指针可用来在proc下创建完整的目录层次结构。
2)、删除/proc中的入口项:remove_proc_entry()函数。
I、如果删除入口项失败,将导致未预期的调用,如果模块已被卸载,内核会奔溃。
7、/proc文件实现的不足
1)、删除/proc项引发的问题。
2)、使用同一名字注册两个入口项的问题。
8、sep_file接口
1)、sep_file接口假定我们正在创建的虚拟文件要顺序遍历一个项目序列,而这些项目正是必须要返回给用户空间的,为使用sep_file,我们必须创建一个简单的“迭代器”对
象,该对象用来表示项目序列中的位置,每前进一步,该对象输出序列中的一个项目。
2)、用sep_file接口创建/proc文件
I、第一步包含<linux/seq_file.h>头文件,然后必须建立四个迭代器对象,分别是start、next、stop和show。
II、start犯法始终会首先调用,该函数的原型如下:
void *start(struct seq_file, loff_t *ops);
这里的sfile参数几乎可在大多数情况下忽略。
ops是一个整数的位置值,表明读取的位置。
III、next函数应将迭代器移动到下一个位置,并在序列中没有其他项目时返回NULL。
void *next(struct seq_file *sfile, void *v, loff_t &pos);
v是先前对start和next的调用所返回的迭代器。
pos是文件的当前位置。
IV、当内核使用迭代器之后,回调用stop方法通知我们进行处理:
void *stop(struct seq_file *sfile, void *v);
V、在stop调用之间,内核会调用show方法来将实际的数据传输到用户空间:
void *show(struct seq_file *sfile, void *v);
此方法应该为迭代器v所指向的项目建立输出。但是,他不能使用printk函数,而要使用针对seq_file输出的一组特殊函数。
int seq_printf(struct seq_file *file, const char *fmt, . . .);
int seq_putc(struct seq_file *file, char c);
int seq_puts(struct seq_file *file, const char c);
int seq_escape(struct seq_file *m, const char *s, const char *esc);
int seq_paths(struct seq_file *file, struct vfsmout *m, struct dentry *dentry, char *esc);
3)、将完整的迭代器操作函数打包并和/proc中的某个文件链接起来
I、首先要填充一个seq_operations结构。
II、创建一个内核能理解的文件实现。
首先创建一个open方法,该方法将文件连接到sep_file操作。
定义seq_operations结构。
建立实际的/proc文件。(struct proc_dir_entry *create_proc_entry(const char *name, mode_t mode, struct proc_dir_entry *parent);)
9、ioctl方法
1)、ioctl方法接受一个“命令”号以及另一个参数,命令号用以标识将要执行的命令,而可选参数通常是个指针。
2)、作为替代/proc文件系统的方法,可以专为调试设计若干ioctl命令。这些命令从驱动程序复制相关的数据到用户空间,然后可在用户空间中检验这些数据。
3)、ioctl方法的优缺点。
三、通过监视测试
1、监视用户空间程序的方法:
1)、调试器一步步跟踪它的函数。
2)、插入打印语句
3)、在strace状态下运行程序。
2、strace工具是一个功能非常强大的工具,它可以显示由用户空间程序发出的所有系统调用。它不仅可以显示调用,还可以显示调用参数以及用符号形式表示的返回值。
1)、strace命令行选项
I、-t:该选项用来显示调用发生的时间。
II、-T:显示调用所花费的时间。
III、-e:限定被跟踪的调用类型。
IV、-o:将输出重定向到一个文件中。默认情况下,strace将跟踪信息打印到stderr上。
3、strace从内核中接收信息。这意味着一个程序师傅支持调试的编译或者是否被去掉了符号信息,都可以被跟踪。
4、跟踪信息通常用于生成错误报告,然后把它们发送给应用程序开发人员,但是它对内核编程人员来说也同样非常有用。
四、调试系统故障
1、oops信息
1)、大部分错误都是因为多NULL指针取值或因为使用了其他不正确的指针值。这些错误通常会导致一个oops信息。
2)、oops显示发生错误时处理器的状态。这些信息由失效处理函数中的printk语句产生。
3)、处理ooops
I、当面对oops时,首先要观察的是发生的问题所在的位置。这通常可以通过调用栈信息得到。
II、如需要更多的信息,调用栈可以告诉我们系统是如何到达故障点的。
III、最后,在观察oops清单时还要记得观察“slab毒剂”值。
4)、只有在构造内核时打开了CONFIG_KALLSYMS选项,我们才能看到符号化的调用栈;否则我们只能看到裸的、十六进制的清单。
2、系统挂起
1)、尽管内核代码中的大多数错误只会导致一个oops信息,但有时它们会将系统完全挂起,如果系统挂起了,任何信息都无法打印出来。
2)、如果系统确实会挂起,而又不知道在什么位置插入schedule调用时,最好的方法是加入一些打印信息,并把它们写入控制台。
如果系统看起来挂起了,但其实没有。这时,运行专为探明此种情况而设计的程序,通过查看它的输出情况,可以发现这些假的挂起。
3)、对于上述情形,可利用“SysRq魔法键”
I、大多数架构上都可以使用魔法键。SysRq魔法键可以通过键盘上的ALT和SysRq组合键来激活。
II、根据与这两个键一起按下的第三个键的不同,内核会执行许多有用动作中的其中一个:
r:关闭键盘的raw模式。
k:激活”留意安全键“功能。
s:对所有磁盘进行紧急同步。
u:尝试以只读模式重新挂载所有磁盘。
b:立即重启系统。
p:打印当前处理器寄存器信息。
t:打印当前任务列表。
m:打印内存信息。
III、可通过下面的命令启用SysRq功能:
echo 0 > /proc/sys/kernel/sysrq
如果有未授权用户可以使用当前系统的键盘,则应该考虑禁止此功能,以避免出现意外或者蓄意的破环。可向/proc/sys文件写1来禁止。
IV、SysRq功能也可以对无法访问控制台的系统管理员开放。
V、复现系统的挂起故障时,另一个要采用的预防措施是。把所有的磁盘以只读的方式挂载在系统(或者干脆卸载它们)。另一个可行的办法是通过NFS挂载所有的文件系
统。
五、调试器和相关工具
1、使用gdb
1)、gdb在探究系统内部行为时非常有用。
2)、启动调试器时必须把内核看作一个应用程序。除了指定未压缩的内核映像文件名以外,还应该在命令行中提供”core文件“的名称。对于正在运行的内核,所谓的core文件
就是这个内核在内存中的核心映像。
3)、在gdb使用中,可使用标准的gdb命令查看内核变量。
4)、对内核进行调试时,gdb的许多常用功能都不可用。为了让内核使用内核的符号信息,必须在打开CONFIG_DEBUG_INFO选项的情况下编译内核。
5)、调试会话相关的代码段:
I、.text。
II、.bss。
III、data。
6)、为了gdb能够处理可装载模块,必须告诉调试器装载模块代码的具体位置。该信息可通过sysfs的/sysfs/module获得。
2、kdb内核调试器
1)、要使用kdb,首先必须获得这个补丁(取得的版本一定要和内核版本匹配),然后对当前内核源代码进行patch操作,在重新编译并安装这个内核。
2)、一旦运行的是支持kdb的内核,则可以用下面几个方法进入kdb的调试状态:
I、在控制台上按下Psuse(或Break)键将启动调试。
II、当内核发生oops,或到达某个断点时,也会启动kdb。
3)、当运行kdb时,内核所做的每一件事都会停下来。当激活kdb调试时,系统不应运行其他任何东西;尤其是,不要开启网络功能——除非是调试网络驱动程序。
3、kgdb补丁
1)、kgdb补丁将运行调试内核的系统和运行调试器的系统隔离开来而工作,而这两个系统之间通过串口线缆相连。
4、用户模式的Linux虚拟机
1)、用户模式Linux作为一个独立的、可移植的Linux内核而被创建,包含在子目录arch/um中。UML不运行于某种新硬件上,二十运行在基于Linux系统调用接口所实现的虚
拟机上。
2)、对内核开发人员来说,可以很容易利用gdb或其他调试器对用户模式Linux进行处理。
3)、从驱动程序编写者看来,UML也有明显的缺点:用户模式的内核无法访问主机系统的硬件。
5、Linux跟踪工具包