Linux内核Crash分析

Linux内核Crash分析

  在工作中经常会遇到一些内核crash的情况,本文就是根据内核出现crash后的打印信息,对其进行了分析,使用的内核版本为:Linux2.6.32。

       每一个进程的生命周期内,其生命周期的范围为几毫秒到几个月。一般都是和内核有交互,例如用户空间程序使用系统调用进入内核空间。这时使用的不再是用户空间的栈空间,使用对应的内核栈空间。对每一个进程来说,Linux内核都会把两个不同的数据结构紧凑的存放在一个单独为进程分配的存储空间中:一个是内核态的进程堆栈,另一个是紧挨进程描述符的数据结构thread_info,叫线程描述符。内核的堆栈大小一般为8KB,也就是8192个字节,占用两个页。在Linux-2.6.32内核中thread_info.h文件中有对内核堆栈的定义:


  1. #define THREAD_SIZE 8192

          在Linux内核中使用下面的联合结构体表示一个进程的线程描述符和内核栈,在内核中文件include/linux/sched.h。


  1. union thread_union {
  2. struct thread_info thread_info;
  3. unsigned long stack[THREAD_SIZE/sizeof(long)];
  4. };

          该结构是一个联合体,我们在C语言书上看到过关于union的解释,在在C Programming Language 一书中对于联合体是这么描述的:

1) 联合体是一个结构;

2) 它的所有成员相对于基地址的偏移量都为0;

3) 此结构空间要大到足够容纳最"宽"的成员;

4) 其对齐方式要适合其中所有的成员;

         通过上面的描述可知,thread_union结构体的大小为8192个字节。也就是stack数组的大小,类型是unsigned long类 型。由于联合体中的成员变量都是占用同一块内存区域,所以,在平时写代码时总有一个概念,对一个联合体的实例只能使用其中一个成员变量,否则会把原先变量 给覆盖掉,这句话如果正确的话,必须要有一个前提假设,成员占用的字节数相同,当成员所占的字节数不同时,只会覆盖相应的字节。对于thread_union联合体,我们是可以同时访问这两个成员,只要能够正确获取到两个成员变量的地址。

         在内核中的某一个进程使用了过多的栈空间时,内核栈就会溢出到thread_info部分,这将导致严重的问题(系统重启),例如,递归调用的层次太深;在函数内定义的数据结构太大。

图:进程中thread_info    task_struct和内核栈中的关系

         下面我们看一下thread_info的结构体:


  1. struct thread_info {
  2. unsigned long flags; /* 底层标志,*/
  3. int preempt_count; /* 0 => 可抢占, <0 => bug */
  4. mm_segment_t addr_limit; /* 进程地址空间 */
  5. struct task_struct *task; /*当前进程的task_struct指针 */
  6. struct exec_domain *exec_domain; /*执行区间 */
  7. __u32 cpu; /* 当前cpu */
  8. __u32 cpu_domain; /* cpu domain */
  9. struct cpu_context_save cpu_context; /* cpu context */
  10. __u32 syscall; /* syscall number */
  11. __u8 used_cp[16]; /* thread used copro */
  12. unsigned long tp_value;
  13.  
  14. struct crunch_state crunchstate;
  15.  
  16. union fp_state fpstate __attribute__((aligned(8)));
  17. union vfp_state vfpstate;
  18. #ifdef CONFIG_ARM_THUMBEE
  19. unsigned long thumbee_state; /* ThumbEE Handler Base register */
  20. #endif
  21. struct restart_block restart_block; /*用于实现信号机制*/
  22. };

 PS:(1)flag 用于保存各种特定的进程标志,最重要的两个是:TIF_SIGPENDING,如果进程有待处理的信号就置位,TIF_NEED_RESCHED表示进程应该需要调度器选择另一个进程替换本进程执行。

         结合上面的知识,看下当内核打印堆栈信息时,都打印了上面信息。下面的打印信息是工作中遇到的一种情况,打印了内核的堆栈信息,PC指针在dev_get_by_flags中,不能访问的内核虚地址为45685516,内核中一般可访问的地址都是以0xCXXXXXXX开头的地址。


  1. Unable to handle kernel paging request at virtual address 45685516
  2. pgd = c65a4000
  3. [45685516] *pgd=00000000
  4. Internal error: Oops: 1 [#1]
  5. last sysfs file: /sys/devices/form/tpm/cfg_l3/l3_rule_add
  6. Modules linked in: splic mmp(P)
  7. CPU: 0 Tainted: P (2.6.32.11 #42)
  8. PC is at dev_get_by_flags+0xfc/0x140
  9. LR is at dev_get_by_flags+0xe8/0x140
  10. pc : [<c06bee24>] lr : [<c06bee10>] psr: 20000013
  11. sp : c07e9c28 ip : 00000000 fp : c07e9c64
  12. r10: c6bcc560 r9 : c646a220 r8 : c66a0000
  13. r7 : c6a00000 r6 : c0204e56 r5 : 30687461 r4 : 45685516
  14. r3 : 00000000 r2 : 00000010 r1 : c0204e56 r0 : ffffffff
  15. Flags: nzCv IRQs on FIQs on Mode SVC_32 ISA ARM Segment kernel
  16. Control: 0005397f Table: 065a4000 DAC: 00000017
  17. Process swapper (pid: 0, stack limit = 0xc07e8270)
  18. Stack: (0xc07e9c28 to 0xc07ea000)
  19. 9c20: c0204e56 c6a00000 45685516 c69ffff0 c69ffff0 c69ffff0
  20. 9c40: c6a00000 30687461 c66a0000 c6a00000 00000007 c64b210c c07e9d24 c07e9c68
  21. 9c60: c071f764 c06bed38 c66a0000 c66a0000 c6a00000 c6a00000 c66a0000 c6a00000
  22. 9c80: c07e9cfc c07e9c90 c03350d4 c0334b2c 00000034 00000006 00000100 c64b2104
  23. 9ca0: 0000c4fb c0243ece c66a0000 c0beed04 c033436c c646a220 c07e9cf4 00000000
  24. 9cc0: c66a0000 00000003 c0bee8e8 c0beed04 c07e9d24 c07e9ce0 c06e4f5c 00004c68
  25. 9ce0: 00000000 faa9fea9 faa9fea9 00000000 00000000 c6bcc560 c0335138 c646a220
  26. 9d00: c66a0000 c64b2104 c085ffbc c66a0000 c0bee8e8 00000000 c07e9d54 c07e9d28
  27. 9d20: c071f9a0 c071ebc0 00000000 c071ebb0 80000000 00000007 c67fb460 c646a220
  28. 9d40: c0bee8c8 00000608 c07e9d94 c07e9d58 c002a100 c071f84c c0029bb8 80000000
  29. 9d60: c07e9d84 c0beee0c c0335138 c66a0000 c646a220 00000000 c4959800 c4959800
  30. 9d80: c67fb460 00000000 c07e9dc4 c07e9d98 c078f0f4 c0029bc8 00000000 c0029bb8
  31. 9da0: 80000000 c07e9dbc c6b8d340 c66a0520 00000000 c646a220 c07e9dec c07e9dc8
  32. 9dc0: c078f450 c078effc 00000000 c67fb460 c6b8d340 00000000 c67fb460 c64b20f2
  33. 9de0: c07e9e24 c07e9df0 c078fb60 c078f130 00000000 c078f120 80000000 c0029a94
  34. 9e00: 00000806 c6b8d340 c0bee818 00000001 00000000 c4959800 c07e9e64 c07e9e28
  35. 9e20: c002a030 c078f804 c64b2070 00000000 c64b2078 ffc45000 c64b20c2 c085c2dc
  36. 9e40: 00000000 c085c2c0 00000000 c0817398 00086c2e c085c2c4 c07e9e9c c07e9e68
  37. 9e60: c06c2684 c0029bc8 00000001 00000040 00000000 c085c2dc c085c2c0 00000001
  38. 9e80: 0000012c 00000040 c085c2d0 c0bee818 c07e9ed4 c07e9ea0 c00284e0 c06c2608
  39. 9ea0: bf00da5c 00086c30 00000000 00000001 c097e7d4 c07e8000 00000100 c08162d8
  40. 9ec0: 00000002 c097e7a0 c07e9f14 c07e9ed8 c00283d0 c0028478 56251311 00023c88
  41. 9ee0: c07e9f0c 00000003 c08187ac 00000018 00000000 01000000 c07ebc70 00023cbc
  42. 9f00: 56251311 00023c88 c07e9f24 c07e9f18 c03391e8 c0028348 c07e9f3c c07e9f28
  43. 9f20: c0028070 c03391b0 ffffffff 0000001f c07e9f94 c07e9f40 c002d4d0 c0028010
  44. 9f40: 00000000 00000001 c07e9f88 60000013 c07e8000 c07ebc78 c0868784 c07ebc70
  45. 9f60: 00023cbc 56251311 00023c88 c07e9f94 c07e9f98 c07e9f88 c025c3e4 c025c3f4
  46. 9f80: 60000013 ffffffff c07e9fb4 c07e9f98 c025c578 c025c3cc 00000000 c0981204
  47. 9fa0: c0025ca0 c0d01140 c07e9fc4 c07e9fb8 c0032094 c025c528 c07e9ff4 c07e9fc8
  48. 9fc0: c0008918 c0032048 c0008388 00000000 00000000 c0025ca0 00000000 00053975
  49. 9fe0: c0868834 c00260a4 00000000 c07e9ff8 00008034 c0008708 00000000 00000000
  50. Backtrace:
  51. [<c06bed28>] (dev_get_by_flags+0x0/0x140) from [<c071f764>] (arp_process+0xbb4/0xc74)
  52. r7:c64b210c r6:00000007 r5:c6a00000 r4:c66a0000

 

         (1)首先,看看这段堆栈信息是在内核中那个文件中打印出来的,在fault.c文件中,__do_kernel_fault函数,在上面的打印中Unable to handle kernel paging request at virtual address 45685516,该地址是内核空间不可访问的地址。


  1. static void __do_kernel_fault(struct mm_struct *mm, unsigned long addr, unsigned int fsr, struct pt_regs *regs)
  2. {
  3. /*
  4. * Are we prepared to handle this kernel fault?
  5. */
  6. if (fixup_exception(regs))
  7. return;
  8. /*
  9. * No handler, we'll have to terminate things with extreme prejudice.
  10. */
  11. bust_spinlocks(1);
  12. printk(KERN_ALERT
  13. "Unable to handle kernel %s at virtual address %08lx\n",
  14. (addr < PAGE_SIZE) ? "NULL pointer dereference" :"paging request", addr);
  15. show_pte(mm, addr);
  16. die("Oops", regs, fsr);
  17. bust_spinlocks(0);
  18. do_exit(SIGKILL);
  19. }

 (2) 对于下面的两个信息,在函数show_pte中进行了打印,下面的打印涉及到了页全局目录,页表的知识,暂时先不分析,后续补上。


  1. pgd = c65a4000
  2. [45685516] *pgd=00000000
  3.  
  4. void show_pte(struct mm_struct *mm, unsigned long addr)
  5. {
  6. pgd_t *pgd;
  7. if (!mm)
  8. mm = &init_mm;
  9. printk(KERN_ALERT "pgd = %p\n", mm->pgd);
  10. pgd = pgd_offset(mm, addr);
  11. printk(KERN_ALERT "[%08lx] *pgd=%08lx", addr, pgd_val(*pgd));
  12. ……………………
  13. }

 (3) die函数中调用在die函数中取得thread_info结构体的地址。


  1. struct thread_info *thread = current_thread_info();
  2.  
  3. static inline struct thread_info *current_thread_info(void){
  4. register unsigned long sp asm ("sp");
  5. return (struct thread_info *)(sp & ~(THREAD_SIZE - 1));
  6. }

 Sp: 0xc07e9c28    通过current_thread_info得到 thread_info的地址

(0xc07e9c28 & 0xffffe000) = 0xC07E8000(thread_info的地址,也就是栈底的地址)

(4)下面的打印信息在__die函数中打印


  1. Internal error: Oops: 1 [#1]
  2. last sysfs file: /sys/devices/form/tpm/cfg_l2/l2_rule_add
  3. Modules linked in: splic mmp(P)
  4. CPU: 0 Tainted: P (2.6.32.11 #42)
  5. PC is at dev_get_by_flags+0xfc/0x140
  6. LR is at dev_get_by_flags+0xe8/0x140
  7. pc : [<c06bee24>] lr : [<c06bee10>] psr: 20000013
  8. sp : c07e9c28 ip : 00000000 fp : c07e9c64
  9. r10: c6bcc560 r9 : c646a220 r8 : c66a0000
  10. r7 : c6a00000 r6 : c0204e56 r5 : 30687461 r4 : 30687461
  11. r3 : 00000000 r2 : 00000010 r1 : c0204e56 r0 : ffffffff
  12. Flags: nzCv IRQs on FIQs on Mode SVC_32 ISA ARM Segment kernel
  13. Control: 0005397f Table: 065a4000 DAC: 00000017
  14. Process swapper (pid: 0, stack limit = 0xc07e8270)
  15. Stack: (0xc07e9c28 to 0xc07ea000)

          函数的调用关系:die("Oops", regs, fsr);---à    __die(str, err, thread, regs);

下面是__die函数的定义:


  1. static void __die(const char *str, int err, struct thread_info *thread, struct pt_regs *regs){
  2. struct task_struct *tsk = thread->task;
  3. static int die_counter;
  4. /*Internal error: Oops: 1 [#1]*/
  5. printk(KERN_EMERG "Internal error: %s: %x [#%d]" S_PREEMPT S_SMP "\n",
  6. str, err, ++die_counter);
  7. /*last sysfs file: /sys/devices/form/tpm/cfg_l2/l2_rule_add*/
  8. sysfs_printk_last_file();
  9. /*内核中加载的模块信息Modules linked in: splic mmp(P) */
  10. print_modules();
  11. /*打印寄存器信息*/
  12. __show_regs(regs);
  13. /*Process swapper (pid: 0, stack limit = 0xc07e8270) tsk->comm task_struct结构体中的comm表示的是除去路径后的可执行文件名称,这里的swapper为idle进程,进程号为0,创建内核进程init;其中stack limit = 0xc07e8270 指向thread_info的结束地址。*/
  14. printk(KERN_EMERG "Process %.*s (pid: %d, stack limit = 0x%p)\n",
  15. TASK_COMM_LEN, tsk->comm, task_pid_nr(tsk), thread + 1);
  16. /* dump_mem 函数打印从栈顶到当前sp之间的内容*/
  17. if (!user_mode(regs) || in_interrupt()) {
  18. dump_mem(KERN_EMERG, "Stack: ", regs->ARM_sp, THREAD_SIZE + (unsigned long)task_stack_page(tsk));
  19. dump_backtrace(regs, tsk);
  20. dump_instr(KERN_EMERG, regs);
  21. }
  22. }

          在上面的函数中,主要使用了thread_info,task_struct,sp之间的指向关系。task_struct结构体的成员stack是栈底,也是对应thread_info结构体的地址。堆栈数据是从栈底+8K的地方开始向下存的。SP指向的是当前的栈顶。(unsigned long)task_stack_page(tsk),

#define task_stack_page(task)        ((task)->stack) ,该宏根据task_struct得到栈底,也就是thread_info地址。

#define task_thread_info(task)       ((struct thread_info *)(task)->stack),该宏根据task_struct得到thread_info指针。

(5)dump_backtrace函数

         该函数用于打印函数的调用关系。Fp为帧指针,用于追溯程序的方式,方向跟踪调用函数。该函数主要是fp进行检查,看看能否进行backtrace,如果可以就调用汇编的c_backtrace,在arch/arm/lib/backtrace.S函数中。


  1. static void dump_backtrace(struct pt_regs *regs, struct task_struct *tsk)
  2. {
  3. unsigned int fp, mode;
  4. int ok = 1;
  5. printk("Backtrace: ");
  6. if (!tsk)
  7. tsk = current;
  8. if (regs) {
  9. fp = regs->ARM_fp;
  10. mode = processor_mode(regs);
  11. } else if (tsk != current) {
  12. fp = thread_saved_fp(tsk);
  13. mode = 0x10;
  14. } else {
  15. asm("mov %0, fp" : "=r" (fp) : : "cc");
  16. mode = 0x10;
  17. }
  18. if (!fp) {
  19. printk("no frame pointer");
  20. ok = 0;
  21. } else if (verify_stack(fp)) {
  22. printk("invalid frame pointer 0x%08x", fp);
  23. ok = 0;
  24. } else if (fp < (unsigned long)end_of_stack(tsk))
  25. printk("frame pointer underflow");
  26. printk("\n");
  27. if (ok)
  28. c_backtrace(fp, mode);
  29. }

 (6)dump_instr

根据PC指针和指令mode, 打印出当前执行的指令码

Code: 0a000008 e5944000 e2545000 0a000005 (e4153010)

内核中函数的调用关系

原文发布时间:2014-07-28

本文来自云栖合作伙伴“linux中国”

时间: 2024-10-03 11:54:17

Linux内核Crash分析的相关文章

《Linux内核Makefile分析》之 auto.conf, auto.conf.cmd, autoconf.h【转】

转自:http://blog.sina.com.cn/s/blog_87c063060101l25y.html 转载:http://blog.csdn.net/lcw_202/article/details/6661364     在编译构建性目标时(如 make vmlinux),顶层 Makefile 的 $(dot-config) 变量值为 1 . 在顶层 Makefile 的 497-504 行看到: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

linux内核md源代码解读 十三 raid5重试读

上节我们讲到条块内读失败,在回调函数raid5_align_endio中将请求加入阵列重试链表,在唤醒raid5d线程之后,raid5d线程将该请求调用retry_aligned_read函数进行重试读: 4539static int retry_aligned_read(struct r5conf *conf, struct bio *raid_bio) 4540{ 4541 /* We may not be able to submit a whole bio at once as ther

linux内核md源代码解读 十二 raid读写

我们都知道,对一个linux块设备来说,都有一个对应的请求队列.注册在这个请求队列上的请求就是该块设备的请求入口.对于raid来说,分配struct mddev时就已经设置好了,在函数md_alloc中有这样的代码: 4846 blk_queue_make_request(mddev->queue, md_make_request); 4847 blk_set_stacking_limits(&mddev->queue->limits); 虽然全国的PM一直保持着稳健的增长,但丝

linux内核md源代码解读 十一 raid5d

正是有了上一篇的读写基础,我们才开始看raid5d的代码.raid5d不是读写的入口,也不是读写处理的地方,只是简简单单的中转站或者叫做交通枢纽.这个枢纽具有制高点的作用,就像美国在新加坡的基地,直接就控制了太平洋和印度洋的交通枢纽. 4626 /* 4627 * This is our raid5 kernel thread. 4628 * 4629 * We scan the hash table for stripes which can be handled now. 4630 * Du

linux内核md源代码解读 十 raid5数据流之同步数据流程

上一节讲到在raid5的同步函数sync_request中炸土豆片是通过handle_stripe来进行的.从最初的创建阵列,到申请各种资源,建立每个阵列的personality,所有的一切都是为了迎接数据流而作的准备.就像我们寒窗苦读就是为了上大学一样.数据流的过程就像大学校园一样丰富多彩并且富有挑战性,但只要跨过了这道坎,内核代码将不再神秘,剩下的问题只是时间而已. 首先看handle_stripe究竟把我们的土豆片带往何处: 3379 static void handle_stripe(s

linux内核md源代码解读 八 阵列同步二:同步过程

在上一小节里讲到启动同步线程: 7824 mddev->sync_thread = md_register_thread(md_do_sync, 7825 mddev, 7826 "resync"); md_register_thread函数如下: 6697 struct md_thread *md_register_thread(void (*run) (struct mddev *), struct mddev *mddev, 6698 const char *name) 6

linux内核md源代码解读 七 阵列同步一 :介绍阵列同步

阵列同步在md_do_sync,那么入口在哪里呢?就是说阵列同步触发点在哪里呢?听说过md_check_recovery吧,但这还不是同步的入口点.那raid5d函数是入口点吧?如果要认真分析起来还不算是. 真正的同步入口点在do_md_run函数,就是在运行阵列run函数之后,有这么一行: 5171         md_wakeup_thread(mddev->thread); 是这一行把raid5d唤醒的,raid5d函数如下: 4823 static void raid5d(struct

linux内核md源代码解读 四 命令字RUN_ARRAY的处理过程

运行阵列意味着阵列经历从无到有,建立了作为一个raid应有的属性(如同步重建),并为随后的读写做好的铺垫.那么运行阵列的时候到底做了哪些事情,让原来的磁盘像变形金刚一样组成一个新的巨无霸.现在就来看阵列运行处理流程: 5158 static int do_md_run(struct mddev *mddev) 5159 { 5160 int err; 5161 5162 err = md_run(mddev); 5163 if (err) 5164 goto out; 5165 err = bi

linux内核md源代码解读 三 阵列创建的过程

这一节我们阅读阵列的创建过程. 按照常理出牌,我们到ioctl中找阵列创建命令,md对应的ioctl函数是md_ioctl,当找对应的cmd命令字时,却完全没有类似CREATE_ARRAY的命令,那么就说明md设备并不是通过ioctl函数来创建的.其实如果我们仔细阅读一下md_ioctl函数的原型就会发现其实创建md设备根本就不在这个地方,函数原型如下: 6303 static int md_ioctl(struct block_device *bdev, fmode_t mode, 6304