ldd3学习笔记:调试技术

来源:互联网 发布:淘宝自动秒杀软件 编辑:程序博客网 时间:2024/05/17 04:48

1.内核中支持的调试选项打开:kernel hacking菜单

CONFIG_DEBUG_KERNEL:使其他调试选项可用; 但不激活任何特性.

CONFIG_DEBUG_DRIVER:在"Device drivers"下 打开驱动核心的调试信息,

CONFIG_INPUT_EVBUG:在"Device drivers/Input device support"下打开输入事件的详细日志.

2.prink打印调试:

1). printk函数中能够指定优先级,在头文件 <linux/kernel.h> 里定义了0-7共8个级别的输出优先级,数字越小,优先级越高;未指定优先级的prink采用默认级别DEFAULT_MESSAGE_LOGLEVEL, 这个宏在kernel/printk.c中定义为一个整数。

#define DEFAULT_MESSAGE_LOGLEVEL 4 /* KERN_WARNING */

  头文档linux/printk.h中宏定义了8个级别,0-8从高到低分别是:
    KERN_EMERG, KERNEL_ALERT, KERN_CRIT, KERN_ERR, KERN_WARNING, KERN_NOTICE, KERN_INFO, KERN_DEBUG
2). 根据日志级别,内核可能会将消息打印到当前控制台上。当优先级小于console_loglevel时,消息才能显示出来。控制台输出级别 console_loglevel 初始化成 DEFAULT_CONSOLE_LOGLEVEL。
  a.能够使用系统调用sys_syslog或klogd -c来修改console_loglevel值。注意要改变当前值, 你必须先杀掉 klogd, 接着使用 -c 选项重启它.
  b.也能够直接通过文档 /proc/sys/kernel/printk 读写控制台记录级别(这个文档包含4个整数值,前两个表示系统当前的优先级和缺省优先级), 指定一个整数在 1 和 8 之间, 包含 1 和 8. 如果它设为 1, 只有 0 级消息( KERN_EMERG )到达控制台; 如果它设为 8, 所有消息, 包括调试消息, 都显示.

例如, 你可以使所有内核消息出现在控制台, 通过简单地输入:

#echo 7 > /proc/sys/kernel/printk
#cat /proc/sys/kernel/printk  
7 4 1 7  

4个整数值分别是:当前的loglevel、默认loglevel、最小允许的loglevel、引导时的默认loglevel。

  c.也能够指定显示在其他控制台,通过调用ioctl(TIOCLINUX)或shell命令setconsole来配置。在 setconsole 里, 使用子命令 11, 下一个字节(存于 bytes[1])指定虚拟控制台.
     使用特殊的 ioctl 命令 TIOCLINUX来设定控制台设备号,(TIOCLINUX在drivers/char/tty_io.c 里定义):

char bytes[2] = {11,3}; /* 11 is the TIOCLINUX cmd number , 3 is the the chosen console number */ 

ioctl(STDIN_FILENO, TIOCLINUX, bytes); /* use stdin */

  d.如果同时运行了klogd和syslogd,则无论console_loglevel为何值,内核消息都将追加到/var/log/messages中。如果klogd没有运行,这些消息就不会传递到用户空间,此时只能查看/proc/kmsg文件(使用dmesg命令轻松做到)。

    如果klogd没有运行,这些消息就不会传递到用户空间,此时只能查看/proc/kmsg文件(使用dmesg命令轻松做到)。

3)消息如何被记录:

printk函数将消息写到一个长度为__LOG_BUF_LEN字节的环形缓冲区,可在配置内核时为__LOG_BUF_LEN指定为4KB~1MB之间。

如果循环缓冲区填满了,printk就绕回缓冲区的开始出填写新的数据,将覆盖最早的数据。

对/proc/kmsg进行读操作,日志缓冲区被读取的数据就不再保留。

syslog系统调用能通过选项返回日志数据并保留数据。

dmesg命令可在不刷新缓冲区数据的情况下获得缓冲区内容。这个命令将缓存区的整个内容返回给 stdout, 不管它是否已经被读过.

 klogd 进程运行, 它获取内核消息并分发给 syslogd, syslogd 接着检查 /etc/syslog.conf 来找出如何处理它们.给 klogd 指定一个 -f (文件) 选项来指示它保存消息到一个特定的文件。

todo:klogd, syslogd和dmesg命令详解

4) 可通过预处理指令开启和关闭调试信息

5)为了避免printk重复输出过快而阻塞系统,内核使用以下函数跳过部分输出:

int printk_ratelimit(void);  当输出级别超过一个限度, printk_ratelimit 开始返回 0 并使消息被扔掉.

应用例子:

if (printk_ratelimit( )) 

    printk(KERN_NOTICE "The printer is still on fire\n");

可以通过修改/proc/sys/kernel/printk_ratelimit(重开信息前应等待的秒数)/proc/sys/kernel/printk_ratelimit_burst(在速度限制前可接受的信息数)来定制printk_ratelimit的行为。

3.查询调试

查询系统的三种方法:在 /proc 文件系统下创建文件; 使用 ioctl 驱动方法;借助 sysfs 输出属性(驱动模型,14章介绍)

方法一:在 /proc 文件系统下创建文件

1) /proc 文件系统是动态的, 因此你的模块可以在任何时候添加或去除条目.大部分时间, /proc 条目是只读的文件.

2)所有使用 /proc 的模块应当包含 <linux/proc_fs.h> 来定义正确的函数.

3)创建一个只读 /proc 文件
int (*read_proc)(char *page, char **start, off_t offset, int count, int *eof, void *data);

连接它到 /proc 层次中的一个入口项. 使用一个 creat_proc_read_entry 调用,可以在目录base(NULL 时默认为在 /proc 根下创建 )下创建一个名为name的文件。

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); 
a

模块卸载后去除/proc 中的入口:

remove_proc_entry("scullmem", NULL /* parent dir */); 
注意:内核不会检查名字是否已经注册, 如果用同样的名字注册两个入口,在存取或移出入口时都是不能区分的。

4)采用seq_file 接口来创建大内核虚拟文件.

包含 <linux/seq_file.h>. 

接着你必须创建 4 个 iterator 方法, 称为 start, next, stop, 和 show.

void *start(struct seq_file *sfile, loff_t *pos);
void *next(struct seq_file *sfile, void *v, loff_t *pos);  //next 函数应当移动 iterator 到下一个位置
void stop(struct seq_file *sfile, void *v); //当内核处理完 iterator, 它调用 stop 来清理
int show(struct seq_file *sfile, void *v); //内核调用 show 方法来真正输出有用的东西给用户空间,这个方法应当创建序列中由 iterator v 指示的项的输出. 不应当使用 printk

(值得注意 seq_file 代码在调用 start 和 stop 之间不睡眠或者进行其他非原子性任务. 你也肯定会看到在调用 start 后马上有一个 stop 调用. 因此, 对你的 start 方法来说请求信号量或自旋锁是安全的. 只要你的其他 seq_file 方法是原子的, 调用的整个序列是原子的.)

int seq_printf(struct seq_file *sfile, const char *fmt, ...); //给 seq_file 实现的 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 中出现的字符以八进制格式打印. esc 的一个通用值是"\t\n\\", 它使内嵌的空格不会搞乱输出和可能搞乱 shell 脚本.

现在已有了一个完整的 iterator 操作的集合, scull 必须包装起它们, 并且连接它们到 /proc 中的一个文件. 第一步是填充一个 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}; 


创建一个 file_operations 结构(是的, 和字符驱动使用的同样结构) 来实现所有内核需要的操作, 来处理文件上的读和移动. 幸运的是, 这个任务是简单的. 第一步是创建一个 open 方法连接文件到 seq_file 操作:

static int scull_proc_open(struct inode *inode, struct file *file){    return seq_open(file, &scull_seq_ops);}

调用 seq_open 连接文件结构和我们上面定义的序列操作. 事实证明, open 是我们必须自己实现的唯一文件操作, 因此我们现在可以建立我们的 file_operations 结构:

static struct file_operations scull_proc_ops = { .owner = THIS_MODULE, .open = scull_proc_open, .read = seq_read, .llseek = seq_lseek, .release = seq_release }; 

最后的步骤是调用低层的 create_proc_entry创建 /proc 中的实际文件:

struct proc_dir_entry *create_proc_entry(const char *name,mode_t mode,struct proc_dir_entry *parent); 

entry = create_proc_entry("scullseq", 0, NULL);if (entry)    entry->proc_fops = &scull_proc_ops;

方法二:ioctl 方法:

ioctl是一个系统调用, 作用于一个文件描述符; 作为一个使用 /proc 文件系统的替代, 你可以实现几个用来调试用的 ioctl 命令. 

这种方式使用 ioctl 来获取信息有些比使用 /proc 困难, 因为你需要另一个程序来发出 ioctl 并且显示结果. 必须编写这个程序, 编译, 并且与你在测试的模块保持同步. 另一方面, 驱动侧代码可能容易过需要实现一个 /proc 文件的代码.

有时候 ioctl 是获取信息最好的方法, 因为它运行比读取 /proc 快. 如果在数据写到屏幕之前必须做一些事情, 获取二进制形式的数据比读取一个文本文件要更有效. 另外, ioctl 不要求划分数据为小于一页的片段.

ioctl 方法的另一个有趣的优点是信息获取命令可留在驱动中, 当调试被禁止时, 不象对任何查看目录的人都可见的 /proc 文件, 不记入文档的 ioctl 命令可能保持不为人知. 另外, 如果驱动发生了怪异的事情, 它们仍将在那里. 唯一的缺点是模块可能会稍微大些.


4. 用观察来调试
strace 命令可以 显示所有的用户空间程序发出的系统调用, 还以符号形式显示调用的参数和返回值. 当一个系统调用失败, 错误的符号值(例如, ENOMEM)和对应的字串(Out of memory) 都显示. strace 有很多命令行选项; 其中最有用的是 -t 来显示每个调用执行的时间, -T 来显示调用中花费的时间, -e 来限制被跟踪调用的类型, 以及-o 来重定向输出到一个文件. 缺省地, strace 打印调用信息到 stderr.
 strace ls /dev > /dev/scull0 

5.调试系统故障
1)oops 消息:当引用了一个NULL指针或其他不正确的时,通常会产生oops错误。通常通过指令指针EIP来判断错误类型。其次通过查看调用堆栈. 
Unable to handle kernel NULL pointer dereference at virtual address 00000000
EIP is at faulty_write+0x4/0x10 [faulty] 


这是运行一个NULL指针导致的结果,指令指针EIP中显示了出错的函数faulty_write.

Unable to handle kernel paging request at virtual address ffffffff
EIP is at 0xffffffff  

当拷贝一个字符串到数组,字串长于目的数组. 当函数返回时导致的缓存区溢出引起一次 oops . 因为返回指令使指令指针到不知何处, 

EIP返回0xffffffff. 同时,我们只看到部分的调用堆栈( vfs_read 和 faulty_read 丢失 ), 内核抱怨一个"坏 EIP 值". 这个抱怨和在开头列出的犯错的地址 ( ffffffff ) 都暗示内核堆栈已被破坏.


Unable to handle kernel paging request at virtual address 0xa5a5a5a5a5
EIP is at 0xa5a5a5a5a5  

如果犯错地址时 0xa5a5a5a5a5, 你几乎肯定 - 某个地方在初始化动态内存.

(注意:只在你的内核是打开 CONFIG_KALLSYMS 选项而编译时可以看到符号的调用堆栈. )

2)系统挂起
 sysrq 功能:参考documentation/sysrq.txt,sysrq p 功能直接指向出错的函数.
开启/关闭:echo 1 > /proc/sys/kernel/sysrq;echo 0 > /proc/sys/kernel/sysrq

内核剖析功能:参考documentation/basic_profiling.txt 。建立一个打开剖析的内核, 并且用命令行中 profile=2 来启动它. 使用 readprofile 工具复位剖析计数器, 接着使你的驱动进入它的循环. 一会儿后, 使用 readprofile 来看内核在哪里消耗它的时间. 另一个更高级的选择是 oprofile, 你可以也考虑下. 

在追逐系统挂起时一个值得使用的防范措施是以只读方式加载你的磁盘(或者卸载它们). 如果磁盘是只读或者卸载的, 就没有风险损坏文件系统或者使它处于不一致的状态. 另外的可能性是使用一个通过 NFS, 网络文件系统, 来加载它的全部文件系统的计算机, 内核的"NFS-Root"功能必须打开, 在启动时必须传递特殊的参数. 在这个情况下, 即便不依靠 sysrq 你也会避免文件系统破坏, 因为文件系统的一致由NFS 服务器来管理, 你的设备驱动不会关闭它.

6.调试器和工具

gdb, kdb, kgdb,UML(用户模式linux), LTT(linux追踪工具), DProbes(动态探针)




原创粉丝点击