入夜,夜幕拉开,寂静黑夜平静如常。某日晚上21:00,某公司数据中心监控人员突然在监控告警中心看到一条深红色的服务器重启告警信息,当他正准备点击查看告警详细信息,鼠标下又弹出一连串红色的告警信息……
告警中心显示:公司准生产环境总计有十余台服务器发生异常重启。故障服务器操作系统都生成了dump文件。值班人员确认,故障对业务无影响。且故障发生前无服务器硬件故障告警,无虚拟机HA告警,无服务器重启变更操作。
从现象上初步判断故障应是准生产环境服务器上某个广泛部署的软件在某个时间节点因未知操作导致。为了全面彻底地查明重启原因,我们需要一把钥匙去解密铁门后的真相。而这把钥匙就是,故障操作系统的dump文件。

一、什么是core dump机制
随着计算机程序越来越复杂,各种程序调试技术不断被创造出来,常见的思路就是将程序在内存上运行的信息转存,以便于查找程序运行错误的位置,这就是
计算机技术发展至今,实现core dump比以前复杂很多很多,但其实现过程都大同小异。其过程大概可简单理解为:当一个程序因为错误(如访问违规、段错误等)而崩溃时,为便于后续的调试分析,操作系统会将程序的内存、寄存器、调用栈等信息保存到转储文件里。
PS:为什么只保留内存、寄存器、调用栈这些信息?它们有什么作用?当CPU执行程序时,它会从内存中读取指令到寄存器,然后执行这些指令。当遇到函数调用时,CPU将相关信息压入调用栈,并跳转到函数的内存地址继续执行。其中,内存保存了程序执行时的程序代码、数据和变量等信息。寄存器保存了指令、数据和地址等信息。调用栈保存了函数调用的顺序和局部变量。
实现core dump机制有很多机制和工具,kdump技术是目前最可靠、最常用的,已被主要的Linux厂商选用。因为kdump是基于kexec的内核崩溃转储机制,所以命名为kdump。Kdump的过程也比较复杂,简单说就是当Linux内核崩溃时,kdump工具捕获当时内存等状态信息,并将生成转储文件(vmcore),然后重启操作系统。启用kdump服务包括安装kdump工具、修改内核启动参数、修改kdump配置文件参数、启动kdump服务功能、验证kdump服务功能。
有了kdump生成的vmcore文件,如何使用这把钥匙呢?通俗理解分析内存转储,就是分析故障发生时系统产生的“快照”。实际上需要工程师以这个快照为出发点,追溯历史,找出问题发生源头。这就像是从案发现场,推理案发经过一样。
二、怎么解析dump文件
以故障当晚的服务器操作系统崩溃异常重启为例,异常崩溃时/var/crash目录下生成了vmcore文件。然后要做的是,就是将vmcore文件拷贝到分析环境来进一步解析。
建议单独搭建分析环境。搭建方法也非常简单,找一台Linux服务器,安装好dump文件分析工具crash,安装好故障服务器操作系统内核对应的debuginfo包。环境搭好后,就可以将故障服务的vmcore文件传过来进行分析。这里的debuginfo包,必须针对内核版本在官网或镜像网下载相同版本的包。这个包安装好后会在操作系统上生成一个vmlinux文件,我们称为Linux映像文件,它类似于读不同语言文章需要用到不同的字典,用于解析vmcore信息。具体可以在网址[2] 下载。我们这次按故障服务器内核版本下载了kernel-debuginfo-common-2.6.32-754.35.1.el6.x86_64.rpm包。
# 安装crash工具
yum install crash
# 安装debuginfo包
debuginfo-install kernel-debuginfo-common-2.6.32-754.35.1.el6.x86_64.rpm
# 然后运行crash工具,分析故障服务器的vmcore文件
crash /usr/lib/debug/lib/modules/2.6.32-754.35.1.el6.x86_64/vmlinux /var/crash/vmcore
三、怎么使用crash工具
crash工具像一把瑞士军刀,其包括了很多分析命令,使用每个命令都可以提供故障时内核相应的信息,综合使用这些命令可以帮助我们追溯内核崩溃的原因。常见命令和作用如下:
| log命令 | 用于显示内核日志信息 |
| sys命令 | 用于显示内核崩溃基本信息 |
| bt命令 | 用于查看调用栈信息 |
| ps命令 | 用于查看进程状态 |
| kmem命令 | 用于查看内存使用情况 |
| task命令 | 用于查看进程的task_struct数据结构 |
| sym命令 | 用于显示虚拟内存地址对应的代码符号 |
| mod命令 | 用于查看加载的内核模块信息 |
| files命令 | 用于查看文件系统信息 |
PS:要非常纯熟地使用crash工具来分析故障,还是有一定的门槛的。要求工程师掌握操作系统、汇编、程序调试等知识,并且最好具备排查故障经验。
我们以分析本次故障服务器dump文件为例,尽量将所有重点分析过程作通俗化解释,方便大家明白怎么使用crash工具。
四、了解崩溃基本信息
前面我们已经用crash工具打开了vmcore文件。分析vmcore文件的第一步,是使用sys命令来直观查看系统的基本信息和宕机的原因。使用该命令显示的信息如下:
crash> sys
第1行 KERNEL:/usr/lib/debug/lib/modules/2.6.32-754.35.1.el6.x86_64/vmlinux
第2行 DUMPFILE: vmcore [PARTIAL DUMP]
第3行 CPUS:4
第4行 DATE:********
第5行 UPTIME:143 days,09:21:23
第6行 LOAD AVERAGE:0.04,0.03,0.00
第7行 TASKS:344
第8行 NODENAME: b*****mal02001
第9行 RELEASE:2.6.32-754.35.1.el6.x86_64
第10行 VERSION:#1 SMP Wed Sep 16 06:48:01 EDT 2020
第11行 MACHINE: x86_64 (2294Mhz)
第12行 MEMORY:16 GB
第13行 PANIC: "BUG: unable to handle kernel paging request at ffffffffa0395070"
使用sys命令查看内核崩溃简要信息
sys命令信息显示了故障服务器系统内核基本信息,我们试着逐行解读。
第1行显示我们安装debuginfo包后,Linux内核映像文件vmlinux的位置。
第2行显示正在分析的vmcore文件。
第3行显示CPU数量为4。
第4行显示dump文件生成的日期。
第5行显示故障发生前已经持续开机143天。
第6行显示系统负载平均值。
第7行显示系统中的进程数量。
第8行显示主机名。
第9行显示系统内核版本。
第10行显示该版本Linux内核的发布时间。
第11行显示操作系统架构。
第12行显示内存大小。
第13行显示导致系统崩溃的报错信息。
最后显示的报错信息非常重要,"BUG: unable to handle kernel paging request at ffffffffa0395070",这是一个内核分页操作报错
PS:要非常纯熟地使用crash工具来分析故障,还是有一定的门槛的。要求工程师掌握操作系统、汇编、程序调试等知识,并且最好具备排查故障经验。
我们以分析本次故障服务器dump文件为例,尽量将所有重点分析过程作通俗化解释,方便大家明白怎么使用crash工具。如果你很少处理系统内核报错可能不太清楚unable to handle kernel paging request的意思。这类报错常见原因是内存不足、内存泄漏、错误的内存访问、内核BUG、硬件故障、驱动程序错误。故障分析必须结合故障现象,一般使用排除法来定位问题。
检查故障服务器的硬件日志、操作系统日志、服务器性能数据发现均无报错,基本排除了硬件故障、驱动程序缺陷、内存不足的可能性。故障发生在多个应用、多种内核的服务器上,基本排除了内核BUG、内存泄露的可能性。剩下的可能性就是,内存访问异常导致内核崩溃。
故障不会无缘无故产生,事物发生总有源头。我们在文章开头从现象推断,很可能是在故障发生前针对某个广泛部署在服务器上的软件,做了某些变更操作导致故障发生。这给我们提供了一个解决思路,就像眺望远方,隐约看到了远处的风筝。但是手上只有杂乱的线头,只有循着vmcore文件
通常使用sys命令查看内核基本信息后,我们会使用bt命令去查询导致内核崩溃的进程信息,顺便查看故障时内核调用栈信息。
五、谁触发了异常
使用crash工具的bt命令(调用栈跟踪backtrace的缩写)查看调用栈信息,显示信息如下:
crash> bt
第1行 PID:177488 TASK: ffff880435b92ab0 CPU:2 COMMAND:"ss"
第2行#0 [ffff880437c0b7e0] machine_kexec at ffffffff8104179b
第3行#1 [ffff880437c0b840] crash_kexec at ffffffff810d7a52
第4行#2 [ffff880437c0b910] oops_end at ffffffff81560310
第5行#3 [ffff880437c0b940] no_context at ffffffff8105578b
第6行#4 [ffff880437c0b990] __bad_area_nosemaphore at ffffffff81055a15
第7行#5 [ffff880437c0b9e0] bad_area_nosemaphore at ffffffff81055ae3
第8行#6 [ffff880437c0b9f0] __do_page_fault at ffffffff810562a0
第9行#7 [ffff880437c0bb10] do_page_fault at ffffffff815622ce
第10行#8 [ffff880437c0bb40] page_fault at ffffffff8155f265
第11行[exception RIP: strnlen+9]
第12行 RIP: ffffffff812ae3a9 RSP: ffff880437c0bbf8 RFLAGS:00010286
第13行 RAX: ffffffff817cce1a RBX: ffff880436b36000 RCX:0000000000000005
第14行 RDX: ffffffffa0395070 RSI: ffffffffffffffff RDI: ffffffffa0395070
第15行 RBP: ffff880437c0bbf8 R8:0000000000000073 R9:0000000000000020
第16行 R10: ffff88043587b198 R11:0000000000000246 R12: ffff880436b350cc
第17行 R13: ffffffffa0395070 R14:0000000000000011 R15:0000000000000010
第18行 ORIG_RAX: ffffffffffffffff CS:0010 SS:0018
第19行#9 [ffff880437c0bc00] string at ffffffff812af7e0
第20行#10 [ffff880437c0bc40] vsnprintf at ffffffff812b12a8
第21行#11 [ffff880437c0bce0] seq_vprintf at ffffffff811c9042
第22行#12 [ffff880437c0bd00] seq_printf at ffffffff811c90ad
第23行#13 [ffff880437c0bd60] s_show at ffffffff811886ca
第24行#14 [ffff880437c0bdf0] seq_read at ffffffff811c9269
第25行#15 [ffff880437c0be70] proc_reg_read at ffffffff8120faf0
第26行#16 [ffff880437c0bec0] vfs_read at ffffffff811a3447
第27行#17 [ffff880437c0bf00] sys_read at ffffffff811a3791
第28行#18 [ffff880437c0bf50] system_call_fastpath at ffffffff81566391
第29行 RIP:00007f45ff1ce7f0 RSP:00007ffc86cf6678 RFLAGS:00010246
第30行 RAX:0000000000000000 RBX:0000000000fc8010 RCX: ffffffff815662ce
第31行 RDX:0000000000000400 RSI:00007f45ff8bf000 RDI:0000000000000008
第32行 RBP:00000000000000ff R8:00000000ffffffff R9:0000000000000000
第33行 R10:0000000000000022 R11:0000000000000246 R12:000000000000000a
第34行 R13:0000000000000000 R14:0000000000000000 R15:00007ffc86cf67f0
第35行 ORIG_RAX:0000000000000000 CS:0033 SS: 002b
调用栈信息,记录了系统崩溃时内核函数调用的顺序。解读调用栈信息时,需要倒序来看。
PS:从第1行可以看出,有程序(就是“ss”命令)调用了system_call_fastpath(第28行)、sys_read(第27行)、vfs_read、proc_reg_read、string(第19行)。在调用string时内存访问异常,内核调用page_fault(第10行)、bad_area_nosemaphore、oops_end、crash_kexec、machine_kexec(第2行)来捕获内核信息,生成dump文件并重启。
调用栈信息看上去比较复杂,我们试着逐行解读。
第1行直接显示出导致内核崩溃的进程是ss,进程ID是177488,崩溃发生在第2个CPU。这里解释下,ss是Linux操作系统常用的显示套接字统计信息工具,用于获取网络连接、监听端口、路由表、接口统计等信息。
第2行#0:调用machine_kexec函数,它用于启动内存转储。
第3行#1:调用crash_kexec函数,它主要用于内存转储。
第4行#2:调用oops_end函数,它是一个与错误处理内核错误的函数。
第5行#3:调用no_context函数,它是用于处理没有足够的上下文信息来执行常规错误处理的函数。
第6行#4:调用_bad_area_nosemaphore函数,它是用于处理访问无效内存区域错误的函数。
第7行#5:调用bad_area_nosemaphore函数,它是_bad_area_nosemaphore的顶层函数。
第8行#6:调用_do_page_fault函数,它是用于处理内存分页错误的函数。
第9行#7:调用do_page_fault,它是_do_page_fault的顶层函数。
第10行#8:page_fault是触发内存页面错误异常的指令,一般发生在当程序尝试访问不存在或不允许访问的虚拟内存地址时。
第11-18行[exception RIP: strnlen+9],表示崩溃发生在strnlen函数的第九个字节上。后面7行是显示了寄存器保存的信息。RIP指向下一条将要执行的指令。RSP指当前的栈顶。RFLAGS指包含处理器状态标志的寄存器。RAX、RBX、RCX、...R15是通用寄存器,用于存储数据。ORIG_RAX是系统调用号。CS、SS是代码段编号和堆栈段编号。
第19行#9:调用string函数,用于字符串操作。
第20行#10:调用vsnprintf函数,用于格式化输出到字符串。
第21行#11:调用seq_printf函数,用于格式化输出到字符串。
第22行#12:调用seq_vprintf函数,用于格式化输出到字符串,常用于/proc文件系统或其他虚拟文件系统。
第23行#13:调用s_show函数,用于填充/proc文件系统下特定文件的内容,并序列化显示。
第24行#14:调用seq_read函数,用于对/proc文件系统下某些文件的读取操作,以及在内核模块中生成格式化的输出。
第25行#15:调用proc_reg_read函数,用于处理与/proc文件系统相关的读取操作。
第26行#16:调用vfs_read函数,在用户空间程序通过系统调用请求读取文件时被调用。
第27行#17:调用sys_read内核函数,用于处理read系统调用。
第28行#18:调用system_call_fastpath内核函数,用于处理系统调用的快速路径。
第29-35行显示了用户程序调用异常相关的CPU寄存器的状态,包括指令指针RIP、堆栈指针RSP、标志寄存器RFLAGS,以及其他通用寄存器如RAX、RBX、RCX等。
PS:/proc是Lin
版权声明:本文遵循 CC 4.0 BY-SA 版权协议,若要转载请务必附上原文出处链接及本声明,谢谢合作! ux系统中的一个特殊文件系统,称为proc文件系统。它是一个虚拟文件系统,提供了一种机制来查询内核和运行中的进程信息。
我们从调用栈信息得知,故障由ss进程导致(进程号是177488),而且故障前ss进程访问proc文件系统。这个进程触发内核报内存访问异常错误,导致内核崩溃。
但是这里的ss命令是常用的查询命令,很多程序可能使用它,因此它基本不可能是故障发生的原因。我们先试着寻找是哪个程序运行了ss命令,顺便看是否能分析出ss进程是如何导致了内存访问异常。
六、崩溃前谁在干活
我们使用crash工具的ps命令查询内核崩溃前所有进程的状态信息。使用ps命令分析显示信息如下:
crash> ps
PID PPID CPU TASK ST %MEM VSZ RSS COMM
0 0 0 ffffffff81a97020 RU 0.0 0 0 [swapper]
0 0 1 ffff88043988d520 RU 0.0 0 0 [swapper]
...
177488 64302 1 ffff880436662ab0 RU 0.0 6280 568 ss
...
内核崩溃时的进程状态信息
ps命令显示了故障服务器344个进程的基本信息(代码段中省略了大部分无关进程)。进程信息包括进程ID(PID)、父进程ID(PPID)、进程组ID(PGID)、使用哪个CPU(CPU)、进程地址(TASK)、进程状态(ST)、内存占用率(%MEM)、进程占用虚拟内存多少KB(VSZ)、进程占用物理内存多少KB(RSS)、启动进程的命令(COMM)。
根据bt命令显示的ss进程号177488,可以查到其父进程号是64302。根据这个进程号,查出父进程为salt-minion,salt-minion进程无父进程。所以ss命令是由salt-minion程序执行的。
经查,公司准生产服务器上都安装了自动化工具,该工具是基于开源自动化工具软件SaltStack开发。其服务端安装salt-master程序,客户端服务器安装salt-minion程序。客户端服务器会每隔几分钟就会调用一次ss命令,以确认客户端和服务端通信状态是否正常。
虽然确认了ss进程的归属问题,但是仔细想想发现还有很多疑点。ss命令是常用的系统命令而且是定时执行,自动化工具也没有做其他变更操作,为什么会导致操作系统内核崩溃呢?这里面肯定有更加深层的原因。
从bt命令查询到的调用栈信息看到,ss进程访问proc文件系统出现异常。正好crash工具提供了file命令能查询进程访问了哪个文件。查询信息如下:
PS:直接使用file命令时显示信息会比较繁杂,一般使用struct file命令可以查看某进程访问文件的结构化信息。f_path是file的一个成员,它提供了关于文件路径的信息,这样进一步简化了显示信息。根据ps命令查到ss进程的进程地址为ffff880436662ab0,也就是进程信息代码段ss进程那一行在TASK列对应的地址,作为输入参数。
crash> struct file.f_path ffff880436662ab0
f_path = {
mnt = 0xffff880432adbe80,
dentry = 0xffff880101cae5c0
}
该命令显示了ss进程访问文件路径的结构信息,mnt表示文件所在的挂载点,dentry表示文件名对应的地址ffff880101cae5c0。
PS:这里只给出了一个内存地址,仍然没有给出一个明确的文件目录。使用crash工具就是比较绕,因为dump文件记录信息一般直接记录内存地址,需要使用命令和映像文件来解析。
接下来我们使用crash工具中的files命令来解析该地址对应的文件名。显示信息如下:
crash> files -d 0xffff880101cae5c0
DENTRY INODE SUPERBLK TYPE PATH
ffff880101cae5c0 ffff880101c1d598 ffff88043a23e800 REG /proc/slabinfo
使用files命令查询某个内存地址对应的文件路径名
从files命令查询的信息看到,故障时ss进程访问的文件是/proc/slabinfo。到目前为止,只知道ss访问该文件,并不能知道为什么会出现内存访问异常。
PS:/proc/slabinfo文件包含了当前内核中所有slab缓存的详细信息。
七、从源码找到异常
为继续查找故障发生的具体情况,我们应该从调用栈信息查看故障时程序运行情况。从bt命令的调用栈信息看到,异常报错位置[exception RIP: strnlen+9],RIP指向异常调用地址为ffffffff812ae3a9。使用dis命令来反汇编RIP地址,可以看到汇编指令。
在crash工具中,使用dis命令(反汇编disassemble的缩写),可以显示给定函数或代码地址的机器指令。
crash> dis -r ffffffff812ae3a9
第1行0xffffffff812ae3a0<strnlen>: push %rbp
第2行0xffffffff812ae3a1<strnlen+1>: test %rsi,%rsi
第3行0xffffffff812ae3a4<strnlen+4>: mov %rsp,%rbp
第4行0xffffffff812ae3a7<strnlen+7>: je 0xffffffff812ae3d7<strnlen+55>
第5行0xffffffff812ae3a9<strnlen+9>: cmpb $0x0,(%rdi)
从反汇编查到的结果看到5行汇编指令,我们逐行解读下。
第1行,这是函数的开始,push %rbp 将基指针(rbp)压入堆栈。
第2行,test指令检查rsi寄存器的值,检查传入的字符串是否为空。
第3行,将堆栈指针(rsp)的值移动到基指针(rbp)。
第4行,如果rsi为零(即test指令的结果为零)则退出函数。
第5行,这条指令将rdi寄存器指向内存地址的字节与空字符(0x0)做比较,意思是字符串最后一位没有字符了就退出。
这段反汇编将函数strnlen+9之前相关的汇编指令动作调了出来,可以看出执行了一个字符串相关的函数。
这时候我们想到crash还有个sym命令可以查看RIP地址对应的源码信息,能更加直观看到程序运行情况。在crash工具,使用sym命令来寻找源码。
crash> sym ffffffff812ae3a9
ffffffff812ae3a9 (T) strnlen+9 /usr/src/debug/kernel-2.6.32-754.35.1.el6/ linux-2.6.32-754.35.1.el6.x86_64/lib/string.c: 407
使用sym命令查看内存地址对应的源码信息
该地址的源码指向了/usr/src/debug/kernel-2.6.32-754.35.1.el6/linux-2.6.32- 754.35.1.el6.x86_64/lib/string.c,源码文件第407行。
毕竟Linux内核是开源的嘛!要是闭源系统,我们就无法自行分析了,就只有找原厂了。
我们在开源社区顺利找到的源码[3],这段完整代码如下:
size_t strlen(const char *s) {
const char *sc;
for (sc = s; *sc != '\0'; ++sc) /*407行*/;
return sc - s;
}
这段代码实现了计算字符串长度功能,核心部分是通过for循环,从输入字符串初始字符s开始,遍历其所指的内容,循环执行直到遇到字符串结尾的空字符'\0',最后输出字符串长度。
根据我大学学过但快忘却的汇编知识,依稀可以判断前面的汇编代码和C代码是同一个意思。毕竟人家都是从同一个函数的地址得来的,只是前面我们反汇编查了其汇编指令,后面查了函数源码。
PS:RIP寄存器作用是存放下一条要执行的指令,说明ss进程正在或者下一步就要计算字符串长度。这说明故障时ss进程正在执行字符串相关操作。
查到这一步我们会有点懵,因为还是无法洗清ss进程的嫌疑。ss是触发者没错,但是它任劳任怨、常年无休,确保了自动化服务正常。这个黑锅可不能让它背。
八、到底谁是元凶
为了守护数字世界的光明与正义,我们决心将内核崩溃时内核运行的详细情况仔细检查几遍。这时候我们想到了crash工具的log命令,使用log命令查到的信息如下:
crash> log
...
VMAGENTMOD: 3846: init_module: get into init_module, syshook_enable:1
VMAGENTMOD: 177350: cleanup_module: get into cleanup_module
...
因为log信息长达好几十页,为便于展示这里省略了无关信息。在log信息中仔细翻找,果然收获了惊喜,故障服务器内核崩溃发生前内核卸载和初始化了某个驱动模块(见上面的代码段)。VMAGENTMOD,从命名字上大概判断是某个工具的客户端驱动的代称,相关的进程号是3846和177350。
进程查询自然难不倒我们,因为之前我们就使用过ps命令查询进程信息。经查询,这2个进程号都属于防病毒工具的icsfilesec进程。它调用的cleanup_module是内核函数,用于卸载驱动模块。
卸载一个第三方驱动程序不会无缘无故发生,只可能是故障前有人在执行了停防病毒工具服务的操作。查看防病毒工具客户端日志,里面显示停防病毒工具进程服务的时间正好与log信息、服务器重启时间关联上。
从故障现象上防病毒工具的罪名呼之欲出,但是必须要有充分的证据才能彻底给其定罪,同时给ss进程洗白。
因为log信息显示故障可能与卸载驱动有关,我们想到crash工具正好有mod命令,可以查看内核模块加载信息。使用参数-t可直接显示出可能和故障相关的驱动模块,显示信息如下:
crash> mod -t
NAME TAINTS
syshook_linux (U)
vmsecmod (U)
从显示信息看,syshook_linux、vmsecmod驱动可能与故障相关,U代表驱动属于第三方软件。这两个驱动都是防病毒软件的驱动。哪个驱动与可能故障相关?当然是故障前被卸载的那个驱动。在log信息中查到“[last unloaded: vmsecmod]”,最后卸载的驱动模块就是vmsecmod。
PS:安全工具常使用系统调用来实现某些功能,也可能会使用slab内存池高效方便的特性来实现某些安全功能。syshook_linux驱动模块从命名上可以判断和Linux系统调用相关,一般不涉及变量和数据保存,不可能用到slab缓存。vmsecmod模块和安全功能相关,判断可能是该模块使用了slab缓存。
我们回想一下,最开始使用sys命令查到的报错信息”BUG: unable to handle kernel paging request at ffffffffa0395070″,即内存地址访问异常。vmsecmod驱动坑ss进程,很大可能就是修改了ss进程要访问的内容。访问的内存地址是什么呢?自然是ffffffffa0395070。这个地址还出现在了bt命令查询到的调用栈信息里,作为一个参数保存在寄存器里被ss进程访问。
我们想到之前使用过的crash工具sym命令,通过查看这个地址的源码来了解这个地址的意义,查询信息如下:
crash> sym ffffffffa0395070
ffffffffa0395070 (r) hash_info_mempool_name [vmsecmod]
查询信息非常震撼,这个异常访问地址的源码是hash_info_mempool_name,来自vmsecmod驱动,它可能是个变量名或函数名。
到目前总算找到了可以洗白ss进程的证据。
服务器自动化工具定时执行ss命令,ss进程每次都会去查询slabinfo(PS:简单理解就是遍历slab缓存链表),防病毒客户端使用了slab缓存对象,并且使用了slab缓存来存放防病毒客户端的变量、数据。而故障就是在ss进程访问防病毒客户端存放在slab缓存中的某个变量时发生的。
通过log信息,我们得知,故障前有人卸载了防病毒的驱动。防病毒客户端本地日志也显示,服务器重启前正好执行了停止防病毒进程的操作。按道理,停止了服务,卸载了驱动,防病毒相关的内存理应被释放掉。ss进程在遍历slab缓存链表时,就不会再访问到防病毒工具的变量了。然而ss进程居然还在访问这个变量,说明防病毒进程的内存未正常释放!
九、原来是防病毒工具的bug
我们首先将故障服务器的防病毒客户端版本、操作系统版本、内核版本信息等信息发给了防病毒工具研发工程师。随后研发工程师定位到,hash_info_mempool_name变量来自客户端某个安全功能模块的源代码。
经过厂商研发工程师分析确认,前述版本的防病毒客户端在红帽6操作系统上实现该安全功能存在软件缺陷。这个缺陷可简单描述如下:

该安全功能具体是在vmsecmod驱动模块中实现。当启动客户端进程服务时,内核加载vmsecmod驱动,在内存为vmsecmod驱动创建内存块,该安全功能创建了hash_info_mempool_name变量存在驱动所在的内存块。同时该安全功能在实现时为了提高代码效率使用了slab缓存创建slab缓存对象(图中kmem_cache n)。kmem_cache n缓存对象包括很多变量和函数,其中有个字符指针name指向了hash_info_mempool_name变量的内存地址。
正常情况下,定时执行ss命令会遍历slabinfo,也就是slab缓存链表,逐个遍历每个缓存对象的变量和函数。当遍历到kmem_cache n对象时,检查发现name变量指向vmsecmod驱动的变量。
当卸载vmsecmod驱动时,该驱动所在内存块被释放,但是slab缓存对象kmem_cache n仍然存在,此时这个slab缓存对象的name指针指向的内存地址不再存在,没有设置成NULL,也就是变成了悬空指针。此时如果执行ss命令,会发现这个指针异常,并报错“内存访问异常”,内核崩溃。
到此,故障真相已全部解密。我们可以看出vmcore并非包治百病的灵丹妙药,某些情况下并不能完全靠它来找到故障根因。如果涉及第三方软硬件,还必须依靠厂商去分析其产品方面的原因。
十、那天发生了什么?
让我们站在上帝视角来回顾那天故障发生的始末。在那个夜晚,某个工程师按计划在准生产环境服务器更新防病毒软件许可。他先在防病毒工具管理端导入软件许可,接着管理端服务器将软件许可分发给每台服务器客户端,接着客户端本地更新许可文件。
在本地服务器的客户端更新许可文件时,操作系统卸载和加载了vmsecmod驱动模块,因客户端软件有缺陷,在红帽6服务器上出现了slab内存池的残留。
而服务器上部署的自动化工具,定时执行ss命令,并会通过操作系统内核查询
十一、现在你会了吗
实际小白很难通过本文来完全搞清楚如何分析vmcore。确实是标题党实锤了。本文的初心是想提供一个案例让大家熟悉基本分析思路,对处理Linux内核崩溃相关故障不再发怵。本文提供的案例是一类比较复杂的故障场景,如果能跟着分析几遍,相信再遇到其他故障也会游刃有余。当然小白最好还是要先从独立搭建分析环境开始,可以自己运行几个简单的BUG程序触发Linux内核崩溃,然后尝试对生成的vmcore做独立分析。通过多次练手就可以熟练使用crash工具分析vmcore。
分析vmcore大多数情况下并不能直接帮我们找到故障原因,必须结合故障现象,充分利用各类信息,逐步明确故障根因。 尽管vmcore客观记录了内核崩溃前的各类信息,但是它并不会直接告诉我们故障发生的真实原因。很可能出现如下复杂情形:报错信息显示是A程序触发,检查发现跟B程序相关,实际C程序也存在软件缺陷导致故障发生。也可能出现:报错信息显示是A程序触发,但是vmcore记录信息实在太少,操作系统日志和硬件日志也都没有信息,导致根本无法进一步查明原因。
这就像探案一样,每一个细节都可能是解开谜团的关键。
- https://www.bilibili.com/video/BV1qT421m7pF/?vd_source=84b4b04a47fa6a4c3cbbfc4984131c9a。
- https://mirrors.ocf.berkeley.edu/centos-debuginfo/7/x86_64/
- https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/lib/string.c?h=v6.9.8
