Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1306400
  • 博文数量: 860
  • 博客积分: 425
  • 博客等级: 下士
  • 技术积分: 1464
  • 用 户 组: 普通用户
  • 注册时间: 2011-08-20 19:57
个人简介

对技术执着

文章分类

全部博文(860)

文章存档

2019年(16)

2018年(12)

2015年(732)

2013年(85)

2012年(15)

我的朋友

分类: LINUX

2015-03-02 18:07:21

原文地址:内核异常分析方法 作者:humjb_1983


=============================================================
 内核异常分析方法
=============================================================

 

-------------------------------------------------------------
1 总体方法
-------------------------------------------------------------


1.1 确定出错函数地址

1.2 确定出错地址在函数中的偏移


-------------------------------------------------------------
2 oops信息(由die函数产生)
-------------------------------------------------------------


/* This is gone through when something in the kernel
 * has done something bad and is about to be terminated.
*/
void die(const char * str, struct pt_regs * regs, long err)
{
  static struct {
     spinlock_t lock;
     u32 lock_owner;
     int lock_owner_depth;
  } die = {
     .lock =     SPIN_LOCK_UNLOCKED,
     .lock_owner =    -1,
     .lock_owner_depth = 0
  };
  static int die_counter;
  unsigned long flags;
  oops_enter();
  if (die.lock_owner != raw_smp_processor_id()) {
     console_verbose();
     spin_lock_irqsave(&die.lock, flags);
     die.lock_owner = smp_processor_id();
     die.lock_owner_depth = 0;
     bust_spinlocks(1);
  }
  else
     local_save_flags(flags);
  if (++die.lock_owner_depth < 3) {
     int nl = 0;
     unsigned long esp;
     unsigned short ss;
     handle_BUG(regs); //对应oops信息的第一二行,指出出错位置所在的文件,以及行号,有了这个信息,就可以直接找到c源代码
     printk(KERN_EMERG "%s: %04lx [#%d]\n", str, err & 0xffff, ++die_counter);//这里的str对应invalid opcode,是在调用die时传入的值,表示错误类型,err表示这类型具体的错误号,die_counter表示调用die的次数
#ifdef CONFIG_PREEMPT
     printk(KERN_EMERG "PREEMPT "); //抢占版本内核
     nl = 1;
#endif
#ifdef CONFIG_SMP
     if (!nl)
       printk(KERN_EMERG);
     printk("SMP ");          //多核版本内核
     nl = 1;
#endif
#ifdef CONFIG_DEBUG_PAGEALLOC
     if (!nl)
       printk(KERN_EMERG);
     printk("DEBUG_PAGEALLOC");
     nl = 1;
#endif
     if (nl)
       printk("\n");
#ifdef CONFIG_SYSFS
     printk(KERN_ALERT "last sysfs file: %s\n", last_sysfs_file);
#endif
     if (notify_die(DIE_OOPS, str, regs, err,
            current->thread.trap_no, SIGSEGV) !=
          NOTIFY_STOP) {
       show_registers(regs); //打印了图中的大部分信息
       /* Executive summary in case the oops scrolled away */
       esp = (unsigned long) (®s->esp);
       savesegment(ss, ss);
       if (user_mode(regs)) {
          esp = regs->esp;
          ss = regs->xss & 0xffff;
       }
       printk(KERN_EMERG "EIP: [<%08lx>] ", regs->eip);
       print_symbol("%s", regs->eip);
       printk(" SS:ESP %04x:%08lx\n", ss, esp);  //这三行打印最后一行
     }
     else
       regs = NULL;
  } else
     printk(KERN_EMERG "Recursive die() failure, output suppressed\n");
  bust_spinlocks(0);
  die.lock_owner = -1;
  spin_unlock_irqrestore(&die.lock, flags);
  if (!regs)
     return;
  if (kexec_should_crash(current))
     crash_kexec(regs);
  if (in_interrupt())
     panic("Fatal exception in interrupt");
  if (panic_on_oops)
     panic("Fatal exception");
  oops_exit();
  do_exit(SIGSEGV);
}


void show_registers(struct pt_regs *regs)
{
  int i;
  int in_kernel = 1;
  unsigned long esp;
  unsigned short ss;

  esp = (unsigned long) (®s->esp);
  savesegment(ss, ss);
  if (user_mode_vm(regs)) {
     in_kernel = 0;
     esp = regs->esp;
     ss = regs->xss & 0xffff;
  }
  print_modules();
  printk(KERN_EMERG "CPU:    %d\nEIP:    %04x:[<%08lx>]    %s VLI\n"
       "EFLAGS: %08lx   (%s %.*s) \n",
     smp_processor_id(), 0xffff & regs->xcs, regs->eip,
     print_tainted(), regs->eflags, system_utsname.release,
     (int)strcspn(system_utsname.version, " "),
     system_utsname.version);
  print_symbol(KERN_EMERG "EIP is at %s\n", regs->eip);//将eip的值转换成符号加偏移的形式表现出来,需要内核配置CONFIG_KALLSYMS
选项,以将地址转换成符号。
  printk(KERN_EMERG "eax: %08lx   ebx: %08lx   ecx: %08lx   edx: %08lx\n",
     regs->eax, regs->ebx, regs->ecx, regs->edx);
  printk(KERN_EMERG "esi: %08lx   edi: %08lx   ebp: %08lx   esp: %08lx\n",
     regs->esi, regs->edi, regs->ebp, esp);
  printk(KERN_EMERG "ds: %04x   es: %04x   ss: %04x\n",
     regs->xds & 0xffff, regs->xes & 0xffff, ss);
  printk(KERN_EMERG "Process %.*s (pid: %d, ti=%p task=%p task.ti=%p)",
     TASK_COMM_LEN, current->comm, current->pid,
     current_thread_info(), current, current->thread_info);
  /*
   * When in-kernel, we also print out the stack and code at the
   * time of the fault..
   */
  if (in_kernel) {
     u8 __user *eip;

     printk("\n" KERN_EMERG "Stack: ");
     show_stack_log_lvl(NULL, regs, (unsigned long *)esp, KERN_EMERG);

     printk(KERN_EMERG "Code: ");

     eip = (u8 __user *)regs->eip - 43;
     for (i = 0; i < 64; i++, eip++) {
       unsigned char c;

       if (eip < (u8 __user *)PAGE_OFFSET || __get_user(c, eip)) {
          printk(" Bad EIP value.");
          break;
       }
       if (eip == (u8 __user *)regs->eip)
          printk("<%02x> ", c);
       else
          printk("%02x ", c);
     }
  }
  printk("\n");

 


2.1 ------------[ cut here ]------------
kernel BUG at /home/USS/src/COMM/linux-2.6.18.i386/lib/list_debug.c:70!      /*这两行由handle_BUG函数打印,指示出错代码所在文件以及行号,有个这个信息就可以直接查看源代码,但这个信息并不总是可以得到*/
invalid opcode: 0000 [#2]       
/*invalid opcode表示错误类型,0000表示此类型的具体错误号,#2表示调用die的次数*/
SMP                                           //如果内核是多核版本,则打印SMP
last sysfs file: /class/mstuio/mstuio/dev
/*以下蓝色信息由show_registers()函数打印*/
Modules linked in: datalink mstuio e1000e e100                   //print_modules()
CPU:    1                                                                                    //cpu序号,从0开始
EIP:    0060:[]    Not tainted VLI                      
/*EIP,0060表示xcs寄存器值,表示eip的值, Not tainted 由print_tainted()打印*/
EFLAGS: 00013092   (2.6.18-prep #3)                                  //EFLAGS是eflags寄存器的值
EIP is at list_del+0x38/0x5c                                                 /*这里的EIP是由print_symbol将值转换成符号加偏移的形式打印,0x38为偏移,0x5c为list_del函数的长度*/
eax: 00000048   ebx: c363f738   ecx: 00003046   edx: 00003000
esi: 00003202   edi: c0881b00   ebp: c363f720   esp: ef041b38
ds: 007b   es: 007b   ss: 0068
Process FS_dev_rw1 (pid: 2863, ti=ef040000 task=c3a10c30 task.ti=ef040000)
Stack: c07fcedb c363f738 00200200 c0881c00 c0455126 c381b8c0 00000003 00000000
       000200d2 c0882da8 00000000 00000001 3274b0af 00000001 3274b2af c0882da8
       000200d2 c32280e0 f70373c0 c0455354 00000044 c0882da8 00000000 000200d2
Call Trace:       //堆栈内容
 [] show_trace_log_lvl+0x1a/0x2f
 [] show_stack_log_lvl+0x9b/0xa3
 [] show_registers+0xa3/0x1df
 [] die+0x11f/0x200
 [] do_page_fault+0x533/0x61a
 [] error_code+0x72/0x78
 [] __unlink_module+0xb/0xf
 [] do_stop+0xb8/0x108
 [] kthread+0x3b/0x63
 [] kernel_thread_helper+0x7/0x10
              =======================

Code: 6b c0 e8 2e 7e f6 ff e8 d1 16 f2 ff b8 01 00 00 00 e8 aa 1c f4 ff 89 d8 83 c4 10 5b 5d c3 90 90 90 55 89 e5 53 83 ec 0c 8b 48 04 <8b> 11 39 c2 74 18 89 54 24 08 89 44 24 04 c7 04 24 be 32 6b c0
/*code段总长度为64,为发生oops时eip处的指令码,前面有43个指令码,所以应该在第44个位置上,
  *后面还有20个指令码
  */
EIP: [] list_del+0x38/0x5c SS:ESP 0068:ef041b38   
/*这是由die函数打印,与show_registers()中的方法一致。要将地址转换成符号加偏移的表示方法,
  *内核中需要配置CONFIG_KALLSYMS选项,以便在内核映像中包含符号信息,以将地址转换成符号
  */

 

-------------------------------------------------------------
3 找到对应c代码的方法
-------------------------------------------------------------


3.1 有debuginfo时

配置了CONFIG_DEBUG_INFO选项,产生的vmlinux文件包含debuginfo信息。


3.1.1 使用符号地址和偏移地址计算出EIP

3.1.1.1 在编译内核生成的System.map中查找到符号地址

3.1.1.2 符号起始地址加上偏移地址得到EIP地址

3.1.2 直接使用EIP数字地址

3.1.2.1 在gdb中使用 b *0xc040122c ,即可得到指令所在文件以及行号

(gdb) b *0xc040122c           
Breakpoint 1 at 0xc040122c: file init/main.c, line 756.
(gdb) 


3.1.2.2 在gdb中使用  l *0xc040122c,即可得到c语言中对应语句行

(gdb) l *0xc040122c
0xc040122c is in init_post (init/main.c:756).
751  /* This is a non __init function. Force it to be noinline otherwise gcc
752   * makes it inline to init() and it becomes part of init.text section
753   */
754  static int noinline init_post(void)
755  {
756    free_initmem();
757    unlock_kernel();
758    mark_rodata_ro();
759    system_state = SYSTEM_RUNNING;
760    numa_default_policy();
(gdb) 


3.1.3 使用符号加偏移地址

3.1.3.1 在gdb中使用b *list_del+0x3c,即可得到指令所在文件以及行号

(gdb) b *list_del+0x3c
Breakpoint 2 at 0xc04eebfc: file lib/list_debug.c, line 70.


3.1.3.2 在gdb中使用  l *list_del+0x3c,即可得到c语言中对应语句行

(gdb) l *list_del+0x3c
0xc04eebfc is in list_del (lib/list_debug.c:70).
65     BUG();
66   }
67   if (unlikely(entry->next->prev != entry)) {
68     printk(KERN_ERR "list_del corruption. next->prev should be %p, but was %p\n",
69        entry, entry->next->prev);
70     BUG();
71   }
72   __list_del(entry->prev, entry->next);
73   entry->next = LIST_POISON1;
74   entry->prev = LIST_POISON2;
(gdb) 


3.2 无debuginfo时

没有配置CONFIG_DEBUG_INFO以产生包含调试信息的vmlinux文件时,


3.2.1 只有vmlinuz文件,不能反汇编

3.2.1.1 将code行的指令反汇编成汇编语句

注意,Oops中的Code:行,会把导致Oops的第一条指令,也就是EIP的值的第一个字节, 用尖括号<>括起来。 但是,有些体系结构(例如常见的x86)指令是不等长的(不一样的指令可能有不一样的长度),所以要不断的尝试(trial-and-error)。


3.2.1.1.1  Linus通常使用一个小程序,类似这样:
const char array[] = "\xnn\xnn\xnn...";
int main(int argc, char *argv[])
{
     printf("%p\n", array);
     *(int *)0 = 0;  //为什么要引发一个异常呢?方便gdb停下
}


/* 注意, array一共有从array[0]到array[63]这65个元素, 其中出错的那个操作码<8b> == arry[43] */
#include
#include
const char array[]
="\x6b\xc0\xe8\x2e\x7e\xf6\xff\xe8\xd1\x16\xf2\xff\xb8\x01\x00\x00\x00\xe8\xaa\x1c\xf4\xff\x89\xd8\x83\xc4\x10\x5b\x5d\xc3\x90\x90\x90\x55\x89\xe5\x53\x83\xec\x0c\x8b\x48\x04\x8b\x11\x39\xc2\x74\x18\x89\x54\x24\x08\x89\x44\x24\x04\xc7\x04\x24\xbe\x32\x6b\xc0";
int main(int argc, char *argv[])
{
       printf("%p\n", array);
       *(int *)0 = 0;
}
 
使用方法:
1, objdump -D hello
    -D选项会将所文件的所有节都反汇编
080484c0 ://array数组的反汇编结果
 80484c0: 6b c0 e8              imul   $0xffffffe8,%eax,%eax
 80484c3: 2e 7e f6              jle,pn 80484bc <__dso_handle+0x14>
 80484c6: ff e8                 ljmp   *
 80484c8: d1 16                 rcll   (%esi)
 80484ca: f2 ff                 repnz (bad) 
 80484cc: b8 01 00 00 00        mov    $0x1,%eax
 80484d1: e8 aa 1c f4 ff        call   7f8a180 <_init-0xbe0d0>
 80484d6: 89 d8                 mov    %ebx,%eax
 80484d8: 83 c4 10              add    $0x10,%esp
 80484db: 5b                    pop    %ebx
 80484dc: 5d                    pop    %ebp
 80484dd: c3                    ret    
 80484de: 90                    nop    
 80484df: 90                    nop    
 80484e0: 90                    nop    
 80484e1: 55                    push   %ebp
 80484e2: 89 e5                 mov    %esp,%ebp
 80484e4: 53                    push   %ebx
 80484e5: 83 ec 0c              sub    $0xc,%esp
 80484e8: 8b 48 04              mov    0x4(%eax),%ecx
 80484eb: 8b 11                 mov    (%ecx),%edx
 80484ed: 39 c2                 cmp    %eax,%edx
 80484ef: 74 18                 je     8048509
 80484f1: 89 54 24 08           mov    %edx,0x8(%esp)
 80484f5: 89 44 24 04           mov    %eax,0x4(%esp)
 80484f9: c7 04 24 be 32 6b 00  movl   $0x6b32be,(%esp)
 8048500: 00 25 70 2c 20 6c     add    %ah,0x6c202c70
 8048506: 65 6e                 outsb  %gs:(%esi),(%dx)
 8048508: 3a                    .byte 0x3a
 8048509: 25                    .byte 0x25
 804850a: 64 0a 00              or     %fs:(%eax),%al
2,           用gcc -g编译,在gdb里运行它:

                [arc在dhcp-cbjs05-218-251 ~]$ gdb hello
                GNU gdb Fedora (6.8-1.fc9)
                Copyright (C) 2008 Free Software Foundation, Inc.
                License GPLv3+: GNU GPL version 3 or later
                This is free software: you are free to change and
 redistribute it.
                There is NO WARRANTY, to the extent permitted by law.
 Type "show copying"
                and "show warranty" for details.
                This GDB was configured as "x86_64-redhat-linux-gnu"...
                (no debugging symbols found)
                (gdb) r
                Starting program: /home/arc/hello
                0x80484e0   //打印的是array的地址

                Program received signal SIGSEGV, Segmentation fault.
/*不产生这个异常也是可以直接反汇编这个数组的,而且数组的位置也是固定的,重开一个gdb也是固定的,所以没必要非产生异常*/

           注意,这时候就可以反汇编0x80484e0这个地址:

                (gdb) disassemble 0x80484e0
                Dump of assembler code for function array:
                0x080484e0 :   imul   $0xffffffe8,%eax,%eax
                0x080484e3 :   jle,pn 0x80484dc <__dso_handle+20>
                0x080484e6 :   ljmp   *
                0x080484e8 :   rcll   (%esi)
                0x080484ea :  repnz (bad)
                0x080484ec :  mov    $0x1,%eax
                0x080484f1 :  call   0x7f8a1a0
                0x080484f6 :  mov    %ebx,%eax
                0x080484f8 :  add    $0x10,%esp
                0x080484fb :  pop    %ebx
                0x080484fc :  pop    %ebp
                0x080484fd :  ret
                0x080484fe :  nop
                0x080484ff :  nop
                0x08048500 :  nop
                0x08048501 :  push   %ebp
                0x08048502 :  mov    %esp,%ebp
                0x08048504 :  push   %ebx
                0x08048505 :  sub    $0xc,%esp
                0x08048508 :  mov    0x4(%eax),%ecx
                0x0804850b :  mov    (%ecx),%edx
                0x0804850d :  cmp    %eax,%edx
                0x0804850f :  je     0x8048529
                0x08048511 :  mov    %edx,0x8(%esp)
                0x08048515 :  mov    %eax,0x4(%esp)
                0x08048519 :  movl   $0xc06b32be,(%esp)
                0x08048520 :  add    %ah,0xa70
                End of assembler dump.
                (gdb)

          OK, 现在你知道出错的那条指令是array[43],也就是mov
 (%ecx),%edx,也就是说,(%ecx)指向了一个错误内存地址。

 


3.2.1.1.2  利用内核源代码中scripts/decodecode脚本

[root@net-test2 ~]# ./decode.sh < oops.txt 
Code: 53 68 68 ce 7f c0 e8 ca 83 ea ff 0f 0b 41 00 a5 ce 7f c0 83 c4 0c 8b 03 8b 40 04 39 d8 74 17 50 53 68 db ce 7f c0 e8 aa 83 ea ff <0f> 0b 46 00 a5 ce 7f c0 83 c4 0c 8b 13 8b 43 04 89 42 04 89 10
All code
========
   0:  53                     push   %ebx
   1:  68 68 ce 7f c0         push   $0xc07fce68
   6:  e8 ca 83 ea ff        call   0xffea83d5
   b:* 0f 0b                 ud2a        <-- trapping instruction
   d:  41                    inc    %ecx
   e:  00 a5 ce 7f c0 83     add    %ah,0x83c07fce(%ebp)
  14:  c4 0c 8b              les    (%ebx,%ecx,4),%ecx
  17:  03 8b 40 04 39 d8     add    0xd8390440(%ebx),%ecx
  1d:  74 17                 je     0x36
  1f:  50                    push   %eax
  20:  53                    push   %ebx
  21:  68 db ce 7f c0        push   $0xc07fcedb
  26:  e8 aa 83 ea ff        call   0xffea83d5
  2b:* 0f 0b                 ud2a        <-- trapping instruction
  2d:  46                    inc    %esi
  2e:  00 a5 ce 7f c0 83     add    %ah,0x83c07fce(%ebp)
  34:  c4 0c 8b              les    (%ebx,%ecx,4),%ecx
  37:  13 8b 43 04 89 42     adc    0x42890443(%ebx),%ecx
  3d:  04 89                 add    $0x89,%al
  3f:  10                    .byte 0x10

Code starting with the faulting instruction
===========================================
   0:  0f 0b                 ud2a   
   2:  46                    inc    %esi
   3:  00 a5 ce 7f c0 83     add    %ah,0x83c07fce(%ebp)
   9:  c4 0c 8b              les    (%ebx,%ecx,4),%ecx
   c:  13 8b 43 04 89 42     adc    0x42890443(%ebx),%ecx
  12:  04 89                 add    $0x89,%al
  14:  10                    .byte 0x10
[root@net-test2 ~]# 


3.2.1.2 汇编c文件为汇编文件

 为了使汇编代码和C代码更好的对应起来, Linux内核的Kbuild子系统提供了这样一个功能: 任何一个C文件都可以单独编译成汇编文件,例如:

 make path/to/the/sourcefile.s

 例如我想把kernel/sched.c编译成汇编,那么:

 make kernel/sched.s V=1

 或者:

 make kernel/sched.lst V=1
 


3.2.1.1 比较两者的汇编指令,找到出错的源代码文件

3.2.2 有vmlinux-stripped文件,包含符号信息
但不包含调试信息

objdump -S vmlinux-stripped 将反汇编出vmlinux-stripped文件,在结果中查找eip值,可以找到其所在位置属于哪个函数

对于-g选项编译出来的程序,objdump -Sl file 可以列出汇编语句对应的文件以及在文件中的行号。
但是对vmlinux没用


3.2.2.1 可以找到eip属于哪个函数

3.2.1 再根据汇编代码,确定出错的c代码行


-------------------------------------------------------------
4 主要异常
-------------------------------------------------------------


4.1 页异常


-------------------------------------------------------------
5 参考资料
-------------------------------------------------------------


5.1 oops-tracing.txt

5.2 Crash工具使用


-------------------------------------------------------------
6 错误类型
-------------------------------------------------------------


6.1 invalid opcode

操作码错误,说明编译出来的指令有问题.
 


6.1.1 invalid_op

6号异常的处理函数,最终调用do_invalid_op->do_trap->die


6.2 oops

内核异常问题


6.2.1 do_page_fault

6.3 double fault

6.3.1 do_double_fault

6.4 iret exception

6.4.1 do_iret_error

6.5 int3

6.5.1 do_int3


-------------------------------------------------------------
7 内核配置选项
-------------------------------------------------------------


CONFIG_KALLSYMS:  没有配置,CONFIG_DEBUG_KERNEL 没有配置,CONFIG_DEBUG_INFO没有配置
-rwxr-xr-x  1 root root 5071204 Nov  2 20:48 vmlinux
-rwxr-xr-x  1 root root 4257676 Nov  2 20:48 vmlinux-stripped
-rw-r--r--  1 root root 1986218 Nov  2 20:48 vmlinuz

CONFIG_KALLSYMS:  配置,CONFIG_DEBUG_KERNEL 没有配置,CONFIG_DEBUG_INFO没有配置
-rwxr-xr-x  1 root root 5361573 Nov  2 22:14 vmlinux
-rwxr-xr-x  1 root root 4547256 Nov  2 22:14 vmlinux-stripped
-rw-r--r--  1 root root 2110703 Nov  2 22:14 vmlinuz

CONFIG_KALLSYMS:  配置,CONFIG_DEBUG_KERNEL 配置,CONFIG_DEBUG_INFO 配置
-rwxr-xr-x  1 root root 63701342 Nov  2 22:51 vmlinux
-rwxr-xr-x  1 root root  4548572 Nov  2 22:51 vmlinux-stripped
-rw-r--r--  1 root root  2112617 Nov  2 22:51 vmlinuz

可见CONFIG_KALLSYMS配置的是内核映像中包含符号信息
CONFIG_DEBUG_INFO配置是的内核映像中包含调试信息,当然也有符号信息

阅读(960) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~