linux设备驱动四(调试技术)
内核中的调试支持
安装自己的内核,发行版内核会关闭映像性能的调试功能,kernel hacking的配置:
- CONFIG_DEBUG_KERNEL,总开关,使其他调试选项可用
- CONFIG_DEBUG_SLAB,可以检查许多内存溢出及忘记初始化的错误,分配给每个字节设置为0xa5,释放设置为0x6b
- CONFIG_DEBUG_PAGEALLOC,可以快速定位内存损坏的位置
- CONFIG_DEBUG_SPINLOCK,捕获未初始化,或者重复解锁的错误
- CONFIG_DEBUG_SPINLOCK_SLEEP,检查持有自旋锁时休眠企图,可能引起休眠的函数
- CONFIG_INIT_DEBUG,检查初始化完成之后对于初始化的内存空间的访问企图
- CONFIG_DEBUG_INFO,内核构造包含完整的调试信息。计划使用gdb还应该打开CONFIG_FRAME_POINTER
- CONFIG_MAGIC_SYSRQ,magic sysrq按键
- CONFIG_DEBUG_*
- CONFIG_DEBUG_STACK_USAGE,跟踪内核栈的溢出,第一个选项明确栈溢出检查,第二个通过sysrq输出统计信息
- CONFIG_KALLSYMS,用于调试上下文,没有此符号,oops清单只给出十六进制内核反向跟踪信息
- CONFIG_IKCONFIG
- CONFIG_IKCONFIG_PROC,内核配置状态包含在内核中,并通过proc访问
- CONFIG_ACPI_DEBUG,ACPI(advanced configuration and power interface,高级配置和电源接口)
- CONFIG_DEBUG_DRIVER,驱动程序核心中的调试信息
- CONFIG_SCSI_CONSTANTS,打开详细的SCSI错误信息
- CONFIG_INPUT_EVBUG,会打开对输入事件的详细记录
- CONFIG_PROFILING,剖析通常用于系统性能的调节
通过打印调试
printk,根据级别或优先级锁表示的严重程度对消息进行分类。使用宏来标示日志界别,宏会展开为一个字符串,编译时和消息文本拼接在一起,它们之间不需要逗号分割
- KERN_EMERG,系统崩溃前提示消息
- KERN_ALERT,需要立即采取动作的情况
- KERN_ERR,用于报告错误,驱动程序用它来报告来自硬件的问题
- KERN_WARNING,可能出现的问题进行警告,不会对系统造成严重问题
- KERN_NOTICE,与安全相关的情况使用这个级别
- KERN_INFO,驱动程序在启动时打印找到的硬件信息
- KERN_DEBUG,调试信息
- 每个字符串代表0-7中的一个数值,数值越小优先级越高
- 未指定时默认级别是DEFAULT_MESSAGE_LOGLEVEL,2.6.10是KERN_WARNING
- 日志优先级小于console_loglevel的值时,会打印的控制台上,每次输出一行,不以newline字符结尾则不输出
- 如果同时运行了klogd和syslogd,则无论console_loglevel为何值,消息都将追加到/var/log/messages中
- 如果klogd没有运行,消息不会传递到用户空间,只能通过dmesg命令从/proc/kmsg文件读取
- console_loglevel初始值是DEFAULT_CONSOLE_LOGLEVEL,可以通过sys_syslog系统调用修改,或者调用klogd时指定-c开关来修改(需要先kill klogd)
- 新值设置为1-8,如果设置为1,则只有0级别(KERN_KMERG)消息能到达控制台
- 可以通过/proc/sys/kernel/printk文件读取和修改控制台的日志级别,包括4个整数值分别是:当前日志级别,默认消息级别,最小允许的日志级别,及引导时默认日志级别,写入单个数值时,修改当前日志级别
重定向控制台信息
- 默认情况下“控制台”就是当前的虚拟终端,可以在任何一个控制台设备上调用ioctl(TIOCLINUX)来指定接受消息的其他虚拟终端
- TIOCLINUX,这个命令可以完成一些特定的linux功能。使用TIOCLINUX时,需要传给它一个指向字节数组的指针参数,数组的第一个字节指定所请求子命令的编号。
消息如何被记录
- printk函数将消息写到一个长度为__LOG_BUF_LEN字节的循环缓冲区
- 然后唤醒睡眠在syslog系统调用上的进程,或者在读取/proc/kmsg的进程,dmesg可以不刷新缓冲区
- 停止klogd之后读取/proc/kmsg文件会阻塞进程,如果已有klogd或其他进程读取时,不能直接读取该文件,避免竞争
- 缓冲区写满会覆盖,可能丢失旧数据
- klogd读取内核消息发送到syslogd
- syslogd根据/etc/syslog.conf找出处理这些数据的方法。
- syslogd根据功能和优先级对消息进行区分。
- 内核消息由LOG_KERN工具记录,并且与printk中对应的优先级记录,如果没有运行klogd,数据将保留在缓冲区
- klogd -(file)可以只是klogd将消息保存到某个特定文件
开启及关闭消息
通过将printk定义为一个宏,使用该宏来打印消息,这样可以开启和关闭消息
速度限制
- 信息输出到慢速控制台,过高的信息输出速度回导致系统变慢
- 过多日志很难发现什么问题,正式版本不应该在正常情况下打印日志
- 启动停止应该有日志,但是要注意那些不断重试的情况
- 打印一条可能重复的信息之前,调用int printk_ratelimit(void),速度超过一个阈值则返回零
- 可以修改/proc/sys/kernel/printk_ratelimit(重新打开消息之前等待的秒数)以及/proc/sys/kernel/printk_ratelimit_burst(速度限制之前可以接受的消息条数)
打印设备编号
- int print_dev_t(char *buffer, dev_t dev); 返回打印的字符数,传入的缓冲区要能包含设备号(64位),缓冲区大小应该至少20字节
- char *format_dev_t(char *buffer, dev_t dev); 返回缓冲区
通过查询调试
- 大量使用printk会显著降低系统性能syslogd试图把每件事都记录到磁盘上
- 在/etc/syslogd.conf中日志文件名字前加一个减号可以避免实时刷新磁盘
- 获取信息最好的方法是需要的时候才去查询
使用/proc文件系统
- int (*read_proc)(char *page, char **start, off_t offset, int count ,int eof, void*data ); start实际数据写到内存页的哪儿位置,如果把start设置为一个小的整数,调用程序可以利用它来增加filp->f_pos的值
- struct proc_dir_entry *create_proc_read_entry(const char *name, mode_t mode, struct proc_dir_entry*, read_proc_t *read_proc, void *data);
- remove_proc_entry
seq_file接口
必须实现四个迭代器对象:
-
void *start(struct seq_file *sfile, loff_t *pos); pos表明读取位置,位置通常解释为指向序列中下一个项目的游标,函数返回值供show使用
-
void *next(struct seq_file *sfile, void*v, loff_t *pos); v是之前start或next返回的迭代器,pos的值应该增加
-
void stop(struct seq_file sfile, void v); 内核使用迭代器之后,调用stop方法做清楚工作
-
int show(struct seq_file *sfile, void*v);
其他一些seq_file输出函数,在show函数中被调用: -
int seq_printf(struct seq_file *sfile, const char *fmt, …); 缓冲区写满,返回非零值,输出被丢弃
-
int seq_putc(struct seq_file *sfile, char c);
-
int seq_puts(struct seq_file *sfile, const char *s);
-
int seq_escape(struct seq_file *sfile, const char*s, const char *esc); s中包含了esc中某个字符,该泽肤以八进制形式打印。
-
int seq_path(struct seq_file *sfile, struct vfsmount *m, struct dentry *dentry, char *esc);输出与某个目录项关联的文件名。
-
要与/proc中的某个文件链接起来,首先填充一个seq_operations结构:
static struct seq_operations {
.start
.next
.stop
.show
}其次创建file_operations,我们只需要实现open方法,用来连接到之前创建seq_operations,而其他的函数,使用默认的seq_read,seq_lseek,seq_release
static int proc_open(struct inode *inode, struct file *file)
{
return seq_open(file, *seq_operations);
}最后创建proc下的文件
struct proc_dir_entry create_proc_entry(const char name, mode_t mode, struct proc_dir_entry *parent);
如果包含大量输出行,建议使用seq_file接口,(而且最新的版本,已经不支持旧的proc方式)
ioctl方法
优点
获取数据比proc快得多,二进制比文本有效
不需要分割数据为不超过一个页的片段
缺点
需要一个程序来调用ioctl
通过监视调试
strace,可以显示程序发出的所有系统调用。不仅可以显示调用,而且还能显示调用参数以及用符号形式表示的返回值。
-t 显示调用发生的时间
-T 显示调用话费的时间
-e 限定被跟踪的调用类型
-o 将输出重定向到一个文件
调试系统故障
oops消息
引用一个非法指针,页表无法映射到物理地址,处理器向操作系统发出page fault。如果地址非法,内核无法换入page in 缺失页面,就会产生oops
通过栈清单确定局部变量和函数参数的值
栈顶部的ffffffff是导致故障的字符串,用户空间的默认栈自0xc00000000乡下,因此0xbfffda70可能使用空间的栈地址,该地址调用连上重复乡下传递。x86架构上,内核空间起始于0xc00000000,大于该地址的几乎肯定是内核空间地址等等。
系统挂起
通过在一些关键点上插入schedule调用可以防止死循环。当驱动程序因为错误陷入死循环时,借助schedule调用杀死这个进程。或者加入一些打印信息
显示器上时钟或者系统复核表任然在更新说明系统任然在工作
sysrq,通过ALT和SysRq组合键**,通过SysRq和第三个按键,内核执行不同动作:
- r 关闭键盘的raw模式,崩溃的应用程序可能让键盘处于奇怪状态
- k **SAK功能,SAK将杀死当前控制台上所有进程,留下干净中断
- s 对所有磁盘进行紧急同步
- u 尝试以制度模式重新挂载所有磁盘
- b 立即重启系统,先要执行同步并重新挂载磁盘
- p 打印当前处理器寄存器信息
- t 打印当前任务列表
- m 打印内存信息
echo 0 > /proc/sys/kernel/sysrq 禁用SysRq功能
可以向/proc/sysrq-trigger写入字符来触发相应的SysRq动作,这个入口点始终可用,即使SysRq是禁止的。
构造一个打开补习功能的内核,并通过引导命令行参数profile=2引导该内核,利用readprofile工具重置剖析计数器,让驱动程序进入死循环,经过一段时间再次使用readprofile即可观察浪费CPU资源的内核位置。复现故障时要保护好数据,以只读挂载硬盘。
调试器和相关工具
调试器非常耗时,应尽量避免
使用gdb
gdb /usr/src/linux/vmlinux(正站在运行内核的未压缩映像文件) /proc/kcore(内核在内存中的核心映像)
kcore用来按照core文件的格式表示内核的“可执行文件”;由于它要表示对应于所有物理内存的整个内核地址空间,所以是一个非常巨大的文件。
使用gdb打印的数据,是正在运行内核数据的一个缓存,要保持更新可以执行core-file/proc/kcore命令。不过缓存的是引用过的,第一次访问总是访问到最新的数据
gdb不能修改内核数据,不能设置断点或观察点,也不能但不跟踪
gdb要起作用,必须打开CONFIG_DEBUG_INFO
模块不是传递给gdb的vmlinux映像的一部分,需要另外处理
模块有很多代码段,和调试相关的只有三个:
.text 模块的可执行代码
.bss
.data 这两个代码段保存模块的变量,编译时未初始化的在.bss中,其他初始化的在.data中
gdb要能处理模块,必须告诉调试器,模块代码段的具体位置,可以通过/sysfs/module获得,在/sys/module/module_name/sections目录中
通过add-symbol-file加入模块代码段地址信息
print *(address) 可以传入十六位地址,输出对应文件以及代码行数
kdb内核调试器
准备工作
oss.sgi.com上以非正式补丁形式提供
获得补丁
patch操作
重编内核
仅可用于IA-32(x86)系统
启动调试
在控制台按下Pause(或Break)启动调试
当内核发生oops,或到达某个断点也会启动调试
注意事项:
kdb运行时,内核所做的每件事都会停下来,**kdb时,不应运行其他东西
如果要使用kdb,最好在启动时进入单用户模式
一些常用命令:
bp,设置断点(gdb中b)
go,开始运行(gdb中人r)
bt,堆栈跟踪信息
mds,用来查看数据,数据显示是以16进制地址显示的,没有符号名,所以需要自己转换
mm,可以修改内存数据
kgdb补丁
gdb和kdb均无法提供类似应用程序开发人员使用的环境
也是通过补丁的方式
kgdb将运行调试内核的系统和运行调试器的系统隔离开,通过串口线连接
以支持通过局域网通信,打开以太网模式,在引导时设置kgdboe参数,指出命令来源IP
用户模式的linux虚拟机
User-mode linux, UML,可以是linux内核称为一个用户模式的进程。
优点:可以很容易利用gdb进行调试。
缺点:无法访问主机硬件。无法调试正真和硬件打交道的驱动程序。
linux跟踪工具包
linux trace toolkit,lTT是一个内核补丁,可以跟踪时间信息,合理建立在一段制定时间内所发生时间的完整描述,可用于测试,或补货性能方面的问题。
动态探测
Dynamic Probes,DProbes,可以在系统任何地方防止一个探针,可以是用户空间也可以是内核空间。探针是一些特殊代码,,到达给定点时,开始执行。
内核需要编译进这个功能