我们在 Kdump-内核崩溃转储服务中,介绍了kdump服务可以在系统崩溃时,生成崩溃转储文件vmcore,便于我们分析内核崩溃原因,那么,下面我们就来看下如何使用crash工具可以帮助我们分析系统崩溃原因。

crash工具,跟gdb很类似,它可以交互的分析运行中的内核,也可以分析由kdump、netdump、diskdump、xendump产生的core dump文件。

本文实验环境:

[root@yglocal ~]# cat /etc/redhat-release 
CentOS Linux release 7.6.1810 (Core) 
[root@yglocal ~]# uname -r
3.10.0-957.el7.x86_64


一、调试环境准备

使用crash工具分析vmcore,需要:

  • crash工具
  • 崩溃转储文件(vmcore)
  • 发生崩溃的内核映像文件(vmlinux),包含调试内核所需调试信息

一般系统在安装后在/boot目录下,也有个内核映像文件,vmlinuxz开头的文件,但是它是压缩过后的,无法完成调试工作,如下图:
在这里插入图片描述
所以我们需要下载带有完整调试信息的内核映像文件(编译时带-g选项),内核调试信息包kernel-debuginfo有两个:

  • kernel-debuginfo
  • kernel-debuginfo-common

对于centos系统,可以在http://debuginfo.centos.org/上下载到各发行版本所需的调试包。
对于centos7.x,安装对应内核版本的内核调试包,执行如下即可:

# wget http://debuginfo.centos.org/7/x86_64/kernel-debuginfo-common-x86_64-`uname -r`.rpm
# wget http://debuginfo.centos.org/7/x86_64/kernel-debuginfo-`uname -r`.rpm

注意:如果系统为centos6.x,则将debuginfo.centos.org/后面的7改成6即可。

对于oracle linux系统,可以在https://oss.oracle.com/上下载内核调试包,比如现在要找oracle linux6.6的:

# wget https://oss.oracle.com/ol6/debuginfo/kernel-uek-debuginfo-3.8.13-44.1.1.el6uek.x86_64.rpm
# wget https://oss.oracle.com/ol6/debuginfo/kernel-uek-debuginfo-common-3.8.13-44.1.1.el6uek.x86_64.rpm

下载完后,安装内核调试包:

rpm -ivh *.rpm

安装完成后,可以在/lib/debug/lib/modules/3.10.0-957.el7.x86_64/(或者是/usr/lib/…下面)目录下看到vmlinux内核映像文件:

[root@yglocal ~]# ll -th /lib/debug/lib/modules/3.10.0-957.el7.x86_64/
total 419M
drwxr-xr-x.  2 root root  119 Mar 26 13:13 vdso
drwxr-xr-x. 12 root root  128 Mar 26 13:13 kernel
-rwxr-xr-x.  2 root root 419M Nov  9  2018 vmlinux

再来安装crash工具,先查看下是否已安装

[root@yglocal ~]# rpm -q crash
crash-7.2.3-8.el7.x86_64

若没有安装过,则执行以下命令安装:

yum install crash


二、使用crash分析vmcore

分析vmcore文件,执行命令:

crash /lib/debug/lib/modules/3.10.0-957.el7.x86_64/vmlinux /var/crash/127.0.0.1-2020-04-04-14\:10\:45/vmcore

在这里插入图片描述
其中下面这些信息,就是导致系统崩溃的直接原因及进程相关信息:

 PANIC: "BUG: unable to handle kernel NULL pointer dereference at 000000000000000c"
    PID: 27005
COMMAND: "pickup"
   TASK: ffff997b388ae180  [THREAD_INFO: ffff997b37f64000]
    CPU: 1
  STATE: TASK_RUNNING (PANIC)

1、bt命令

backtrace打印内核栈回溯信息,bt pid 打印指定进程栈信息。
在这里插入图片描述
最重要的信息:[exception RIP: my_openat+36],指出发生异常的指令信息

RIP: ffffffffc07c5024,可以得知,发生crash的函数是my_openat中,偏移36字节处的指令。

这里,对应x86-64汇编,应用层下来的系统调用对应的6个参数存放的寄存器依次对应:rdi、rsi、rdx、rcx、r8、r9。对于多于6个参数的,仍存储在栈上。

2、log命令

打印vmcore所在的系统内核日志信息
在这里插入图片描述
可以分析出当时,内核启动的各项配置,内核最后崩溃时异常日志信息。

3、dis命令

dis -l (function+offset) 10 反汇编出指令所在代码开始,10行代码
在这里插入图片描述
幸运的话,反汇编出来会直接对应源码,我们这里只能看出执行mov 0x70(%rdi),%r13时系统崩溃。

4、mod命令

mod 查看当时内核加载的所有内核模块信息
在这里插入图片描述
重装加载进改内核模块:

crash> mod -s my_test_lkm /mnt/hgfs/test_ko/lkm-test05/my_test_lkm.ko
     MODULE       NAME                            SIZE  OBJECT FILE
ffffffffc07c7000  my_test_lkm                    12740  /mnt/hgfs/test_ko/lkm-test05/my_test_lkm.ko

5、sym命令

sym 转换指定符号为其虚拟地址,显示系统中符号信息

如上面bt打印的RIP: ffffffffc07c5024,使用sym转换查看系统符号信息
sym addr 通过虚拟地址,显示symbol和源码位置

crash> sym ffffffffc07c5024
ffffffffc07c5024 (t) my_openat+36 [my_test_lkm] /mnt/hgfs/test_ko/lkm-test05/my_lkm.c: 25

这时就可以看出对应到my_test_lkm模块的源码,/mnt/hgfs/test_ko/lkm-test05/my_lkm.c文件里第25行,查看代码可以发现:
在这里插入图片描述
原因:这种hook写法在centos8.x上的最新系统调用约定,是内核版本4.17及之后采用的调用约定。而我当前环境是centos7.6,内核版本为3.10。0,其调用约定并不是这样的,所以这样取参数是有问题的。

sym symbol 通过symbol,显示虚拟地址和源码位置:

crash> sym vfs_fstat
ffffffff81196f30 (T) vfs_fstat ../debug/kernel-3.8.13/linux-3.8.13-44.1.1.el6uek/fs/stat.c: 59

6、ps命令

ps 打印内核崩溃时,正常的进程信息
在这里插入图片描述
带 > 标识代表是活跃的进程,ps pid打印某指定进程的状态信息:

crash> ps 27005
   PID    PPID  CPU       TASK        ST  %MEM     VSZ    RSS  COMM
> 27005   7783   1  ffff997b388ae180  RU   0.2   91732   4124  pickup

查看指定进程的进程树(ps -p pid)

crash> ps -p 85151
PID: 0      TASK: ffffffff818b6420  CPU: 0   COMMAND: "swapper/0"
 PID: 1      TASK: ffff881f91dae040  CPU: 28  COMMAND: "init"
  PID: 14544  TASK: ffff881f8d7b05c0  CPU: 11  COMMAND: "init.tfa"
   PID: 85138  TASK: ffff880bab01a400  CPU: 8   COMMAND: "tfactl"
    PID: 85151  TASK: ffff880b7a728380  CPU: 17  COMMAND: "perl"

7、files命令

files pid 打印指定进程所打开的文件信息
在这里插入图片描述

8、vm命令

vm pid 打印某指定进程当时虚拟内存基本信息
在这里插入图片描述

9、task命令

task 查看当前进程或指定进程task_struct和thread_info的信息
在这里插入图片描述

10、kmem命令

kmen 查看当时系统内存使用信息

kmem [-f|-F|-c|-C|-i|-v|-V|-n|-z|-o|-h] [-p | -m member[,member]]

       [[-s|-S] [slab] [-I slab[,slab]]] [-g [flags]] [[-P] address]]

我们是要kmem -i查看:
在这里插入图片描述

11、struct命令

1、查看结构体成员变量,直接struct后面跟结构体名,比如我要看下内核中dentry结构体的定义,可以这样:

crash> struct dentry
struct dentry {
    unsigned int d_flags;
    seqcount_t d_seq;
    struct hlist_bl_node d_hash;
    struct dentry *d_parent;
    struct qstr d_name;
    struct inode *d_inode;
    unsigned char d_iname[32];
    unsigned int d_count;
    spinlock_t d_lock;
    const struct dentry_operations *d_op;
    struct super_block *d_sb;
    long unsigned int d_time;
    void *d_fsdata;
    struct list_head d_lru;
    union {
        struct list_head d_child;
        struct callback_head d_rcu;
    } d_u;
    struct list_head d_subdirs;
    struct hlist_node d_alias;
}
SIZE: 192

2、查看成员变量在结构体中的偏移量,struct加-o选项:

crash> struct dentry -o
struct dentry {
    [0] unsigned int d_flags;
    [4] seqcount_t d_seq;
    [8] struct hlist_bl_node d_hash;
   [24] struct dentry *d_parent;
   [32] struct qstr d_name;
   [48] struct inode *d_inode;
   [56] unsigned char d_iname[32];
   [88] unsigned int d_count;
   [92] spinlock_t d_lock;
   [96] const struct dentry_operations *d_op;
  [104] struct super_block *d_sb;
  [112] long unsigned int d_time;
  [120] void *d_fsdata;
  [128] struct list_head d_lru;
        union {
            struct list_head d_child;
            struct callback_head d_rcu;
  [144] } d_u;
  [160] struct list_head d_subdirs;
  [176] struct hlist_node d_alias;
}
SIZE: 192

crash> struct dentry.d_inode
struct dentry {
   [48] struct inode *d_inode;
}

3、查看内存中指定结构体的值,struct后跟指针地址

crash> struct dentry ffff881fbdc02c80
struct dentry {
  d_flags = 136, 
  d_seq = {
    sequence = 4
  }, 
  d_hash = {
    next = 0x0, 
    pprev = 0xffffc900031bd8a0
  }, 
  d_parent = 0xffff881fbdc02ec0, 
  d_name = {
    {
      {
        hash = 1819047278, 
        len = 4
      }, 
      hash_len = 18998916462
    }, 
    name = 0xffff881fbdc02cb8 "null"
  }, 
  d_inode = 0xffff881f9011f5d0, 
  d_iname = "null\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000", 
  d_count = 2974, 
  d_lock = {
    {
      rlock = {
        raw_lock = {
          {
            head_tail = 2290387076, 
            tickets = {
              head = 34948, 
              tail = 34948
            }
          }
        }
      }
    }
  }, 
  d_op = 0xffffffff815cce40, 
  d_sb = 0xffff881f9011e800, 
  d_time = 0, 
  d_fsdata = 0x0, 
  d_lru = {
    next = 0xffff881fbdc02d00, 
    prev = 0xffff881fbdc02d00
  }, 
  d_u = {
    d_child = {
      next = 0xffff881fbdc02dd0, 
      prev = 0xffff881fbdfe1f50
    }, 
    d_rcu = {
      next = 0xffff881fbdc02dd0, 
      func = 0xffff881fbdfe1f50
    }
  }, 
  d_subdirs = {
    next = 0xffff881fbdc02d20, 
    prev = 0xffff881fbdc02d20
  }, 
  d_alias = {
    next = 0x0, 
    pprev = 0xffff881f9011f6e8
  }
}

注:如果要查看二阶指针的值,可以通过rd命令需要先获取一级指针的值,然后再用struct 结构体名 + addr获取具体的值,比如:
在这里插入图片描述
下面将通过struct file**fd的地址,获取fd指针数组中各个file结构体成员地址(二阶指针存放的是file指针的地址,所以可以通过rd获取一级指针的地址;二阶指针+8,则指向下一个file结构体指针):
在这里插入图片描述
对比可以看出,这种方式获取的file指针跟files命令读取到的文件指针的值相同。

13、p命令

p命令可以用来打印出表达式或者变量的值

crash> p jiffies
jiffies = $7 = 4314305182
crash>
crash> p old_close_func
old_close_func = $5 = (close_t) 0xffffffff811ffd60 <SyS_close>
crash> p SyS_close
SyS_close = $6 = 
 {long (long)} 0xffffffff811ffd60 <SyS_close>
crash> sym SyS_close
ffffffff811ffd60 (T) SyS_close /usr/src/debug/kernel-3.10.0-693.el7/linux-3.10.0-693.el7.x86_64/fs/open.c: 1113
crash> 

14、查看某个命令使用方法及使用示例

使用help+某个命令,可以查看这个命令的使用方法及使用示例:
比如我们想看struct命令如何使用,可以执行 help struct命令:

crash> help struct

NAME
  struct - structure contents

SYNOPSIS
  struct struct_name[.member[,member]][-o][-l offset][-rfuxdp][address | symbol]
         [count | -c count]

DESCRIPTION
  This command displays either a structure definition, or a formatted display
  of the contents of a structure at a specified address.  When no address is
  specified, the structure definition is shown along with the structure size.
  A structure member may be appended to the structure name in order to limit
  the scope of the data displayed to that particular member; when no address
  is specified, the member's offset and definition are shown.

    struct_name  name of a C-code structure used by the kernel.
        .member  name of a structure member; to display multiple members of a
                 structure, use a comma-separated list of members.
             -o  show member offsets when displaying structure definitions; 
                 if used with an address or symbol argument, each member will
                 be preceded by its virtual address.
      -l offset  if the address argument is a pointer to a structure member that
                 is contained by the target data structure, typically a pointer
                 to an embedded list_head, the offset to the embedded member may
                 be entered in either of the following manners:
                   1. in "structure.member" format.
                   2. a number of bytes. 
             -r  raw dump of structure data.
             -f  address argument is a dumpfile offset.
             -u  address argument is a user virtual address in the current
                 context.
             -x  override default output format with hexadecimal format.
             -d  override default output format with decimal format.
             -p  if a structure member is a pointer value, show the member's
                 data type on the output line; and on the subsequent line(s),
                 dereference the pointer, display the pointer target's symbol
                 value in brackets if appropriate, and if possible, display the
                 target data; requires an address argument.
        address  hexadecimal address of a structure; if the address points
                 to an embedded list_head structure contained within the
                 target data structure, then the "-l" option must be used.
         symbol  symbolic reference to the address of a structure.
          count  count of structures to dump from an array of structures;
                 if used, this must be the last argument entered.
       -c count  "-c" is only required if "count" is not the last argument
                 entered or if a negative number is entered; if a negative
                 value is entered, the (positive) "count" structures that
                 lead up to and include the target structure will be displayed.

  Structure data, sizes, and member offsets are shown in the current output
  radix unless the -x or -d option is specified.

再比如,想查看ps命令在crash工具中怎么使用,可以help ps查看:

crash> help ps

NAME
  ps - display process status information

SYNOPSIS
  ps [-k|-u|-G][-s][-p|-c|-t|-l|-a|-g|-r] [pid | taskp | command] ...

DESCRIPTION
  This command displays process status for selected, or all, processes
  in the system.  If no arguments are entered, the process data is
  is displayed for all processes.  Specific processes may be selected
  by using the following identifier formats:

       pid  a process PID.
     taskp  a hexadecimal task_struct pointer.
   command  a command name.  If a command name is made up of letters that
            are all numerical values, precede the name string with a "\".
 
  The process list may be further restricted by the following options:

        -k  restrict the output to only kernel threads.
        -u  restrict the output to only user tasks.
        -G  display only the thread group leader in a thread group.
 
  The process identifier types may be mixed.  For each task, the following
  items are displayed:
 
    1. the process PID.
    2. the parent process PID.
    3. the CPU number that the task ran on last.
    4. the task_struct address or the kernel stack pointer of the process.
       (see -s option below)
    5. the task state (RU, IN, UN, ZO, ST, TR, DE, SW).
    6. the percentage of physical memory being used by this task.
    7. the virtual address size of this task in kilobytes.
    8. the resident set size of this task in kilobytes.
    9. the command name.
 
  The default output shows the task_struct address of each process under a
  column titled "TASK".  This can be changed to show the kernel stack 
  pointer under a column titled "KSTACKP".
 
       -s  replace the TASK column with the KSTACKP column.
 
  On SMP machines, the active task on each CPU will be highlighted by an
  angle bracket (">") preceding its information.
 
  Alternatively, information regarding parent-child relationships,
  per-task time usage data, argument/environment data, thread groups,
  or resource limits may be displayed:
 
       -p  display the parental hierarchy of selected, or all, tasks.

15、其它命令

可以通过help查看到如下:
在这里插入图片描述

如:可以使用rd memory读取内存内容;
struct命令显示结构体定义及指定地址的结构体内容;
irq查看中断信息;
vtop查看地址页表信息等等。

三、写在最后

如果我们确定是某个内核模块导致的问题,可以反汇编出该模块的源代码:

objdump -S -D my_test_lkm.ko > lkm.S

然后vim查看lkm.S文件,查看对应的源码,我们上面调试的bt信息中**[exception RIP: my_openat+36]**,也即是my_openat中偏移量为0x24的地方,如下图:
在这里插入图片描述
如果崩溃处对应有c代码的话,这样排查起来就简单多了。

最后,可以关注我的微信公众号 大胖聊编程 ,一起交流学习,共同进步。

参考资料:
[1]: Red_Hat_Enterprise_Linux-7-Kernel_Administration_Guide-en-US 7.5. ANALYZING A CORE DUMP

GitHub 加速计划 / li / linux-dash
6
1
下载
A beautiful web dashboard for Linux
最近提交(Master分支:3 个月前 )
186a802e added ecosystem file for PM2 4 年前
5def40a3 Add host customization support for the NodeJS version 4 年前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐