CentOS 7 内核调试工具
有时候会碰到内核panic(内核崩溃)的问题,需要定位具体的原因,对于内核panic的问题定位稍微研究了下。
需要先了解下相关的概念:
-
kexec
在调试过程中,经常需要重启内核以还原现场,进而复现某些问题予以追踪解决。由于每一次的内核启动,都会伴随着一次的boot自检。但是,对于已经启动过的同一内核,重复的boot自检完全没有必要,且造成了资源浪费。此外,有时候我们需要使用一个小内核来启动一个大内核。在这两种需求下,kexec应运而生,kexec是一款可以让您重新启动到一个新 Linux 内核的快速重新引导功能部件,不再必须通过固件和引导装载程序阶段,从而跳过序列中最长的部分,大大减少了重启时间。对企业级系统而言,Kexec 大大减少了重启引起的系统宕机时间。对内核和系统软件开发者而言,Kexec 帮助您在开发和测试成果时可以迅速重新启动系统,而不必每次都要再经历耗时的固件阶段。
需要安装kexec-tools:
yum install kexec-tools
需要检测本系统内核是否已经选中支持kexec system call,若在“/boot/config-XXXXX”中CONFIG_KEXEC=y则是本版本号的内核已开启;若=n,则需要重新编译内核重置CONFIG_KEXEC=y。
-
kdump
kdump是内核转储工具,kexec是实现kdump机制的关键。kdump是基于kexec的内核崩溃转储机制。
当系统崩溃时,kdump利用kexec启动另一个内核,这一个内核叫捕获内核,以很小的内存启动捕获内核,第一个内核保留了内存的一部分给捕获内核启动用。kdump利用kexec启动捕获内核,免去启动BIOS(绕过BIOS,没有经历BIOS,节省了系统启动的时间),所以第一个内核的内存得以保留。
捕获内核只会使用分配给他的内存空间,不会污染第一内核的内存数据。
与kexec同属于kexec-tools这个软件包。
yum install kexec-tools
开启服务:
systemctl start kdump systemctl enable kdump
配置内核启动参数:
必须在引导“第一内核”期间为捕获内核保留 crashkernel 的内存,一般设置为 crashkernel=auto 表示根据系统内存自动reserve一些内存给kernelcrash用。
vim /boot/grub2/grub.cfg ... linux16 /vmlinuz-3.10.0-693.21.1.el7.x86_64 root=/dev/mapper/centos-root ro crashkernel=auto ... ...
kdump.service 相关的配置文件 /etc/kdump.conf 里面可以修改一些默认的配置,比如dump完成后的动作(默认是reboot)、dump文件存放的方式(本地目录、NFS、scp到另外服务器等),默认存在本地目录:
path /var/crash
默认发行的Centos系统安装了kexec-tools,关于kexec以及kdump的安装与配置不需要手动执行。
-
crash
crash是和kdump工具配套使用,用于解析kdump生成的vmcore文件。vmcore是内核的映像(实际上是整个内存的映像,一般来说我们会开启过滤功能,只记录内核页)。内核全部的数据结构都在这个映像里面。crash解析vmcore可以让我们看到触发kdump时刻系统的各种状态和内容,信息非常丰富,是定位分析内核问题的利器。
使用crash需要安装crash工具包和内核调试信息包:
其中内核调试信息包是指与待分析内核版本相匹配的debuginfo软件包。
yum install crash yum install kernel-debuginfo-common yum install kernel-debuginfo
使用crash来调试vmcore,至少需要两个参数:
NAMELIST:未压缩的内核映像文件vmlinux,默认位于/usr/lib/debug/lib/modules/XXX/vmlinux,由内核调试信息包提供。
MEMORY-IMAGE:内存转储文件vmcore,默认位于/var/crash/%HOST-%DATE/vmcore,由kdump生成。
例如:
crash /usr/lib/debug/lib/modules/3.10.0-693.el7.x86_64/vmlinux vmcore
注意:
当安装的dedubginfo版本与待分析内核不一致时会报错。
crash: /usr/lib/debug/lib/modules/3.10.0-693.el7.x86_64/vmlinux and vmcore do not match
使用crash分析
当系统panic时,会自动生成vmcore以及vmcore-dmesg.txt,目录默认在/var/crash/(配置文件中的默认配置)
上层文件夹命名规则为%HOST-%DATE
[root@localhost 127.0.0.1-2019-07-06-23:33:43]# pwd
/var/crash/127.0.0.1-2019-07-06-23:33:43
[root@localhost 127.0.0.1-2019-07-06-23:33:43]# ll
total 73840
-rw------- 1 root root 75570158 Jul 6 23:33 vmcore
-rw-r--r-- 1 root root 37982 Jul 6 23:33 vmcore-dmesg.txt
crash命令:
crash使用gdb作为内部引擎,语法类似于gdb:
你可以输入help显示所有支持的命令;
你可以输入help <命令名>显示这个命令的帮助。命令名>
crash命令可以和外部得命令结合使用,比如:log | tail -n 20。
-
bt命令显示执行KDUMP的堆栈,通过调用堆栈可以看到触发内核panic的运行过程。RIP为造成内核崩溃的指令,Call Trace为函数调用栈,通过RIP和Call Trace可以确定函数的调用路径,以及在哪个函数中的哪条指令引发了错误。Call Trace为函数的调用栈,是从下往上看的。可以用来分析函数的调用关系。
bt 后面可以跟进程id,查看某个进程具体的调用栈。
bt -l task-id 指定task显示调用栈。
crash> bt PID: 176144 TASK: ffff8810c8659fa0 CPU: 39 COMMAND: "multipathd" #0 [ffff881b86e03870] machine_kexec at ffffffff8105c4cb #1 [ffff881b86e038d0] __crash_kexec at ffffffff81104a32 #2 [ffff881b86e039a0] crash_kexec at ffffffff81104b20 #3 [ffff881b86e039b8] oops_end at ffffffff816ad278 #4 [ffff881b86e039e0] die at ffffffff8102e97b #5 [ffff881b86e03a10] do_general_protection at ffffffff816acbfe #6 [ffff881b86e03a40] general_protection at ffffffff816ac4a8 [exception RIP: kmem_cache_close+201] RIP: ffffffff811e18a9 RSP: ffff881b86e03af0 RFLAGS: 00010202 RAX: ffff882fdc529101 RBX: ffff880624ddc000 RCX: 0000000001dc2cfd RDX: 0000000001dc2cfc RSI: 0000000000000246 RDI: ffff88017fc07e00 RBP: ffff881b86e03b40 R8: 0000000000019b60 R9: ffffffff811e19c5 R10: ffff882fdddd9b60 R11: ffffea00bf714a40 R12: ffff88303fdd5d00 R13: ffffea0018937600 R14: ffff880624dd8000 R15: dead0000000000e0 ORIG_RAX: ffffffffffffffff CS: 0010 SS: 0018 #7 [ffff881b86e03b48] __kmem_cache_shutdown at ffffffff811e1ad4 #8 [ffff881b86e03b68] kmem_cache_destroy at ffffffff811a6874 #9 [ffff881b86e03b88] kmem_cache_destroy_memcg_children at ffffffff811f6009 #10 [ffff881b86e03bb0] kmem_cache_destroy at ffffffff811a6849 #11 [ffff881b86e03bd0] bioset_free at ffffffff8123afee #12 [ffff881b86e03bf0] dm_free_md_mempools at ffffffffc07c6411 [dm_mod] #13 [ffff881b86e03c08] dm_table_free_md_mempools at ffffffffc07c7e29 [dm_mod] #14 [ffff881b86e03c20] dm_swap_table at ffffffffc07c5cc8 [dm_mod] #15 [ffff881b86e03cd8] dev_suspend at ffffffffc07cb6ea [dm_mod] #16 [ffff881b86e03d08] ctl_ioctl at ffffffffc07cc015 [dm_mod] #17 [ffff881b86e03ea0] dm_ctl_ioctl at ffffffffc07cc343 [dm_mod] #18 [ffff881b86e03eb0] do_vfs_ioctl at ffffffff812151cd #19 [ffff881b86e03f30] sys_ioctl at ffffffff81215471 #20 [ffff881b86e03f80] system_call_fastpath at ffffffff816b4fc9 RIP: 00007f10e038d107 RSP: 00007ffe5d280480 RFLAGS: 00000293 RAX: 0000000000000010 RBX: ffffffff816b4fc9 RCX: ffffffffffffffff RDX: 0000561db269c100 RSI: 00000000c138fd06 RDI: 0000000000000004 RBP: 00007f10e1162283 R8: 00007ffe5d27f5b0 R9: 00007f10e136f938 -- MORE -- forward: <SPACE>, <ENTER> or j backward: b or k quit: q
-
dis 反汇编指令
dis -l probe_2093+497 10 // 反汇编从某个地址开始的10条指令
crash> dis -l ffffffff811e18a9 /usr/src/debug/kernel-3.10.0-693.el7/linux-3.10.0-693.el7.x86_64/mm/slub.c: 3377 0xffffffff811e18a9 <kmem_cache_close+201>: mov 0x20(%r15),%rax crash> dis -l kmem_cache_close+201 5 /usr/src/debug/kernel-3.10.0-693.el7/linux-3.10.0-693.el7.x86_64/mm/slub.c: 3377 0xffffffff811e18a9 <kmem_cache_close+201>: mov 0x20(%r15),%rax 0xffffffff811e18ad <kmem_cache_close+205>: lea 0x20(%r15),%rdi 0xffffffff811e18b1 <kmem_cache_close+209>: sub $0x20,%rax 0xffffffff811e18b5 <kmem_cache_close+213>: cmp -0x30(%rbp),%rdi 0xffffffff811e18b9 <kmem_cache_close+217>: je 0xffffffff811e19d0 <kmem_cache_close+496>
-
gdb list显示源代码
后跟函数名或者星号函数地址来显示源代码,与gdb调试规则一样。
crash> gdb l *kmem_cache_close+201 0xffffffff811e18a9 is in kmem_cache_close (mm/slub.c:3377). 3372 */ 3373 static void free_partial(struct kmem_cache *s, struct kmem_cache_node *n) 3374 { 3375 struct page *page, *h; 3376 3377 list_for_each_entry_safe(page, h, &n->partial, lru) { 3378 if (!page->inuse) { 3379 remove_partial(n, page); 3380 discard_slab(s, page); 3381 } else { crash> gdb list kmem_cache_close 3387 3388 /* 3389 * Release all resources used by a slab cache. 3390 */ 3391 static inline int kmem_cache_close(struct kmem_cache *s) 3392 { 3393 int node; 3394 3395 flush_all(s); 3396 /* Attempt to free all objects */ crash> crash> crash> gdb list 3397 for_each_node_state(node, N_NORMAL_MEMORY) { 3398 struct kmem_cache_node *n = get_node(s, node); 3399 3400 free_partial(s, n); 3401 if (n->nr_partial || slabs_node(s, node)) 3402 return 1; 3403 } 3404 free_percpu(s->cpu_slab); 3405 free_kmem_cache_nodes(s); 3406 return 0;
-
log:打印系统消息缓冲区,displays the kernel log_buf contents,如log | tail -n 30。
-
ps:显示进程的状态
ps | grep UN #显示所有D状态(TASK_UNINTERRUPTIBLE)的进程
ps | grep RU #显示所有R状态(TASK_RUNNING)的进程
ps | grep IN #显示所有S状态(TASK_INTERRUPTIBLE)的进程
ps -t pid 显示进程的运行时间
ps -g pid 显示进程组相关信息
ps -a pid 可以显示进程的启动参数(不一定可以获取)
-
kmem -i:显示内存使用信息。
-
sys:显示系统信息
-
p:显示全局变量
-
task TASKID: 显示与task相关的信息,这里面的信息较多,可以使用task_struct数据结构分别获取特定的信息,例如task_struct.comm获取命令、task_struct.mm 获取内存地址等
crash> task_struct.comm ffff880f1ae96eb0 comm = "CPU 31/KVM\000\000\000\000\000" crash> task_struct.mm ffff880f1ae96eb0 mm = 0xffff887f39570c80 crash> mm_struct.arg_start,arg_end 0xffff887f39570c80(内存地址) arg_start = 140727726191214 arg_end = 140727726194574
-
vtop 转化虚拟地址到物理地址
通过crash工具提供的相关命令可以找到触发系统内核panic的代码,从社区寻找相关的patch进行合入解决。但是有些触发panic的命令并非是真正的原因,例如系统的资源持续被占用最终导致内核panic,需要根据系统日志以及系统相关操作来综合分析根因。
参考文档: