术语介绍
生产内核(production kernel):产品或者线上服务器当前运行的内核。
捕获内核(capture kernel):系统崩溃时,使用kexec启动的内核,该内核用于捕获生产内核当前内存中的运行状态和数据信息。
Kdump简介
当系统崩溃时,kdump利用kexec启动第二个内核,这个内核驻留在宿主系统内存保留区中,而且宿主内核无法访问它。第二个内核会捕获到崩溃内核的内存上下文并保存到dump文件中(vmcore)。
Kdump工作原理
Kdump的核心实现基于Kexec(Kernel execution),kexec类似于Linux的exec系统调用。
Kexec可以快速启动一个新的内核(捕获内核),它会跳过BIOS或者Bootloader等引导程序的初始化阶段,这个特性可以让系统崩溃时快速切换到捕获内核,这样生产内核的内存就得到保留,Kdump的工作流程如下图所示:
捕获内核启动后,会像一般内核一样,去运行为它创建的ramdisk上的init程序。捕获内核ramdisk中的init脚本就可以通过通常的文件读写和网络来实现各种策略了。而各种转储机制就是在init中实现的。
为了在生产内核崩溃时能顺利启动捕获内核,捕获内核以及它的ramdisk是事先放到生产内核的内存中的(保留内存)。
安装Kdump
在rhel/centos7.x系统上,kdump服务默认已经安装并且启用了。
但是有些时候,比如自定义安装系统时,可能kdump就不一定被安装及启用。
可以查看系统是否已经安装:
[root@ecs-3370 ~]# rpm -q kexec-tools
kexec-tools-2.0.15-33.el7.x86_64
若没有安装,则可以执行命令安装
yum install kexec-tools
配置Kdump
配置预留内存
上文提到,要生成内核崩溃转储文件,需要预留内存空间给kdump服务,具体需要预留多大空间,主要有两方面因素:
取决于系统硬件架构,可以使用uname -m命令查看。
取决于系统总的内存大小。
在大多数系统中,kdump服务默认下会自动评估需要预留的内存大小,前提是系统总大小要满足一定条件。
上面那个页面点击进去之后,可看到:
默认自动分配了160M(根据我当前主存4GB来确定的),也可以勾选"Manul",然后手动修改预留给kdump服务的内存大小,不建议改太大,因为预留的这部分内存系统不能再使用了,意味着系统可用内存变小。
在centos7/RHEL7上,默认已经安装及配置了kdump服务,可以查看系统启动参数:
[root@ecs-3370 ~]# cat /proc/cmdline
BOOT_IMAGE=/boot/vmlinuz-3.10.0-1062.el7.x86_64 root=UUID=fac7eb66-a4af-4ab4-9318-f66dc5c8dbe1 ro crashkernel=auto spectre_v2=retpoline rhgb quiet LANG=en_US.UTF-8 init 3
其中crashkernel=auto,说明系统自动分配预留给kdump内存大小。
修改系统启动参数中crashkernel参数,在/boot/grub2/grub.cfg文件中,找到menuentry中当前内核启动参数那行:
[root@ecs-3370 ~]# cat /boot/grub2/grub.cfg
...
menuentry 'CentOS Linux (0-rescue-e0972351d26748d6941cfc55f8a027e4) 7 (Core)' --class centos --class gnu-linux --class gnu --class os --unrestricted $menuentry_id_option 'gnulinux-0-rescue-e0972351d26748d6941cfc55f8a027e4-advanced-fac7eb66-a4af-4ab4-9318-f66dc5c8dbe1' {
load_video
insmod gzio
insmod part_msdos
insmod ext2
set root='hd0,msdos2'
if [ x$feature_platform_search_hint = xy ]; then
search --no-floppy --fs-uuid --set=root --hint-bios=hd0,msdos2 --hint-efi=hd0,msdos2 --hint-baremetal=ahci0,msdos2 --hint='hd0,msdos2' fac7eb66-a4af-4ab4-9318-f66dc5c8dbe1
else
search --no-floppy --fs-uuid --set=root fac7eb66-a4af-4ab4-9318-f66dc5c8dbe1
fi
linux16 /boot/vmlinuz-0-rescue-e0972351d26748d6941cfc55f8a027e4 root=UUID=fac7eb66-a4af-4ab4-9318-f66dc5c8dbe1 ro crashkernel=auto spectre_v2=retpoline rhgb quiet init 3
initrd16 /boot/initramfs-0-rescue-e0972351d26748d6941cfc55f8a027e4.img
}
...
如果没有crashkernel参数,则在该行参数中加入:
crashkernel=160M
修改完后,重启系统会生效。
注:在rhel6.x 5.x上配置文件路径可能会有所不同,找到grub.conf文件修改即可。
配置vmcore产生路径
内核crash转储文件可以存储在本地磁盘上,也可以使用nfs或者ssh协议通过网络发送到其它地方。
/etc/kdump.conf配置文件可以配置很多选项,比如配置dump转储文件存放位置、是否压缩dump文件数据、当kdump失败后的行为等待。详细可以查看该文件注释:
移到最下面,可以看到默认配置:
#raw /dev/vg/lv_kdump
#ext4 /dev/vg/lv_kdump
#ext4 LABEL=/boot
#ext4 UUID=03138356-5e61-4ab3-b58e-27507ac41937
#nfs my.server.com:/export/tmp
#ssh user@my.server.com
#sshkey /root/.ssh/kdump_id_rsa
path /var/crash
core_collector makedumpfile -l --message-level 1 -d 31
#core_collector scp
#kdump_post /var/crash/scripts/kdump-post.sh
#kdump_pre /var/crash/scripts/kdump-pre.sh
#extra_bins /usr/bin/lftp
#extra_modules gfs2
#default shell
#force_rebuild 1
#force_no_rebuild 1
#dracut_args --omit-drivers "cfg80211 snd" --add-drivers "ext2 ext3"
#fence_kdump_args -p 7410 -f auto -c 0 -i 10
#fence_kdump_nodes node1 node2
可以看到,vmcore默认路径是在/var/path,可以修改该路径,之后重启kdump服务生效。
[root@ecs-3370 ~]# systemctl restart kdump
测试kdump服务及配置是否真正生效
使用如下命令简单快速测试:
echo c > /proc/sysrq-trigger
如果Kdump配置正确,上述命令会让系统快速重启并且启动捕获内核进行转储,转储完成后会自动切换为生产内核,待系统重启完成后,登录系统,可以查看到 /var/crash目录下,已经有vmcore dump文件生成,vmcore-dmesg.txt文件可以查看到崩溃时系统日志信息:
[root@yglocal ~]# tree /var/crash/
/var/crash/
├── 127.0.0.1-2019-08-06-17:30:37
│ ├── vmcore
│ └── vmcore-dmesg.txt
├── 127.0.0.1-2020-03-20-14:59:51
│ ├── vmcore
│ └── vmcore-dmesg.txt
├── 127.0.0.1-2023-01-2-16:30:56
├── vmcore
└── vmcore-dmesg.txt
什么是crash
Crash是一个用于分析内核转储文件的工具,和Kdump配套使用。Kdump会在内存中保留一块区域,这个区域用于存放捕获内核,生产内核运行过程中崩溃时,Kdump通过Kexec机制自动启动捕获内核,对生产内核的完整信息(CPU寄存器、栈帧数据等)进行保存,生产内核重启后,使用Crash工具来分析这个转储的文件。
Kdump测试
调试环境准备
使用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/上下载内核调试包
下载完后,安装内核调试包:
[root@ecs tmp]# ll /tmp/
-rw-r--r-- 1 root root 462533696 Oct 20 2020 kernel-debuginfo-3.10.0-1160.el7.x86_64.rpm
-rw-r--r-- 1 root root 65172668 Oct 20 2020 kernel-debuginfo-common-x86_64-3.10.0-1160.el7.x86_64.rpm
安装完成后,可以在安装的文件中找到vmlinux内核映像文件:
[root@ecs tmp]# rpm -ql kernel-debuginfo | grep vmlinux
/usr/lib/debug/lib/modules/3.10.0-1160.el7.x86_64/vmlinux
若没有安装过crash,则执行以下命令安装:
yum install crash
使用crash分析vmcore
分析vmcore文件,执行命令:
[root@ecs tmp]# crash /usr/lib/debug/lib/modules/3.10.0-1160.el7.x86_64/vmlinux /var/crash/127.0.0.1-2023-01-09-16\:16\:49/vmcore
crash 7.2.3-11.el7_9.1
Copyright (C) 2002-2017 Red Hat, Inc.
Copyright (C) 2004, 2005, 2006, 2010 IBM Corporation
Copyright (C) 1999-2006 Hewlett-Packard Co
Copyright (C) 2005, 2006, 2011, 2012 Fujitsu Limited
Copyright (C) 2006, 2007 VA Linux Systems Japan K.K.
Copyright (C) 2005, 2011 NEC Corporation
Copyright (C) 1999, 2002, 2007 Silicon Graphics, Inc.
Copyright (C) 1999, 2000, 2001, 2002 Mission Critical Linux, Inc.
This program is free software, covered by the GNU General Public License,
and you are welcome to change it and/or distribute copies of it under
certain conditions. Enter "help copying" to see the conditions.
This program has absolutely no warranty. Enter "help warranty" for details.
GNU gdb (GDB) 7.6
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
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-unknown-linux-gnu"...
WARNING: kernel relocated [578MB]: patching 87292 gdb minimal_symbol values
KERNEL: /usr/lib/debug/lib/modules/3.10.0-1160.el7.x86_64/vmlinux
DUMPFILE: /var/crash/127.0.0.1-2023-01-09-16:16:49/vmcore [PARTIAL DUMP]
CPUS: 4
DATE: Mon Jan 9 16:16:47 2023
UPTIME: 32 days, 06:03:53
LOAD AVERAGE: 0.10, 0.05, 0.06
TASKS: 132
NODENAME: ecs
RELEASE: 3.10.0-1160.el7.x86_64
VERSION: #1 SMP Mon Oct 19 16:18:59 UTC 2020
MACHINE: x86_64 (2200 Mhz)
MEMORY: 16 GB
PANIC: "SysRq : Trigger a crash"
PID: 31407
COMMAND: "bash"
TASK: ffff961ff63cb180 [THREAD_INFO: ffff961ff3bbc000]
CPU: 2
STATE: TASK_RUNNING (SYSRQ)
crash>
其中下面这些信息,就是导致系统崩溃的直接原因及进程相关信息:
PANIC: "SysRq : Trigger a crash"
PID: 31407
COMMAND: "bash"
TASK: ffff961ff63cb180 [THREAD_INFO: ffff961ff3bbc000]
CPU: 2
STATE: TASK_RUNNING (SYSRQ)
crash常用命令
help命令
help命令用于在线查看crash命令的帮助,在crash命令行中输入help命令可以查看crash支持的所有子命令。
crash> help
* extend log rd task
alias files mach repeat timer
ascii foreach mod runq tree
bpf fuser mount search union
bt gdb net set vm
btop help p sig vtop
dev ipcs ps struct waitq
dis irq pte swap whatis
eval kmem ptob sym wr
exit list ptov sys q
crash version: 7.2.3-11.el7_9.1 gdb version: 7.6
For help on any command above, enter "help <command>".
For help on input options, enter "help input".
For help on output options, enter "help output".
help命令不仅可以查看crash支持的命令,还可以查看某个子命令的帮助说明,如查看bt命令的帮助说明。
crash> help bt
NAME
bt - backtrace
SYNOPSIS
bt [-a|-c cpu(s)|-g|-r|-t|-T|-l|-e|-E|-f|-F|-o|-O|-v] [-R ref] [-s [-x|d]]
[-I ip] [-S sp] [pid | task]
DESCRIPTION
Display a kernel stack backtrace. If no arguments are given, the stack
trace of the current context will be displayed.
-a displays the stack traces of the active task on each CPU.
(only applicable to crash dumps)
-A same as -a, but also displays vector registers (S390X only).
-c cpu display the stack trace of the active task on one or more CPUs,
which can be specified using the format "3", "1,8,9", "1-23",
or "1,8,9-14". (only applicable to crash dumps)
-g displays the stack traces of all threads in the thread group of
the target task; the thread group leader will be displayed first.
...
bt命令
bt命令查看一个进程的内核栈的函数调用关系,包括所有异常栈的信息,常用参数如下。
-f:显示每一栈帧里的数据。
-l:显示文件名和行号。
-t:显示栈中所有的文本符号。
pid:显示指定pid的进程的内核栈的函数调用信息。
crash> bt
PID: 31407 TASK: ffff961ff63cb180 CPU: 2 COMMAND: "bash"
#0 [ffff961ff3bbfab0] machine_kexec at ffffffffa5266294
#1 [ffff961ff3bbfb10] __crash_kexec at ffffffffa5322562
#2 [ffff961ff3bbfbe0] crash_kexec at ffffffffa5322650
#3 [ffff961ff3bbfbf8] oops_end at ffffffffa598b798
#4 [ffff961ff3bbfc20] no_context at ffffffffa5275d14
#5 [ffff961ff3bbfc70] __bad_area_nosemaphore at ffffffffa5275fe2
#6 [ffff961ff3bbfcc0] bad_area_nosemaphore at ffffffffa5276104
#7 [ffff961ff3bbfcd0] __do_page_fault at ffffffffa598e750
#8 [ffff961ff3bbfd40] trace_do_page_fault at ffffffffa598ea26
#9 [ffff961ff3bbfd80] do_async_page_fault at ffffffffa598dfa2
#10 [ffff961ff3bbfda0] async_page_fault at ffffffffa598a7a8
[exception RIP: sysrq_handle_crash+22]
RIP: ffffffffa5674856 RSP: ffff961ff3bbfe58 RFLAGS: 00010246
RAX: ffffffffa5674840 RBX: ffffffffa5ee74a0 RCX: 0000000000000000
RDX: 0000000000000000 RSI: ffff9622bfd138d8 RDI: 0000000000000063
RBP: ffff961ff3bbfe58 R8: ffffffffa620487c R9: ffff961ff3b88500
R10: 00000000000003b9 R11: 00000000000003b8 R12: 0000000000000063
R13: 0000000000000000 R14: 0000000000000004 R15: 0000000000000000
ORIG_RAX: ffffffffffffffff CS: 0010 SS: 0018
#11 [ffff961ff3bbfe60] __handle_sysrq at ffffffffa567507d
#12 [ffff961ff3bbfe90] write_sysrq_trigger at ffffffffa56754e8
#13 [ffff961ff3bbfea8] proc_reg_write at ffffffffa54c6d40
#14 [ffff961ff3bbfec8] vfs_write at ffffffffa544db50
#15 [ffff961ff3bbff08] sys_write at ffffffffa544e92f
#16 [ffff961ff3bbff50] system_call_fastpath at ffffffffa5993f92
RIP: 00007f46827d1a00 RSP: 00007ffc54846a78 RFLAGS: 00000202
RAX: 0000000000000001 RBX: 0000000000000002 RCX: 000000000149e260
RDX: 0000000000000002 RSI: 00007f46830f6000 RDI: 0000000000000001
RBP: 00007f46830f6000 R8: 000000000000000a R9: 00007f46830f0740
R10: 00007f46830f0740 R11: 0000000000000246 R12: 00007f4682aaa400
R13: 0000000000000002 R14: 0000000000000001 R15: 0000000000000000
ORIG_RAX: 0000000000000001 CS: 0033 SS: 002b
crash>
上图可以看到,bt命令将系统崩溃瞬间正在运行的进程内核栈信息全部显示出来。当前进程的id是31407,task_struct数据结构的地址是0xffff961ff63cb180,当前进程运行在CPU2上,当前进程的运行命令是bash。后面的栈帧列出了该进程在内核态的调用关系,执行顺序是自下而上。
mod命令
mod命令不仅可以用来显示当前系统加载的内核模块信息,还可以用来加载某个内核模块的符号信息和调试信息等,常用参数如下。
-s:加载某个内核模块的符号信息。
-S:从某个指定目录加载所有内核模块的符号信息,默认目录是/lib/modules/<release>目录查找并加载内核模块的符号信息。
-d:删除某个内核模块的符号信息。
crash> mod
MODULE NAME SIZE OBJECT FILE
ffffffffc03c2b00 floppy 69432 (not loaded) [CONFIG_KALLSYMS]
ffffffffc03cc220 virtio_blk 18323 (not loaded) [CONFIG_KALLSYMS]
ffffffffc03d41a0 virtio 14959 (not loaded) [CONFIG_KALLSYMS]
ffffffffc03e02e0 virtio_console 28076 (not loaded) [CONFIG_KALLSYMS]
ffffffffc03e54a0 pata_acpi 13053 (not loaded) [CONFIG_KALLSYMS]
ffffffffc03ea640 ata_generic 12923 (not loaded) [CONFIG_KALLSYMS]
ffffffffc03f00e0 virtio_ring 22991 (not loaded) [CONFIG_KALLSYMS]
ffffffffc03fc120 virtio_pci 22985 (not loaded) [CONFIG_KALLSYMS]
ffffffffc0402040 net_failover 18147 (not loaded) [CONFIG_KALLSYMS]
ffffffffc0407080 mbcache 14958 (not loaded) [CONFIG_KALLSYMS]
ffffffffc0435440 libata 243094 (not loaded) [CONFIG_KALLSYMS]
ffffffffc0449040 failover 13374 (not loaded) [CONFIG_KALLSYMS]
ffffffffc0451360 virtio_net 28085 (not loaded) [CONFIG_KALLSYMS]
ffffffffc0459360 ip_tables 27126 (not loaded) [CONFIG_KALLSYMS]
ffffffffc0460180 serio_raw 13434 (not loaded) [CONFIG_KALLSYMS]
ffffffffc0465000 crct10dif_common 12595 (not loaded) [CONFIG_KALLSYMS]
ffffffffc046b1a0 crc32c_intel 22094 (not loaded) [CONFIG_KALLSYMS]
...
dis命令
dis命令用来输出反汇编结果,常用参数如下。
-l:显示反汇编和对应源代码的行号。
-r:(反向)显示从例程开始到指定地址(包括指定地址)的所有指令。
-s:显示对应的源代码。
如输出sysrq_handle_crash+22的反汇编结果。
dis -l (function+offset) 10 反汇编出指令所在代码开始,10行代码
crash> dis sysrq_handle_crash+22
0xffffffffa5674856 <sysrq_handle_crash+22>: movb $0x1,0x0
crash> dis -l sysrq_handle_crash+22
/usr/src/debug/kernel-3.10.0-1160.el7/linux-3.10.0-1160.el7.x86_64/drivers/tty/sysrq.c: 145
0xffffffffa5674856 <sysrq_handle_crash+22>: movb $0x1,0x0
crash> dis -l sysrq_handle_crash+22 10
/usr/src/debug/kernel-3.10.0-1160.el7/linux-3.10.0-1160.el7.x86_64/drivers/tty/sysrq.c: 145
0xffffffffa5674856 <sysrq_handle_crash+22>: movb $0x1,0x0
/usr/src/debug/kernel-3.10.0-1160.el7/linux-3.10.0-1160.el7.x86_64/drivers/tty/sysrq.c: 146
0xffffffffa567485e <sysrq_handle_crash+30>: pop %rbp
0xffffffffa567485f <sysrq_handle_crash+31>: retq
/usr/src/debug/kernel-3.10.0-1160.el7/linux-3.10.0-1160.el7.x86_64/drivers/tty/sysrq.c: 86
0xffffffffa5674860 <sysrq_handle_loglevel>: nopl 0x0(%rax,%rax,1) [FTRACE NOP]
0xffffffffa5674865 <sysrq_handle_loglevel+5>: push %rbp
/usr/src/debug/kernel-3.10.0-1160.el7/linux-3.10.0-1160.el7.x86_64/drivers/tty/sysrq.c: 91
0xffffffffa5674866 <sysrq_handle_loglevel+6>: xor %eax,%eax
/usr/src/debug/kernel-3.10.0-1160.el7/linux-3.10.0-1160.el7.x86_64/drivers/tty/sysrq.c: 90
0xffffffffa5674868 <sysrq_handle_loglevel+8>: movl $0x7,0x7d38fe(%rip) # 0xffffffffa5e48170
/usr/src/debug/kernel-3.10.0-1160.el7/linux-3.10.0-1160.el7.x86_64/drivers/tty/sysrq.c: 86
0xffffffffa5674872 <sysrq_handle_loglevel+18>: mov %rsp,%rbp
0xffffffffa5674875 <sysrq_handle_loglevel+21>: push %rbx
/usr/src/debug/kernel-3.10.0-1160.el7/linux-3.10.0-1160.el7.x86_64/drivers/tty/sysrq.c: 89
0xffffffffa5674876 <sysrq_handle_loglevel+22>: lea -0x30(%rdi),%ebx
crash>
这里能看出执行movb $0x1,0x0时系统崩溃。
rd命令
rd命令用来读取内存地址中的值,常用参数如下。
-a:显示ASCII。
-d:显示十进制。
-p:读取物理地址。
-s:显示符号。
-32:显示32位宽的值。
-64:显示64位宽的值。
struct命令
struct命令用于显示内核中数据结构的定义或者具体的值,常用的参数如下。
struct_name:数据结构的名称。
.member:数据结构的某个成员。
-o:显示成员在数据结构中的偏移量。
以下命令用于显示task_struct数据结构的定义。
irq命令
irq命令用于显示中断的相关信息,常用参数如下。
index:显示某个指定IRQ的信息。
-a:显示中断的亲和性。
-b:显示中断的下半部信息。
-s:显示系统中断信息。
vm命令
vm命令用于显示进程地址空间的相关信息,常用参数如下。
-p:显示虚拟地址和物理地址。
-m:显示mm_struct数据结构。
-v:显示进程中所有vma(vm_area_struct)数据结构的值。
-R:搜索特定字符串或者数值。
kmem命令
kmem命令用来显示系统内存信息,常用参数如下。
-g:显示page数据结构里flagsde标志位。
-i:显示系统内存使用情况。
-p:显示每个页面的使用情况。
-s:显示slab使用情况。
-v:显示vmalloc使用情况。
-z:显示没个zone使用情况。
-V:显示系统vm_stat情况。
task命令
task命令用于显示进程的task_struct数据结构和thread_info数据结构的内容。以下命令用于显示task_struct数据结构信息,其中-x表示以十六进制显示。
sym命令
sym命令用于解析内核符号信息,常用选项如下所示。
-l:显示所有符号信息。
-m:显示某个内核模块的所有符号信息。
-q:查询符号信息。
list命令
list命令用来遍历链表,并且可以输出链表成员的值,常用的参数如下。
-h:指定链表头的地址。
-s:用来输出链表成员的值。
写在最后
如果我们确定是某个内核模块导致的问题,可以反汇编出该模块的源代码:
objdump -S -D my_test.ko > my_test.S
然后vim查看my_lkm.S文件,查看对应的源码