|
|
|
内容:
|
|
编写 Kprobes 模块
|
|
使用 Kprobes 更好地进行调试
|
|
参考资料
|
|
关于作者
|
|
对本文的评价
|
|
|
|
相关内容:
|
|
Kprobes home page
|
|
Kprobes README
|
|
Dynamic Probes patch
|
|
Kernel Hooks
|
|
|
|
|
将 printk 插入到运行中的 Linux 内核
Prasanna S. Panchamukhi
开发人员,Linux Technology Center, IBM India Software Labs
2004 年 9 月 19 日
使 用 printk 收集 Linux ™ 内核的调试信息是一个众所周知的方法 —— 而使用了 Kprobes,不需要经常重新引导和重新编译内核就可以完成这一任务。Kprobes 与 2.6 内核结合起来提供了一个动态插入 printk's 的轻量级、无干扰而且强大的装置。记录调试信息(比如内核栈追踪、内核数据结构和寄存器)日志从来没有这么简单过!
Kprobes 是 Linux 中的一个简单的轻量级装置,让您可以将断点插入到正在运行的内核之中。 Kprobes 提供了一个强行进入任何内核例程并从中断处理器无干扰地收集信息的接口。使用 Kprobes 可以 轻松地收集处理器寄存器和全局数据结构等调试信息。开发者甚至可以使用 Kprobes 来修改 寄存器值和全局数据结构的值。
为完成这一任务,Kprobes 向运行的内核中给定地址写入断点指令,插入一个探测器。 执行被探测的指令会导致断点错误。Kprobes 钩住(hook in)断点处理器并收集调试信息。Kprobes 甚至可以单步执行被探测的指令。
安装
要安装 Kprobes,需要从 Kprobes 主页下载最新的补丁(参阅 参考资料 中的链接)。 打包的文件名称类似于 kprobes-2.6.8-rc1.tar.gz。解开补丁并将其安装到 Linux 内核:
$tar -xvzf kprobes-2.6.8-rc1.tar.gz
$cd /usr/src/linux-2.6.8-rc1
$patch -p1 < ../kprobes-2.6.8-rc1-base.patch
Kprobes 利用了 SysRq 键,这个 DOS 时代的产物在 Linux 中有了新的用武之地(参阅 参考资料)。您可以在 Scroll Lock键左边找到 SysRq 键;它通常标识为 Print Screen。要为 Kprobes 启用 SysRq 键,需要安装 kprobes-2.6.8-rc1-sysrq.patch 补丁:
$patch -p1 < ../kprobes-2.6.8-rc1-sysrq.patch
使用 make xconfig/ make menuconfig/ make oldconfig 配置内核,并 启用 CONFIG_KPROBES 和 CONFIG_MAGIC_SYSRQ 标记。 编译并引导到新内核。您现在就已经准备就绪,可以插入 printk 并通过编写简单的 Kprobes 模块来动态而且无干扰地 收集调试信息。
编写 Kprobes 模块
对于每一个探测器,您都要分配一个结构体 struct kprobe kp; (参考 include/linux/kprobes.h 以获得关于此数据结构的详细信息)。
清单 1. 定义 pre、post 和 fault 处理器
/* pre_handler: this is called just before the probed instruction is * executed. */
int handler_pre(struct kprobe *p, struct pt_regs *regs) { printk("pre_handler: p->addr=0x%p, eflags=0x%lx ",p->addr, regs->eflags); return 0; }
/* post_handler: this is called after the probed instruction is executed * (provided no exception is generated). */
void handler_post(struct kprobe *p, struct pt_regs *regs, unsigned long flags) { printk("post_handler: p->addr=0x%p, eflags=0x%lx ", p->addr, regs->eflags); }
/* fault_handler: this is called if an exception is generated for any * instruction within the fault-handler, or when Kprobes * single-steps the probed instruction. */
int handler_fault(struct kprobe *p, struct pt_regs *regs, int trapnr) { printk("fault_handler:p->addr=0x%p, eflags=0x%lx ", p->addr, regs->eflags); return 0; }
|
获得内核例程的地址
在注册过程中,您还需要指定插入探测器的内核例程的地址。使用这些方法中的任意一个来获得内核例程 的地址:
- 从 System.map 文件直接得到地址。
例如,要得到 do_fork 的地址,可以在命令行执行 $grep do_fork /usr/src/linux/System.map 。
- 使用 nm 命令。
$nm vmlinuz |grep do_fork
- 从 /proc/kallsyms 文件获得地址。
$cat /proc/kallsyms |grep do_fork
- 使用 kallsyms_lookup_name() 例程。
这个例程是在 kernel/kallsyms.c 文件中定义的,要使用它,必须启用 CONFIG_KALLSYMS 编译内核。 kallsyms_lookup_name() 接受一个字符串格式内核例程名, 返回那个内核例程的地址。例如: kallsyms_lookup_name("do_fork");
然后在 init_moudle 中注册您的探测器:
清单 2. 注册一个探测器
/* specify pre_handler address */ kp.pre_handler=handler_pre; /* specify post_handler address */ kp.post_handler=handler_post; /* specify fault_handler address */ kp.fault_handler=handler_fault; /* specify the address/offset where you want to insert probe. * You can get the address using one of the methods described above. */ kp.addr = (kprobe_opcode_t *) kallsyms_lookup_name("do_fork");
/* check if the kallsyms_lookup_name() returned the correct value. */ if (kp.add == NULL) { printk("kallsyms_lookup_name could not find address for the specified symbol name "); return 1; }
/* or specify address directly. * $grep "do_fork" /usr/src/linux/System.map * or * $cat /proc/kallsyms |grep do_fork * or * $nm vmlinuz |grep do_fork */ kp.addr = (kprobe_opcode_t *) 0xc01441d0;
/* All set to register with Kprobes */ register_kprobe(&kp);
|
一旦注册了探测器,运行任何 shell 命令都会导致一个对 do_fork 的调用,您将可以在控制台上或者运行 dmesg 命令来查看您的 printk。做完后要记得注销探测器:
unregister_kprobe(&kp);
下面的输出显示了 kprobe 的地址以及 eflags 寄存器的内容:
$tail -5 /var/log/messages
Jun 14 18:21:18 llm05 kernel: pre_handler: p->addr=0xc01441d0, eflags=0x202
Jun 14 18:21:18 llm05 kernel: post_handler: p->addr=0xc01441d0, eflags=0x196
获得偏移量
您可以在例程的开头或者函数中的任意偏移位置插入 printk(偏移量必须在指令范围之内)。 下面的代码示例展示了如何来计算偏移量。首先,从对象文件中反汇编机器指令,并将它们 保存为一个文件:
$objdump -D /usr/src/linux/kernel/fork.o > fork.dis
其结果是:
清单 3. 反汇编的 fork
000022b0 <do_fork>: 22b0: 55 push %ebp 22b1: 89 e5 mov %esp,%ebp 22b3: 57 push %edi 22b4: 89 c7 mov %eax,%edi 22b6: 56 push %esi 22b7: 89 d6 mov %edx,%esi 22b9: 53 push %ebx 22ba: 83 ec 38 sub $0x38,%esp 22bd: c7 45 d0 00 00 00 00 movl $0x0,0xffffffd0(%ebp) 22c4: 89 cb mov %ecx,%ebx 22c6: 89 44 24 04 mov %eax,0x4(%esp) 22ca: c7 04 24 0a 00 00 00 movl $0xa,(%esp) 22d1: e8 fc ff ff ff call 22d2 <do_fork+0x22> 22d6: b8 00 e0 ff ff mov $0xffffe000,%eax 22db: 21 e0 and %esp,%eax 22dd: 8b 00 mov (%eax),%eax
|
要在偏移位置 0x22c4 插入探测器,先要得到与例程的开始处相对的偏移量 0x22c4 - 0x22b0 = 0x14 ,然后将这个偏移量添加到 do_fork 的地址 0xc01441d0 + 0x14 。(运行 $cat /proc/kallsyms | grep do_fork 命令以获得 do_fork 的地址。)
您还可以将 do_fork 的相对偏移量 0x22c4 - 0x22b0 = 0x14 添加到 kallsyms_lookup_name("do_fork"); 的输入,即: 0x14 + kallsyms_lookup_name("do_fork");
转储内核数据结构
现在,让我们使用修改过的用来转储数据结构的 Kprobe post_handler 来转储运行在系统上的所有作业的一些组成部分:
清单 4. 用来转储数据结构的修改过的 Kprope post_handler
void handler_post(struct kprobe *p, struct pt_regs *regs, unsigned long flags) { struct task_struct *task; read_lock(&tasklist_lock); for_each_process(task) { printk("pid =%x task-info_ptr=%lx ", task->pid, task->thread_info); printk("thread-info element status=%lx,flags=%lx, cpu=%lx ", task->thread_info->status, task->thread_info->flags, task->thread_info->cpu); } read_unlock(&tasklist_lock); }
|
这个模块应该插入到 do_fork 的偏移位置。
清单 5. pid 1508 和 1509 的结构体 thread_info 的输出
$tail -10 /var/log/messages
Jun 22 18:14:25 llm05 kernel: thread-info element status=0,flags=0, cpu=1 Jun 22 18:14:25 llm05 kernel: pid =5e4 task-info_ptr=f5948000 Jun 22 18:14:25 llm05 kernel: thread-info element status=0,flags=8, cpu=0 Jun 22 18:14:25 llm05 kernel: pid =5e5 task-info_ptr=f5eca000
|
启用奇妙的 SysRq 键
为了支持 SysRq 键,我们已经进行了编译。这样来启用它:
$echo 1 > /proc/sys/kernel/sysrq
现在,您可以使用 Alt+SysRq+W 在控制台上或者到 /var/log/messages 中去查看所有插入的内核探测器。
清单 6. /var/log/messages 显示出在 do_fork 插入了一个 Kprobe
Jun 23 10:24:48 linux-udp4749545uds kernel: SysRq : Show kprobes Jun 23 10:24:48 linux-udp4749545uds kernel: Jun 23 10:24:48 linux-udp4749545uds kernel: [<c011ea60>] do_fork+0x0/0x1de
|
使用 Kprobes 更好地进行调试
由于探测器事件处理器是作为系统断点中断处理器的扩展来运行,所以它们很少或者根本不依赖于系统 工具 —— 这样可以被植入到大部分不友好的环境中(从中断时间和任务时间到禁用的上下文间切换和支持 SMP 的代码路径)—— 都不会对系统性能带来负面影响。
使用 Kprobes 的好处有很多。不需要重新编译和重新引导内核就可以插入 printk。为了进行调试可以记录 处理器寄存器的日志,甚至进行修改 —— 不会干扰系统。类似地,同样可以无干扰地记录 Linux 内核数据结构的日志,甚至 进行修改。您甚至可以使用 Kprobes 调试 SMP 系统上的竞态条件 —— 避免了您自己重新编译和重新引导的所有 麻烦。您将发现内核调试比以往更为快速和简单。
参考资料
- 您可以参阅本文在 developerWorks 全球站点上的 英文原文.
- 在 Kprobes 主页可以找到更多信息、最新消息、README 和下载。 README 详尽地描述 了 kprobes 接口。
- Kprobes 开发自完整的 Dynamic Probes补丁。Dynamic Probes 使用 Kernel Hooks 来收集难以获得的诊断信息。
- v.2.5.26 版本的内核已经融入了对 Kprobes 的支持;参阅 Support for kernel probes( Kernel Traffic,2002 年 7 月 25 日)的声明以及简短评论。 KProbes 在 2.5.73 版本中增加了 out-of-line single-stepping。
- Kprobes 利用了 BIOS 中断 SysRq 键。 它可以成为一个 Magic SysRq 键, 用来实现防御间谍软件、不完全的重新引导、显示内存信息、杀死进程等等(多得多的)功能。 它适合用于调试; 不太适合用于生产环境中的机器,它会引发安全威胁。
- objdump 显示关于一个或多个对象文件的信息。要获得更多资料,请参阅 objdump 手册页 Linux 2.6 内核模块。
- Captain's Universe 曾经公布过一个关于 如何为内核 2.6 编译内核模块 的文档。
- 下载内核 Module Utilities for 2.6 的源文件;它可以取代当前新内核的 modutils。它是 Rusty Russell 的众多实用补丁 之一。
- 在 developerWorks Linux 专区 可以找到 更多为 Linux 开发者准备的参考资料。
- 在 Developer Bookstore Linux 区中订购 打折出售的 Linux 书籍。
- 通过 developerWorks Subscription 使用最新的 IBM 工具和中间件来开发和测试您的 Linux 应用程序:您可以从 WebSphere、DB2、 Lotus、Rational 和 Tivoli 得到 IBM 软件,以及一个可以在 12 个月内使用这些软件的许可,所有的花费都比您想像的要低。
- 从 developerWorks 的 Speed-start your Linux app 专区下载可以运行于 Linux 之上的经过挑选的 developerWorks Subscription 产品免费测试版本,包括 WebSphere Studio Site Developer、WebSphere SDK for Web services、WebSphere Application Server、DB2 Universal Database Personal Developers Edition、Tivoli Access Manager 和 Lotus Domino Server。要更快速地开始上手,请参阅针对各个产品的 how-to 文章和技术支持。
关于作者
Prasanna S. Panchamukhi 作为一名开发人员,在印度班加罗尔的 IBM Linux Technology Center 工作。 他当前的工作是改进 Linux 的各种调试工具。以前,他从事的是编写光纤通道设备驱动程序 和开发网络处理器应用程序,以及维护 UNIX 操作系统。可以通过 [email protected] 与 Prasanna 联系。
|
|